Туториалы
19 июля 2021
Туториалы
19 июля 2021
Принцип KISS для разработчиков на языке Swift

Что такое KISS и откуда?

KISS (все буквы в заглавной) - это аббревиатура от слова Keep it simple, stupid. Или формально: «Сделайте это просто и понятно» [1]. Это принцип, который гласит, что система должна быть спроектирована таким образом, чтобы впоследствии было легко понять внутреннее устройство. В результате внесение любых изменений потребует минимальных усилий. Считается, что этот принцип был изобретен авиационным инженером Келли Джонсон. Создавая реактивный самолет в качестве ведущего инженера, Келли руководил своими конструкторами, чтобы система оставалась достаточно простой, чтобы любой, у кого есть начальная подготовка механика и основные инструменты, мог отремонтировать ее в боевой обстановке [3]. Если система работает очень хорошо, но переконфигурировать/отремонтировать сложно, значит, конструкция не соответствует принципу KISS. Этот принцип актуален и полезен для любой области разработки системы. Например, для аэрокосмических инженеров для разработки авиационной системы или для производителя телевизоров для создания пульта дистанционного управления и, конечно, для разработчиков ПО для разработки программного обеспечения.

Цель статьи

В первую очередь инженеры-программисты пишут код, пишут тесты модулей/пользовательского интерфейса, выбирают архитектуры и делают многое другое. Поскольку написание кода является самым фундаментальным в программной инженерии, эта статья  будет посвящена только этому процессу. В этой статье мы пытается показать, можно ли заменить трудный для понимания и подробный код более простым и лаконичным, сохраняя при этом то же поведение. Таким образом, он соответствует принципу KISS. Принцип объясняется путем сравнения фрагментов кода в пяти различных сценариях. Несмотря на то, что код написан на Swift, сами принцип и его основные идеи можно применить к другим языкам программирования.

Мотивация

Принципы разработки программного обеспечения, например SOLID, DRY, YAGNI, KISS и т. д., Сводятся к общей цели - написанию "здорового" кода, который можно легко читать, расширять и поддерживать. KISS настоятельно рекомендует писать простой код. В этом есть много плюсов. Например:

  • Облегчение жизни других разработчиков: написание кода - это работа, но написание хорошего кода - это ответственность. Если кто-то напишет простой код, другие программисты могут легко это прочитать и понять. Это облегчает жизнь каждому.
  • Самолюбие: часто разработчикам приходится заглядывать в свой старый код, чтобы исправить ошибки или добавить новые функции. Если этот код написан сложно и запутано, то разработчик будет страдать от своей собственной работы. Итак, написание детализированного и простого кода - это благотворительность для самого себя.
  • Лучший код: простой код - это не обязательно хороший код, это характеристика хорошего кода. Если можно добавить простоты, то уже сделан один шаг к лучшему коду. Согласно «Бритве Оккама», «простейшее объяснение какого-либо явления с большей вероятностью будет точным, чем более сложные объяснения» [4]. Имея это в виду, если есть несколько способов написать фрагмент кода, самый простой, вероятно, будет точным. Может показаться расплывчатым связывать программирование с чем-то вроде бритвы Оккама, имеющей глубокие корни в литературе и философии, но некоторые исследования уже пытались смягчить этот пробел. [5] [6] [7].
  • Лучший кодер: Не факт, но некоторые думают, что Альберт Эйнштейн однажды сказал: «Если вы не можете объяснить это шестилетнему ребенку, вы сами этого не понимаете». Н так важно действительно он сказал это или нет, но идея имеет смысл. Способность выразить что-либо прямо и просто отражает твёрдое понимание этого. Таким образом, чем более выразительным умением обладает разработчик, тем более опытным он является. Мастерства можно достичь практикой, опытом и, что наиболее важно, желанием.
    Код с меньшим количеством ошибок: немного самоуверенно, но тем не менее иногда разработчики склонны добавлять ненужную абстракцию без предусмотрительности. Может быть сделать код более многоразовым и модульным? Для этого нужно добавить буквально несколько дополнительных слоев кода. Цель этого кода - просто удовлетворить абстракцию. Но реальный код, имеющий бизнес-логику, остается глубоко внутри туманного фасада. Через несколько дней появляется новое требование, и теперь старая абстракция не соответствует новым требованиям, а значит нужно ее менять. Эти изменения добавляет новый код, который заставляет абстракцию соответствовать новому требованию, а также гарантирует, что существующие требования не нарушаются. Часто эти слои являются условными выражениями, и если неаккуратно поработать с ними, то можно породить сложно вылавливаемые логические ошибки. Простой код не противоречит абстракции. Однако абстракция должна быть целенаправленной и адаптивной. Так что в будущем они не станут уязвимыми для ошибок.

Заткнись и покажи мне (простой) код

Теперь давайте рассмотрим некоторые подходы и посмотрим, может ли простое написание кода улучшить читаемость и расширяемость.

Сокращения

Один из способов сохранить кодовую базу менее загроможденной - использовать сокращения там, где это возможно. Одним из таких сокращений является тернарный оператор (? :). Это очень элегантно, но зачастую им пренебрегают. Давайте рассмотрим приведенный ниже код.

if isSuccess {
    label.textColor = .black
} else {
    label.textColor = .red
}

Для приведенного выше кода предположим, что от внутреннего сервера получен логический флаг ответа, который сохранен в переменной isSuccess. В зависимости от значения цвет текста метки обновляется, а затем он будет использоваться для отображения некоторого сообщения пользователям. Здесь использовалась инструкция if-else.
Инструкция может быть написана с использованием тернарного оператора, как показано ниже, и результат будет таким же.

label.textColor = isSuccess ? .black : .red

Последний блок кода является более кратким и легко читаемым. Вместо четырех строк, тут используется всего одна строка. Хотя остается спорным, определяет ли меньшее количество строк лучший код, в данном случае этот вариант воспринимается легче, чем выражение if-else. Хотя одно предостережение - не нужно использовать вложенные тернарные операторы, поскольку это может привести к затруднению чтения кода.
Некоторые другие сокращения: оператор объединяющий по nil, сокращенный синтаксис параметров, комбинированные базовые операторы (+=), и т. д.

Адекватные альтернативы

Следуя принципу KISS, следует помнить одну вещь - не стрелять из пушки по воробьям. Например, при определении метода можно сначала подумать о вычисляемом свойстве. Если вычисляемого свойства достаточно, то нет необходимости использовать метод. В приведенном ниже коде есть метод getAgeAfterFiveYears(), который возвращает возраст человека, который он будет иметь через пять лет.

struct Person {
    let name: String
    let age: Int
    var gender: GenderType? // GenderType is an arbitrary enum that has the possible gender cases
    
    func getAgeAfterFiveYears() -> Int { age + 5 }
}

Метод можно заменить вычисляемым свойством. Вычисляемые свойства - это особый вид свойств, которые не хранят значения. Вместо этого они вычисляют значение на основе других свойств и возвращают его. [8]

var ageAfterFiveYears: Int { age + 5 }

Теперь и метод, и вычисляемое свойство возвращают одно и то же значение. Таким образом, у них одинаковое поведение. Но для вызывающей стороны вычисляемое свойство - это просто еще одно свойство, а не метод.

let person = Person(name: "David Fincher", age: 58)
person.getAgeAfterFiveYears() // возвращает 63
person.ageAfterFiveYears // возвращает 63

Короче говоря, если метод очень простой, например, он не принимает никаких аргументов, не выполняет тяжелой работы и т. д., То, вероятно, это не метод, а хороший кандидат на вычисляемое свойство. Здесь достаточно вычисляемого свойства, и его использование не только следует принципу KISS, но также следует так называемому способу написания кода Swifty. Всегда разумно искать альтернативные способы, которых достаточно и не являются избыточными.

Заметка

Может быть непонятно, почему у getAgeAfterFiveYears() и ageAfterFiveYears нет ключевых слов возврата. Это потому, что они используют так называемый неявный возврат. [9]

Синтаксический сахар

Языки программирования уже предлагают разработчикам некоторый синтаксический сахар, чтобы они могли избежать написания многословного кода. Вспомним структуру Person, определенную в предыдущих примерах. Предположим, существует массив с именем people, который содержит объекты класса Person. Теперь цель состоит в том, чтобы узнать индекс элемента из этого массива, у которого свойство name равно «Chloé Zhao». Ниже приведен один из способов сделать это.

var index: Int? // свойство для нашего индекса
for i in 0 ..< people.count {
    if people[i].name == "Chloé Zhao" {
        index = i
    }
}
print(index) // выведет значение индекса

Альтернативный способ добиться того же результата - использовать метод firstIndex(where : ) типа коллекции Array, предлагаемый самим языком Swift.

let index = people.firstIndex { $0.name == "Chloé Zhao" }
print(index) // выведет индекс/code>

Есть еще такие варианты синтаксического сахара в Swift: последующие замыкания, обертки свойств, if-let и прочие.

Заметка

Прежде чем двигаться дальше, в первом примере использовался цикл for, для которого временная сложность равна O(n). То же верно и для второго примера [10]. Под капотом firstIndex(where : ) выполняет ту же (или аналогичную) операцию. Поэтому последний пример предпочтительнее для простоты, но никак не улучшает временную сложность.

Функции высшего порядка

Функции высшего порядка - это функции, которые принимают одну или несколько функций в качестве аргументов или / и возвращают функцию в качестве своего результата. [11]. Эти функции из вселенной функционального программирования (ФП). Некоторые языки являются симбиотическими, поскольку в них сосуществуют элементы, заимствованные из других языков. К счастью, Swift - один из них, поскольку его типы коллекций поддерживают некоторые функции более высокого порядка. Например, sorted, map, flatMap, reduce и т. д. В некоторых случаях функции высшего порядка могут добавить простоту и лаконичность коду. Предположим, что необходимо заполнить массив имен из массива people, упомянутого в предыдущих примерах. Наиболее вероятный подход, который можно было бы использовать инстинктивно, был бы следующим:

var names = [String]()

for person in people {
    names.append(person.name)
}

print(names) // выведет ["David Fincher", "Thomas Vinterberg", "Chloé Zhao", "Lee Isaac Chung", "Emerald Fennell"]

Все то же самое можно сделать в стиле ФП:

var names = people.map { $0.name }
print(names) // prints ["David Fincher", "Thomas Vinterberg", "Chloé Zhao", "Lee Isaac Chung", "Emerald Fennell"]

Это может выглядеть как синтаксический сахар. Но это совсем другое. Функции высшего порядка гораздо более гибкие по своей природе. Например, map применяет общую операцию к каждому элементу коллекции. Операция обеспечивается через замыкание в качестве аргумента.
На сайте raywenderlich.com есть хорошая статья Уоррена Бертона с примерами ФП и функций высшего порядка [12].

Согласованность

В некоторых случаях полезно пожертвовать некоторой простотой, чтобы получить более расширяемый код. Это альтернативная стоимость, уплачиваемая за достижение положительной согласованности всей кодовой базы. Давайте начнем с примеров.

func showTower(title: String, description: String, levels: Int) {
    print(title)
    print(description)
    print(levels)
}

let difficulty = "novice"

if difficulty == "novice" {
    showTower(title: "Novice", description: "All Too Easy!", levels: 8)
} else if difficulty == "warrior" {
    showTower(title: "Warrior", description: "You Will Die Mortal!", levels: 10)
} else if difficulty == "master" {
    showTower(title: "Master", description: "You Will Never Win!", levels: 12)
} else {
    showTower(title: "Unknown", description: "Unknown!", levels: -1)
}

Приведенный выше код, основанный на константе difficult, печатает некоторую информацию о башнях. Представьте, что башни взяты из видеоигры Mortal Kombat 3. Очевидно, что if-else не выглядит лаконичным, и сопоставление строк небезопасно. Это также не является перспективным, потому что, если появятся новые значения сложности, компилятор не сможет сказать, что нужно добавить новые блоки else-if. Программист должен позаботиться о том, чтобы добавлять их самостоятельно.
К счастью, с помощью enum (с необработанными значениями), typealias, кортежей и вычисляемого свойства все упомянутые проблемы могут быть решены.

enum Difficulty: String {
    typealias DifficultyContent = (title: String, description: String, levels: Int)
    
    case novice
    case warrior
    case master
    case unknown
    
    var content: DifficultyContent {
        switch self {
        case .novice:
            return (title: "Novice", description: "All Too Easy!", levels: 8)
        case .warrior:
            return (title: "Warrior", description: "You Will Die Mortal!", levels: 10)
        case .master:
            return (title: "Master", description: "You Will Never Win!", levels: 12)
        case .unknown:
            return (title: "Unknown", description: "Unknown!", levels: -1)
        }
    }
}

func showTower(for difficulty: Difficulty) {
    print(difficulty.content.title)
    print(difficulty.content.description)
    print(difficulty.content.levels)
}

let difficulty = Difficulty(rawValue: "novice")

showTower(for: difficulty ?? .unknown)

Теперь всякий раз, когда появляется новый тип сложности, например, Легендарный, в enum Difficulty нужно добавлять только новый кейс. Компилятор выдаст ошибку, и пока этот случай не будет учтен в инструкции switch, код не будет выполняться. Так что программист не может случайно пропустить кейс при возникновении ошибки.
Вместо typealias можно было бы использовать структуру, но для этого нет веской причины. Потому что, скорее всего, структура не будет использоваться в других местах.

Заметка

По-видимому, второй подход более многословный, чем первый. Более того, для новичков в Swift код может показаться отталкивающим. Но, несмотря на это, второй подход легко расширяем и является более безопасным. Хотя это звучит как оксюморон, иногда простоты можно достичь за счет некоторого количества контролируемых усложнений, баланса между чрезмерной и недостаточной инженерией.

Последние слова

Спасибо всем, кто нашел время, чтобы прочитать. Надеюсь, эта статья оказалась для вас полезной. Несомненно, написать хороший код непросто, и я считаю, что это долгий путь в карьере, и одной статьи или книги недостаточно. Я нахожусь в пути, и я попытался записать некоторые полезные советы, которые я узнал на сегодняшний день. В качестве итога хочу посоветовать стараться следовать KISS - писать простой для чтения код. И мы, разработчики Swift, предложим новую расшифровку аббревиатуры для KISS. «Keep it simple and Swifty» :)

Оригинал статьи


Оцените статью
0
0
0
0
0

Чтобы добавить комментарий, авторизуйтесь
Войти
Акулов Иван Борисович
Пишет и переводит статьи для SwiftBook