Найти в Дзене
Я, Golang-инженер

#67. KISS и DRY в Golang. Примеры плохого и хорошего кода, сфера применения принципов и история возникновения

Это статья об основах программирования на Go. На канале я рассказываю об опыте перехода в IT с нуля, структурирую информацию и делюсь мнением. Хой, джедаи и амазонки! Разбираюсь с тем, что стоит за известными IT-терминами. В публикации я покажу примеры кода с принципами KISS, DRY и без них, чтобы было ясно, в чём преимущество программирования с соблюдением принципов. Также расскажу об истории появления принципов и поделюсь своим видением применения принципов. Go! Принцип предполагает упрощать то, что можно упростить. По-английски принцип расшифровывается так: «Keep it simple, stupid» - «Делай проще, тупица». Грубо, не так ли? Вспоминаю историю из машиностроения, когда я практиковал бережливое производств в вертолётостроении. Тогда я активно изучал всё что с этим связано, чтобы разобраться - как можно применить у себя: Один из создателей "бережливого производства" или "Toyota Production System", Сигео Синго (1909-1990), японский инженер-промышленник на Тойота, ввёл понятие бака-ёкэ ил
Оглавление

Это статья об основах программирования на Go. На канале я рассказываю об опыте перехода в IT с нуля, структурирую информацию и делюсь мнением.

Хой, джедаи и амазонки!

Разбираюсь с тем, что стоит за известными IT-терминами. В публикации я покажу примеры кода с принципами KISS, DRY и без них, чтобы было ясно, в чём преимущество программирования с соблюдением принципов. Также расскажу об истории появления принципов и поделюсь своим видением применения принципов. Go!

1. KISS

1.1. Историческая справка

Принцип предполагает упрощать то, что можно упростить.

По-английски принцип расшифровывается так: «Keep it simple, stupid» - «Делай проще, тупица». Грубо, не так ли?

Вспоминаю историю из машиностроения, когда я практиковал бережливое производств в вертолётостроении. Тогда я активно изучал всё что с этим связано, чтобы разобраться - как можно применить у себя:

Один из создателей "бережливого производства" или "Toyota Production System", Сигео Синго (1909-1990), японский инженер-промышленник на Тойота, ввёл понятие бака-ёкэ или "защита от дурака", предполагающий невозможность использовать изделие неправильно. Защита от дурака, например, предполагает изменение конструкции таким образом, чтобы её невозможно было установить не той стороной:

Модули ОЗУ: фото Википедия
Модули ОЗУ: фото Википедия

На иллюстрации на планках ОЗУ установлены выемки, препятствующие неверной установке планок в материнскую планку. Пример "защиты от дурака" в конструкции. И хотя пример не из Toyota, а ближе к IT, он показателен.

Однажды в цеху к Сигео Синго подошёл рабочий и сказал: "Что значит защита от дурака? Это кто дурак?" С тех пор начали использовать термин "защита от ошибок" или "пока-ёкэ". В английской и советской терминологии также встречалось прилагательное foolproof, имеющее ту же суть.

Сигео Синго. Фото: https://alpinabook.ru/authors/sigeo-singo/
Сигео Синго. Фото: https://alpinabook.ru/authors/sigeo-singo/

Один из эвфемизмов аббревиатуры KISS звучит так: "«keep it super simple»" - держи это супер простым, - мне нравится такая расшифровка.

Аббревиатура KISS ассоциируется с американским авиаконструктором компании Lockheed Кларенсом Джонсоном, 1910-1990.

Принцип KISS характеризует история, когда Джонсон вручил команде инженеров-авиаконструкторов относительно простой набор инструментов для техобслуживания во время разработки новой конструкции самолёта, и потребовал:

Механик среднего уровня должен отремонтировать реактивный самолёт (который вы проектируете) в полевых условиях только с этими инструментами.

Если смотреть со стороны программирования, код, написанный по принципам KISS, должен быть легко обслуживаемым программистом невысокого уровня.

Можно утверждать, что системы работают лучше, когда они остаются простыми, а не усложняются. И я не случайно в начале привёл аналогию KISS с концепцией "защита от ошибок". Помню одну из тез во время обучения:

Надёжность системы всегда меньше надёжности самого ненадёжного компонента.

Интуитивно, надёжность объектов связывают с хорошей работой и недопустимостью отказов.

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

Кстати, в уже утратившем силу советском ГОСТ 27.002-89 «Надёжность в технике...", в поясняющем приложении было прямое упоминание термина «foolproof» как одного из показателей надёжности человеко-машинных систем:

Границ понятия "надёжность" не изменяет следующее определение: надёжность - это свойство объекта сохранять во времени способность к выполнению требуемых функций в заданных режимах и условиях применения и технического обслуживания...
… Для характеристики отказоустойчивости по отношению к человеческим ошибкам в последнее время начали употреблять термин fool-proof concept.

В обновлённом ГОСТ 27.002-2015 «Надёжность в технике...» это разъяснение было удалено.

1.2. KISS в программировании

Кто именно ввёл принцип KISS в IT сказать трудно. Вероятно, это нечто естественное, что постепенно в опыте и на интуитивном уровне начинают выполнять опытные инженеры, и будет полезно осознать инженерам начинающим.

Как реализация принципа KISS может выглядеть в Golang и вообще, в программировании? Помним, что механик должен починить реактивный самолёт в поле, - а мы, те самые конструкторы, которые разрабатывают реактивный самолёт (сервис): он должен не только быстро летать, но и легко подвергаться обслуживанию в любых условиях: напрямую на сервере, во внешнем репозитории или на локальной машине. А ещё мы можем усложнять код - если это позволит упростить его обслуживание. Парадокс? Как бы не так, мы в этом убедимся далее.

Кто может быть тем инженером, обслуживающим реактивный самолёт код? Например, новый программист, которого вводят в курс дела и знакомят с проектом или тестировщик, да и вы сами - когда откроете код для расширения через месяц.

Выделю несколько элементов, характерных для KISS в качестве упрощения обслуживания кода, которые я считаю логичными исходя из своего опыта. Если знаете, что ещё сюда можно добавить, напишите в комментарии.

1.2.1. Ветвления

Избегаем сложных условий, где if в if'е. По-возможности вообще избегать else, т.к. они затрудняют чтение кода. Это часто лечится оптимизацией кода, т.к. сразу сложно может быть понять оптимальное ветвление:

Ниже сложный для понимания фрагмент кода:

Сложные ветвления
Сложные ветвления

А этот фрагмент кода я считаю более читабельным, хотя они могут повторять некоторые условия - т.е. идёт усложнение кода в плане увеличения объёма кода, но упрощение кода в плане последующей поддержки кода:

Ветвления попроще
Ветвления попроще

1.2.2. Простые функции

Функции/методы не должны быть сложными: с длинной логикой и возвращать множество значений. Вместо чего-то такого:

Код со множеством возвращаемых значений
Код со множеством возвращаемых значений

Полезнее сделать структуру, и возвращать экземпляр структуры:

Код со структурой
Код со структурой

Если функция возвращает больше 7 или даже 4 значений, напомни себе - это много. Стоит разделить функцию на несколько функций, или создать структуру. Это как раз пример видимого усложнения кода, которое, однако, систематизирует программу и позволяет при повторной работе с кодом, упростить работу.

1.2.3. Понятные имена

Использовать понятные имена для переменных, функций, структур и т.д. Корректные имена - это целое искусство - как дать понятное имя функции, константе и т.д. Вот пример кода с непонятными именами:

Код с непонятными именами
Код с непонятными именами

В этом коде непонятно, что делают функции f и g. Более-менее понятное имя у переменной res, которая охарактеризовать её значение, как результат чего-то; хотя имя и этой переменной можно улучшить:

Код с именами по-KISS
Код с именами по-KISS

multiply чётко указывает, что делает функция. printNumbers также ясно показывает читатель кода, что делает функция.

Переменную res переименовал в resMultiplication, которая показывает что за результат мы получаем.

Это также можно назвать усложнением кода в плане увеличения количества символов, но это упрощение кода в плане последующей поддержки кода.

Сюда ещё можно добавить комментирование перед функциями, но я пока с этим не особо работаю, да и слышал, что комментарии никто не читает, если всё понятно. А смысл KISS - в т.ч. писать понятный код без комментирования того, что мы делаем кодом.

1.2.4. Следовать формальным требованиям языка

В каждом языке есть общие требования к коду. Они помогают разработчикам легче воспринимать код. Вот некоторые общепринятые вещи для Go (если знаете ещё требования - пишите в комментарии):

  • Форматировать код через функцию gofmt. Если мы пишем код в IDE, а не текстовом редакторе типа блокнота для Windows или nano для Linux, функция gofmt автоматически применяется при сохранении.
  • Использовать CamalCase для именования типов данных. Например, для функции писать не do_something() или dosomething(), а написать DoSumething(). Под типами данных я подразумеваю все типы: базовые (инты, булевы, строки, флоаты, руны и т.д.); агрегатные типы данных - массивы и структуры; ссылочные типы данных - указатели, срезы, каналы, карты; и прочие типы данных - методы, функции и интерфейсы.
  • Использовать английские буквы для именования типов данных, и не писать транслитом. Т.е не нужно присваивать типам данных имена kvadrat или квадрат, а следует написать square. Если требуется - пусть имя переменной будет длинным, если оно требуется для описания её значения.
  • Для булевых типов данных именование в виде вопроса: isActive, hasPermission, isInitialized.
  • Для целых чисел используют имена, описывающее число: count, totalItems, userID.
  • Для строк следует делать описательными: userName, emailAddress, messageBody.
  • Руна обычно используется для отображения символов. Наименования рун могут быть связаны с контекстом использования: char, initial, separator.
  • Массивы и срезы называть во-множественном числе: nums, values, users, items и т.д.
  • Пользовательские структуры обычно называются в единственном числе существительным: User, Product, Order.
  • Экземпляры структур можно использовать те же наименования, что и для структур: user или adminUserдля структуры User.
  • С каналами я толком не работал, но предположу, что имя канала должно отражать помимо передаваемых данных тип канала, или явное указание на тип: messageChannel, taskQueue, errorChannel.
  • Для карт имя может характеризовать ключ и значение, где ключ в единственном числе, значение - во множественном: userScores, productPrices, carSpeeds, studentGrades и т.д.
  • Для указателей рекомендации те же, что и базовых типов данных. Можно добавить Ptr - сокращение от pointer: configPtr, userPointer, listNodePtr.
  • Для функций и методов наименования должны отображать действие, выполняемое функцией, и часто начинаются с глагола: CalculateTotal, SendMessage, LoadUserData.
  • Экспортируемые функции должны быть выше неэкспортируемых, т.к. переходя в пакет, нас в первую очередь интересуют импортируемые. При соблюдении этого требования, функции должны быть расположены примерно в порядке их вызова в коде.
  • Для получателей (ресиверов) в методах наименования краткие, в виде одного-двух символов, с которых начинается имя структуры. Так, для структуры User, получатель в методе будет u: func (u User)... Для структур с более детализированными названиями, получатели в методах тоже могут иметь более содержательное наименование: для структуры DatabaseConnection получатель можно назвать dbConn.
  • Не используйте префикс get в именах функций/методов. Например, вместо GetUser() напишите User(). И ничего страшного, если у нас есть одноимённая структура User, которую может возвращать функция User(). Возможно более содержательные имена также приветствуются: FindUser(), LoadConfig().
  • Для интерфейсов использовать суффикс "er", а название составлять из методов, входящих в интерфейс. И не использовать слова Interface в имени. Например, интерфейс с методами Read и Write назвать ReadWriter.
  • Для именования пакетов использовать краткое описание названия в единственном числе без подчёркиваний и заглавных букв. Например, если пакет представляет утилиты, можно назвать его client, а не clients.

Ещё примеры по стилю языка можно смотреть в руководствах, например в таком: >>>клик<<<

1.2.5. Обработка ошибок

Здесь опыта у меня немного, предположу, что ошибки должны быть возвращены из функции, а не обрабатываться внутри функции. А также должны быть ясные сообщения об ошибках, чтобы разработчики могли быстро понять, где проблема, если она возникла.

1.2.6. Документирование

Если комментируем функцию, пакет или структуру, то начать комментарий нужно с имени этой функции, пакета или структуры. Например:

Работа в IDE
Работа в IDE

Кстати, если наведём курсор на имя функции printNumbers в функции main, то получим подсказку, что делает функция - удобно, когда функция из другого пакета, чтобы не искать её для понимания, что же она делает (если вдруг это не понятно из названия функции):

Работа в IDE
Работа в IDE

Документировать полезно сложные участки кода, в которых будет сложно разобраться стороннему разработчику. А документировать каждую подряд функцию или структуру, или ещё что-то - плохая идея.

1.2.7. Разделение кода

Полезно разделять код на логические единицы. А не писать весь код в одном файле. Это упрощает понимание кода.

Не нужно писать много кода в пакет main. Например, идеальный файл пакета main может содержать чтение начальных настроек, создания экземпляра структуры и запуск метода с этой структурой - 20 строк вместе с импортами и объявлением пакета.

1.2.8. Глобальные переменные

Минимизируйте глобальные переменные (ГП). Напомню, глобальные переменные - объявленные вне функций. Они могут привести к нежелательным зависимостям и сложностям при тестировании. Например:

  • ГП могут быть изменены во многих участках кода, что делает их менее предсказуемыми: затруднена отладка.
  • Несколько тестов могут менять переменные, что повлияет на результат тестов.
  • Гонка данных, когда несколько горутин попытаются читать и записывать в ГП: непредсказуемый результат.
  • Код с ГП сложнее понять. Разработчику трудно разобраться, где и когда изменена ГП в ходе работы приложения.

1.2.9. Используйте интерфейсы

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

1.2.10. Разумный эмбендинг

Не используйте много вложений структур в структур. На мой взгляд вряд ли нужно делать больше трёх вложенных структур - иначе трудно разобраться.

1.2.11. Тестируйте код

Для повышения надёжности. Я поставил этот пункт лишь 11-м, но весь KISS ни к чему, если продукт не рабочий. Пожалуй, это самый важный элемент KISS - обеспечить работоспособность кода.

1.2.12. Рефакторинг

Выполняйте рефакторинг - это позволит обнаружить и устранить избыточную сложность, дублирование кода и т.д. Как пример - можно упростить конструкции if-else, придумать понятные имена и т.д.

1.2.13. Производительность и алгоритмы

Есть разные способы выполнить одну и ту же задачу. И не всегда более сложные способы - более эффективные. Но среди двух примерно одинаковых по сложности решений - предпочтение следует отдать более производительному. А если более сложный способ не эффективнее - тогда зачем он нужен?

Простой пример - чтение данных из стандартного ввода.

Функция ...fmt.Scan... считается намного медленнее, чем ...bufio.NewScanner(os.Stdin), когда требуется прочитать много элементов.

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

Вот прохожу я сейчас Алгоритмы 6.0. Проша первая неделя, я решил 3 из пяти задач. Одна задача, которая со слов преподавателя "посложнее" заняла у меня 290 строк кода. После рефакторинга 220 строк кода. А сегодня был разбор темы, и решение преподавателя было на 15 строках кода. Ладно, он писал на Python, а не на Go, а "питон" более подходит для алгоритмических задач в силу особенностей своего языка - более лаконичен для алгоритмов.

Смысл в чём - как думаете, что легче поддерживать: код из 15 или из 230 строк? Даже если они одной эффективности.

Это основные пункты, которые мне удалось выделить в программировании на основе принципа KISS.

1.3. Итоги с KISS

Думаю, что примеров можно привести намного больше: всё про логичность, последовательность и интуитивное понимание кода.

В общем, основной посыл KISS, это:

Писать код с реализацией требуемого функционала, задумываясь о том, как с кодом будут работать другие люди: всё сложное делать простым, а если что-то поможет сделать поддержку кода проще и требует усложнения - сделать это.

Откуда придёт понимание - что поможет сделать поддержку кода проще? Только с опытом. А пока его не очень много, можно ориентироваться на те 13 маяков, что я указал выше: имена переменных, ветвление условий и т.д.

KISS - основной принцип, фундамент, на котором, я считаю, базируются все остальные принципы в разработке: DRY, каждый из принципов SOLID и другие. Для себя я решил так.

2. DRY

2.1. История DRY

DRY - это аббревиатура на английское Don’t repeat yourself, т.е. не повторяй себя. Принцип сформулирован Энди Хантом (родился в 1964 г., США) и Дейвом Томасом (родился в 1956 г., США) в книге "Программист-прагматик", 1999 г., звучит он витиевато:

Каждая часть знания должна иметь единственное, непротиворечивое и авторитетное представление в рамках системы.

На человеческом принцип DRY означает нацеленность на снижение повторения информации различного рода.

Энди Хант. Фото: Википедия
Энди Хант. Фото: Википедия
Дейв Томас. Фото: Википедия
Дейв Томас. Фото: Википедия

Когда принцип DRY применяется успешно, изменение единственного элемента системы не требует внесения изменений в другие, логически не связанные элементы.

Когда я проектировал чертежи, согласно ЕСКД (Единая система конструкторской документации), одно из требований к размерам на чертежах - необходимое и достаточное (для однозначной трактовки геометрии).

В ВУЗе на предмете "инженерная графика" первой практической работой было нарисовать свободу, как мы её понимаем. Я нарисовал тогда двух самураев и наблюдающего за ними орла. А суть задачи была раскрыта на последующих лекциях - когда мы чертили конструкции и были свободны в выборе, какие размеры куда ставить; и от этого зависела сложность изготовления и контроля изделия. Хотя фактически, деталь можно изготовить при любом обозначении размеров (конечно, если размеров достаточно).

Пример с деталями возможно трудно осмыслить, если вы не получили близкое к машиностроению образование. Суть в том, что в программировании мы свободны во многих аспектах, но согласно принципу DRY, мы должны обеспечить отсутствие повторений, т.е. обеспечить "необходимость и достаточность" кода.

При написании кода, необходимо обеспечить его необходимость и достаточность.

Под необходимостью подразумевается реализация требуемого функционала. Под достаточностью - избегать дублирования фрагментов кода.

Какие бывают повторения в коде, и как с ними можно бороться? И со всеми ли повторениями нужно бороться?

2.2. Примеры повторений

Ниже я приведу несколько примеров повторений кода, потенциально с которыми полезно бороться.

2.2.1. Дублирование функций

Бывают ситуации, когда функции имеют схожий функционал, тогда может быть полезно их объединить. Пример кода без DRY:

Код
Код

Можно доработать функционал, избегая дублирования:

Код
Код

Код стал лаконичнее.

2.2.2. Дублирование структур и методов

Бывают ситуации, когда мы создаём структуры с одинаковыми полями. Зачем, если можно их объединить? Пример кода без DRY:

Код
Код

Оптимизируем код, устраняя дублирование структур и методов:

Код
Код

Код стал лаконичнее.

2.2.3. Сообщения

Бывают ситуации, когда нам часто нужно возвращать какое-то сообщение. Например, текст ошибки, в т.ч. с форматированием. Или другое сообщение. Пример кода без DRY:

Код
Код

Здесь у нас дублирование текста ошибки. Можно вывести её в константу:

Код
Код

Теперь текст ошибки не дублируется.

2.2.4. Функции

Пример, который я посмотрел в блоге Николая Тузова о разработке телеграм-бота.

Ситуация: у нас во всём коде одинаковым образом формируются сообщения об ошибках:

Код
Код

Можно обернуть эту функцию в библиотеку:

Код библиотеки
Код библиотеки

И в основной части кода использовать функцию библиотеки:

Код
Код

Т.е, было:

fmt.Errorf("не смогли отправить сообщение: %w", err)

Стало:

e.Wrap("не смогли отправить сообщение", err)

Пример может показаться недостаточно убедительным: мы что, выигрываем только в отсутствии печати символов ": %w", если библиотеку не использовать? Не совсем, во-первых, у нас становится однотипной обработка ошибок. А во-вторых, обработка ошибок может быть несколько сложнее, и её также можно вывести в библиотеку.

Обратите внимание на функцию WrapIfErr пакета e, см. иллюстрацию выше. Вот пример её использования в коде:

Код
Код

Здесь уже логика сложнее, и мы экономим не только на четырёх символах. Плюс однообразие при обработке ошибок - а это упрощение поддерживаемости кода.

2.3. Где DRY может быть не уместен?

Посмотрим ситуации, когда принцип DRY может быть не уместен.

2.3.1. Типы данных в сигнатуре функции

Пример, который я посмотрел в в том же блоге Николая Тузова о разработке телеграм-бота.

Код
Код

Функция New здесь создаёт нового клиента. В сигнатуре функции у параметров host и token один тип данных - string. Мы знаем, что если подряд идут параметры с одним типом данных, можно упростить и не повторяться:

Вместо
func New(host string, token string) *Client {
написать
func New(host, token string) *Client {

Короче? Короче. Полезнее? Не факт. Почему не факт? Конкретно в этом коде писался бот для Телеграма. Для бота Телеграма токен выглядит как набор различных символов. В теории, если мы хотим использовать наш бот для нескольких мессенджеров, в одном из которых токен может быть интовым, нам нужно будет менять функцию.

Казалось бы - её и так нужно будет менять, но просто понимание этой вещи - может натолкнуть на мысль, что предоставляемым функционалом языка необязательно всегда пользоваться.

2.3.2. Одинаковые функции в разных пакетах

Если у нас есть две похожие или даже одинаковые функции из разных пакетов, согласно принципа DRY мы можем создать библиотеку, куда поместим эту функцию.

Рассмотрим пример. По-легенде, у нас сервис, где помимо прочего есть два пакета - user и admin. В обоих пакетах есть проверка адреса электронной почты по длине символов. Если адрес электронной почты состоит менее чем из пяти символов, он считается невалидным:

Код пакета admin
Код пакета admin
Код пакета user
Код пакета user

Vы решили: зачем дублирование кода в проверке e-mail в разных пакетах? Сделаем новый пакет и поместим туда функцию ValidateEmail. Так и сделали:

Код пакета validation
Код пакета validation
Код пакета admin
Код пакета admin
Код пакета user
Код пакета user

Какое-то время всё в порядке. А затем появляется новое требование к проверке e-mail для админа: в нём должно быть 10 символов, а не пять; или появилась структура с json-объектом для админа или ещё что-то.

Можно начать дорабатывать функцию из библиотеки validation, превращая её в универсальную, но принесёт ли это пользу в части поддерживаемости кода? Не лучше ли было для разных частей кода оставить две одинаковые функции, которые могут изменяться по-разному в процессе эксплуатации ПО?

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

DRY ориентируется на KISS, а не действует сам по себе.

Если есть сомнения, что функции могут с течением времени изменяться по-разному, оставьте эти одинаковые функции как есть.

2.3.2. Дублирование структур

Этот пример я посмотрел в публикации на английском языке, автор Miłosz Smółka - программист из Польши, специализирующийся на backend'е:

https://threedots.tech/post/things-to-know-about-dry/
https://threedots.tech/post/things-to-know-about-dry/

Легенда:

Есть работающий сервис. Нужно сохранить последний IP-адрес каждого пользователя. В дальнейшем это поможет повысить безопасность. Эту задачу поручили новому программисту, назовём её Сьёзен, в первый рабочий день. Благодаря задаче Сьюзен заодно познакомится с проектом.

Сьюзен нашла в проекте структуру:

Код
Код

Сьюзен добавила в структуру поле c IP-адресом:

Код
Код

И отправила Pull-Request. Сеньор, назовём его Дейв, посмотрел, и ответил:

Я не думаю, что мы должны предоставлять доступ к этому полю через REST API.

Дэйв объяснил, что структура User используется не только для работы с клиентом: в базе данных Firestore хранится информация, преобразованная из структур Go. Структура User была совместима как с ответами на запросы клиента, так и с хранилищем.

«Благодаря такому подходу нам не нужно дублировать код. Достаточно один раз изменить файл-YAML», — с энтузиазмом сказал Дейв.

Дейв и его команда реализовали принцип DRY.

Сьюзан ещё подумала, и дополнила логику работу с клиентом примерно так:

user.LastIp = nil

Т.е., когда подходила очередь взаимодействовать со структурой в отношении запросов клиентов, а не в отношении БД, то соответствующее поле приравнивалось к нилу.

Идея была принята Дейвом.

Сьюзен после работы обдумала ситуацию, и на следующий день поделилась сомнениями с Дейвом:

Правильно ли использовать одну и ту же структуру как для ответа API, так и для модели базы данных? Не рискуем ли мы случайно раскрыть личные данные пользователей, если будем продолжать расширять её по мере роста приложения? Что, если мы захотим изменить только ответ API, не меняя поля базы данных?

В конце концов, они договорились обсудить это в новом Pull Request.

Сьюзен создала новую структуру, похожую на изначальную, и предназначенную не для RESTAPI, а для базы данных:

Код с новым полем
Код с новым полем

Новое решение длиннее по количеству строк кода, но оно устраняет связь между REST API и уровнем базы данных.

Т.е., было:

Схема работы сервиса
Схема работы сервиса

Стало:

Схема работы сервиса
Схема работы сервиса

Дэйва беспокоило то, что второе решение нарушает принцип DRY и приводит к шаблонного кода.

Автор публикации подводит к мысли, что:

Как правило, принцип DRY лучше применять к поведению, а не к данным. Например, выделение общего кода в отдельную функцию не имеет тех недостатков, о которых мы говорили ранее.

Однако, как я показывал в примере выше в параграфе 2.3.2, с функциями тоже не всё однозначно.

В общем, с DRY я считаю, что познакомились, посмотрели на ограничения его применения. Хочу ещё познакомить с противоположным DRY принципом - WET.

2.4. WET

В противовес DRY появился принцип WET (dry - сухой, wet - мокрый с англ.).

Варианты расшифровки аббревиатуры WET такие:

  • write every time - пиши всё дважды;
  • we enjoy typing - мы наслаждаемся печатанием;
  • waste everyone's time - крадите время каждого.

Принцип "влажного" программирования в противовес "сухого" существует по крайней мере с 2002 г. По сути, это то, от чего избавляет DRY. Вот типичный сценарий использования WET-принципа:

Текстовая строка «комментарий» может повторяться в метке, HTML-теге, в имени функции чтения, в частной переменной, в DDL-операторе базы данных, в запросах и так далее.

Можно предположить, что WET характерно только для фронтенда. Как бы не так; вот например, недавно в телеграм-группе "Организованное программирование" видел пост на эту тему:

Фрагмент поста: https://t.me/orgprog/226
Фрагмент поста: https://t.me/orgprog/226

В общем суть в чём на мой взгляд - WET-принцип, это не антипод DRY, а желание его не использовать; при WET дублирование происходит и когда не нужно, и когда нужно. В то время, как DRY при согласовании с KISS допускает дублирование кода, когда это полезно для поддержки кода; а когда полезнее не дублировать - не дублирует.

WET - это отсутствие реализации принципа DRY.

2.5. AHA

Принцип AHA на мой взгляд - это принцип DRY при согласовании KISS.

AHA от англ. avoid hasty abstractions - избегай поспешных абстракций. Принцип описан Кентом К. Доддсом (родился в 1988 г., США) как оптимизация в первую очередь для изменений и избегание преждевременной оптимизации.

Фото Кента с его сайта: https://kentcdodds.com/about
Фото Кента с его сайта: https://kentcdodds.com/about

AHA основано на понимании того, что, чем глубже программисты погружаются в абстракцию программного обеспечения, тем больше они осознают, что затраты на эти инвестиции никогда не окупятся.

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

Чем глубже программисты погружаются в абстракцию ПО, тем больше они осознают, что затраты на эти инвестиции никогда не окупятся.

Моё мнение такое - принцип AHA - это принцип DRY, согласованный с KISS. В первой главе я высказал мысль, что KISS - основа всех остальных принципов, и все другие принципы могут действовать только в направлении повышения эффективности KISS.

2.6. Итоги DRY

Когда мы пишем код, всегда полезно думать о том, как можно переиспользовать уже написанный фрагмент кода.

При этом создавать библиотеку под каждую повторяющуюся функцию из разных пакетов стоит, если мы уверены, что функции с течением времени будут изменяться одинаково; т.е. если у них одна логика создания и изменения.

Что касается структур, то для разной логики, может быть полезно использовать разные структуры - пример с запросами клиента и БД, где использовалась одна структура.

Если у вас есть другие интересные способы применения DRY, или примеры, где применялась WET - поделитесь в комментариях.

3. Выводы

Мы лучше разобрались что стоит за принципами KISS и DRY. Разобрались, почему принцип "держи это супер простым" подразумевает усложнение кода - это допустимо, когда поддержка кода от усложнения упроститься.

Поняли, что принцип DRY нужно применять аккуратно, и всегда ориентируясь, не противоречит ли он KISS.

В целом оба принципа рассчитаны на улучшение поддерживаемости кода, при этом KISS я считаю родительским принципом, а DRY - принципом, раскрывающим KISS в части влияния дублирования кода на дальнейшую поддержку кода.

На этом у меня всё. Благодарю, что дочитали публикацию до конца. Изучая такие технологии, мы каждый раз развиваемся, как разработчики. Успехов в этом труде. Будем на связи.

https://ru.freepik.com/premium-photo/morning-misty-autumn-mountain-landscape-with-fir-forest-mountainside_16223523.htm
https://ru.freepik.com/premium-photo/morning-misty-autumn-mountain-landscape-with-fir-forest-mountainside_16223523.htm

Бро, ты уже здесь? 👉 Подпишись на канал для начинающих IT-специалистов «Войти в IT» в Telegram, будем изучать IT вместе 👨‍💻👩‍💻👨‍💻