Простота недооценена

Повторно используемый код — это святой Грааль программирования. Инженеры-программисты одержимы созданием служебных библиотек и функций, которые можно повторно использовать в бизнес-приложениях всех брендов.

Они весь день спорят за чашкой кофе о том, чтобы сделать свой код более пригодным для повторного использования и не изобретать его повторно.

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

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

Мы часто сталкиваемся с дилеммами рефакторинга при создании новых функций в большой кодовой базе.

«Менять или не менять».

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

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

Я возражаю против его заявления.

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

Благодаря этой истории я понял, что простота имеет первостепенное значение для обслуживания программного обеспечения.

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

Почему трудно достичь простоты?

Исследования показывают, что человеческая природа при решении проблем предпочитает складывать, а не вычитать. Одним из примеров является то, что Адамс и его коллеги проанализировали архивные данные и заметили, что, когда новый резидент университета запрашивает предложения по изменениям, университет будет лучше обслуживать своих студентов и сообщество. Они поняли, что только 11% ответов касались удаления существующих правил, практики или программы.

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

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

  1. Уменьшать. Самый простой способ добиться простоты — это продуманное сокращение. Если сомневаетесь, удалите.
  2. Организовать. Это заставляет систему многих казаться меньше.
  3. Время. Экономия времени кажется простотой, а ожидание — сложностью.
  4. Учиться. Знания делают все проще.
  5. Отличия. Простота и сложность подобны инь и ян — когда преобладает одно, выделяется другое.
  6. Контекст. Всегда давайте контекст, чтобы сделать источник более простым.
  7. Эмоция.
  8. Доверять. Мы верим в простоту, несмотря на последствия, которые она приносит.
  9. Отказ. Вы должны извлечь урок из своей ошибки, чтобы упростить следующую вещь.
  10. Тот самый. Простота заключается в вычитании очевидного и добавлении смысла.

Люди часто путали простоту с минимализмом — думали, что это синонимы. Минимализм стремится передать суть предметов — речь идет о форме: много места. С другой стороны, простота означает отсутствие лишних элементов. Простота подчеркивает то, что важно. Простота порождает минимализм, но не наоборот. Минимализм легко понять, потому что мы видим его визуально. Однако простота — это абстрактный термин, который часто вызывает споры среди разработчиков программного обеспечения.

3 совета по созданию простой системы

Уменьшите количество абстракций в вашем коде

В одной из работ, которые я недавно представил, есть метод, который декодирует внешние источники во внутреннюю модель. Кодовая база уже содержит аналогичный метод декодирования. Однако метод декодирования очень специфичен и ориентирован на другие бизнес-потоки. Как хороший инженер, я реорганизую один из существующих методов декодирования, чтобы не повторяться. Я посмотрел на метод декодирования, и он состоит из трех шагов:

  1. base64 декодировать
  2. Декодируйте строку JSON в объект JSON.
  3. Верните один из атрибутов JSON в объекте JSON.

Из трех шагов более конкретным является последний шаг, возвращающий определенный атрибут JSON вызывающей стороне. Таким образом, я создал вспомогательный метод, который инкапсулирует первые два шага, а существующий метод декодирования использует вспомогательную функцию. Мой PR был прокомментирован одним из старших инженеров в команде, что я должен создать класс для функции декодирования, потому что создание объекта должно быть помещено в класс.

"Почему мы хотим создать еще одну косвенность только для декодирования одного сообщения?" Я подумал.

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

Когда вы пытаетесь добавить больше абстракции, спрашиваете себя: «Какую пользу это принесет нашему процессу? Насколько это усложнит систему?

Ответ всегда зависит.

Создание класса и абстракции в этом сценарии не имеет большого значения, поскольку приложение декодирует только два сообщения. Оператор if/else в функции декодирования может добавить цикломатической сложности исходной функции декодирования. Однако это увеличивает время рефакторинга всей структуры кодовой базы и затрудняет последующее отслеживание кода.

Каковы недостатки абстракции? Более сложная навигация по коду. Предположим, вы создаете интерфейс decode, который можно расширить с помощью нескольких реализаций pub-sub. Вы почувствовали, что сделали код расширяемым на уровне API. Однако стоимость такого обобщения значительно усложняет код. Новому инженеру, пытающемуся расширить новую функцию, потребуется более глубокое понимание всех созданных вами уровней абстракции.

Более того, трассировка и отладка кода занимает гораздо больше времени. Одна из причин заключается в том, что когда мы отслеживаем строки кода на всех уровнях абстракции, мы также должны создать стек в нашей памяти, чтобы помнить все шаги, которые мы предприняли. Становится трудно понять часть бизнес-реализации с несколькими абстракциями. Если вы используете любую IDE или даже vim, вы должны выполнять Find All References каждый раз, когда пытаетесь проследить реализацию.

Избегайте написания общего кода

Многие ошибки, которые появляются в сложной кодовой базе, часто являются слишком общим программным обеспечением.

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

Например, учитывая список слов, вернуть Map всех частот этих слов.

Эту задачу можно написать:

  1. создание хеш-карты
  2. перебрать все слова в списке
  3. поместите слово в качестве ключа и частоту в качестве значения.
def countFreq(lst: List[String]): Map[String, Int] = { 
lst.foldLeft(Map.empty){(acc,el) =>
  val count = acc.getOrElse(el, 0) + 1  
  (el -> count) + acc 
  }
}

Общий способ сделать это — использовать моноид для преобразования списка слов в кортеж слова и его начальную частоту, равную единице, и использовать foldMap для сокращения всего списка записей до одного значения Map. Это та же реализация, что и map-reduce.

def mapMergeMonoid[K,V](V: Monoid[V]): Monoid[Map[K, V]] =
 new Monoid[Map[K, V]] {
  def zero = Map[K,V]()
  def op(a: Map[K, V], b: Map[K, V]) =
   (a.keySet ++ b.keySet).foldLeft(zero) { (acc,k) =>
    acc.updated(k, V.op(a.getOrElse(k, V.zero),
              b.getOrElse(k, V.zero)))
   }
 }
def foldMapV[A, B](as: IndexedSeq[A], m: Monoid[B])(f: A => B): B =
 if (as.length == 0)
  m.zero
 else if (as.length == 1)
  f(as(0))
 else {
  val (l, r) = as.splitAt(as.length / 2)
  m.op(foldMapV(l, m)(f), foldMapV(r, m)(f))
 }
val intAddition: Monoid[Int] = new Monoid[Int] {
 def op(x: Int, y: Int) = x + y
 val zero = 0
}

def countFreq(lst: List[String]): Map[String, Int] = {
  foldMapV(as, mapMergeMonoid[String, Int](intAddition))((a: A) => Map(a -> 1))

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

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

Хотя обобщение имеет много преимуществ в том, что оно не повторяется (DRY), стоимость обобщения может повлиять на скорость вашей команды в поддержке и расширении функций. Внезапно изменение события отслеживания может занять несколько дней, потому что нам нужно изучить все обобщения в нашей кодовой базе. Так что выбирайте свои сражения.

Сократите количество вычислительных ресурсов при проектировании вашей системы

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

Даниэль Стёртевант упомянул в своей статье Проектирование систем и стоимость архитектурной сложности, что архитектурная сложность влияет на производительность разработчиков, качество программного обеспечения и текучесть кадров. У него было исследование, в котором проверялась гипотеза о том, что файлы со сложной архитектурой имеют больше дефектов. Кроме того, он изучает архитектурную сложность отдельных файлов и количество исправлений ошибок, возникающих в этих файлах. Он обнаружил, что в более сложном файле нужно исправить в 2,1 раза больше ошибок, чем в другом.

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

Если вы можете спроектировать свое приложение в виде монолита, вы должны это сделать.

Облачная функция находится между публикацией в облаке Google и заданием потока данных, если очередь публикаций может немедленно подключиться к заданию потока данных.

Чем больше услуг, тем больше точек отказа. Предположим, мы поместили несколько сервисных уровней между сервисами A и B. В этом случае мы должны учитывать все сценарии отказа, которые может вызвать промежуточный сервис.

А это больше инженерные ресурсы и вычислительные ресурсы.

Резюме

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

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

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

Наконец, начните без косвенных действий при проектировании системы и увеличивайте сложность по своему усмотрению. Например, если вы хотите спроектировать платежную систему для оформления заказа, начните с того, что Frontend сразу же вызовет API Gateway (Stripe), это будет самый простой дизайн. Затем оцените, нужен ли какой-либо промежуточный микросервис, чтобы сделать процесс разработки более эффективным.

Есть ли у вас какие-либо другие советы по упрощению программного обеспечения и процессов? Прокомментируйте их внизу!

Первоначально опубликовано на https://pathtosenior.substack.com 9 марта 2023 г.