Alek�ei Matiu�hkin

сделано с умом



YAGNIN, but YAGNIL

Monday, 25 Dec 2023 Tags: 2023tech

третья часть «четырех главных навыков разработчика»

умение проектировать ПО таким образом, чтобы в первой версии не было ни единой строки «в расчете на будущие изменения», но будущие изменения не затрагивали существующий код никаким образом


Одним из самых омерзительных «приниципов» разработки, появившихся в последнее десятиление, можно с уверенностью назвать «YAGNI». Несмотря на то, что он вырос из кокетливого, но в целом небезыдейного «premature optimization is the root of all evil» Дональда Кнута, озвученного почти 60 лет тому назад, современная интерпретация дарит лентяям и неквалифицированным специалистам (и подлецам) огромный простор для увиливания от корректного решения задач.

Правильная формулировка вынесена в заголовок этой заметки: «You aren’t gonna need it now, but you are gonna need it later». Если бы это было не так, и «architecting for future requirements / applications turns out net-positive» (по выражению Джона Кармака) действительно бы пригождалось крайне редко, мы бы никогда не сталкивались с реакцией «этот код проще написать с нуля, чем изменить». Никогда бы не возникала ситуация «придется рефакторить на три модуля вглубь» при добавлении нового параметра в вызов функции. Люди бы никогда не ломали обратную совместимость, ведь выдернутая из контекста цитата Рона (доступна по ссылке выше) — обещает нам простое добавление новых штук по мере надобности.

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

С другой стороны, Кнут, Джеффрис и Кармак — люди уважаемые, написавшие по груде кода, проверенного десятилетиями, и, скорее всего, совсем уж ерунду нести не станут.

Как обычно, все дело в нюансах. Сразу реализовывать все потенциальные возможности, которые только могут в будущем прийти в голову безумному заказчику — очевидно, задача неблагодарная. Так делать не сто́ит. Да и не угадать все эти потенциальные идеи прямо сейчас. Но к ним необходимо быть готовыми.

Что это значит? — Давайте я попробую пояснить на примере.

Пусть перед нами стоит задача написания публикатора текста в мастодон. Консольная утилита, буквально. Тут — файлик с текстом, там — тут (toot). С синтаксисом вызова наподобие $ post file.txt.

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

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

Итак, куда оно может разростись?

  • добавится новый сервис;
  • добавится новый формат;
  • добавится пакетное выполнение;
  • что-то еще, наверное, но пока хватит.

Пришло время произнести два заветных слова: «dependency injection». Везде, где вы ожидаете масштабирование требований вширь, можно подстелить себе соломку не написав ни единой лишней строчки. Вместо вот такого хардкода

@spec publish(String.t()) :: :ok
def publish(text) do
  text
  |> Markdown.format()
  |> Mastodon.publish()
end

можно обколоться зависимостями, с внятным умолчанием.

@spec publish(String.t() | [String.t()], Formatter.t(), Publisher.t()) :: :ok
def publish(texts, formatter \\ Markdown, publisher \\ Mastodon) do
  texts
  |> List.wrap() # accept a single string as well
  |> Enum.each(fn text ->
    text
    |> formatter.format()
    |> publisher.publish()
  end)
end

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

Разумеется, мы не сможем предугадать все потребности завтрашнего дня. Но некоторые прямо бросаются в глаза настолько, что их можно добавить в код, не вставая со стула. Все знают, что магические константы — это плохо. Поэтому люди делают так:

@pi 3.14159265

def pi, do: @pi

Так делать не нужно. По двум причинам: если это π, оно никогда не изменится, и его никто не спутает с ФИО генерального. А если это что-то, что может измениться завтра (например, максимальное количество символов для перепубликации) — @max_symbols 140 не очень-то поможет: его потребуется найти и изменить прямо в коде. Вот так хорошо:

@max_symbols Application.compile_env(:my_app, :max_symbols, 140)

def truncate(text, symbols \\ @max_symbols),
  do: String.slice(0..symbols-1)

Надо изменить значение по умолчанию? — Добро пожаловать в конфиг. Надо иметь возможность отрезать разное количество символов? — Вот тут параметр, привет.

На самом деле, этот пример очень легко масштабируется на любую «серую зону» изменений архитектуры. Архитектура меняться не должна в принципе: должны меняться запчасти. Бизнес молодой, и клиентов пока только пять? — Значит функция отправки новогоднего поздравления по почте (простите) должна быть готова распараллелиться. Это MailSender, экспортирующий единственную функцию send/2 который пока принимает текст и список почтовых адресов и просто последовательно проходит по нему. Когда придет сотый клиент — мы его перепишем в несколько потоков, не трогая остальной код, — и все.


Старайтесь делать так, чтобы за любую операцию в вашем коде отвечал отдельно взятый, крайне простой, обособленный и протестированный модуль. В конце концов, можно с детства готовиться к тому, как в твоем сортире замочится унитаз, и в совершенстве овладеть навыками его прочистки. А можно, когда настанет час, позвать сантехника, который умеет только это, но умеет хорошо. Это и есть «dependency injection».


  ¦