YAGNI — друг, или враг?

Один из самых вредоносных принципов в разработке, когда-либо получивших широкую известность — YAGNI. Его озвучил Рон Джеффрис в 1998, а спустя более двадцати лет — еще и Кармак подлил керосин в огонь со своим: «Неопытным разработчикам трудно оценить, как редко разработка архитектуры с учетом будущих требований приводит к успеху».
Кармак имел в виду вообще немного иное, но квинтэссированная эрудиция толпы намертво припечатала эти брошенные вскользь по иному поводу слова к YAGNI.
Как примерно полтора столетия назад по иному поводу сказал Марк Твен:»Truth is mighty and will prevail. There is nothing wrong with this, except that it ain«t so».
Вообще говоря, в нашем разнообразном мире ни одна максима не может быть без оговорок применима всегда (кроме саркастично-нигилистических, конечно, наподобие приведенной выше реплики Твена). Насколько могу судить, будучи сформулированным столь безжалостно афористично, принцип YAGNI привел преимущественно к тому, что у лентяев появилась отговорка на все случаи жизни.
— Давай предусмотрим передачу конфигурационных параметров в эту функцию? — YAGNI.
— Инъекция зависимости здесь уместнее приколоченной гвоздями реализации хранилища. — YAGNI.
— Черный кофе без сахара, пожалуйста. — YAGNI!
Разумеется, есть ситуации, когда этот принцип полезен. Если начитавшийся Мартина джун начинает реализацию класса User
с построения иерархии Creature
→ Cellular
→ Eukaryotic
→ Animal
→ Eumetazoa
→ Bilateralia
→ Chordal
→ Mammals
→ Primates
→ Hominids
→ Humans
→ Homo Sapiens
, ему нужно дать по рукам (и не только потому, что наследовать юзера от человека разумного — ключевая логическая ошибка). Но обычно все-таки мы сталкиваемся с дискуссиями о применимости принципа YAGNI в менее очевидных ситуациях.
Я поделюсь своим опытом: когда этот принцип худо-бедно работает, и как можно избежать его применения в тех случаях, когда сегодня это YAGNI, а спустя год — да тут проще все с нуля переписать, чем добавить возможность сохранять промежуточные состояния в базу.
Типы проектов
Если вы пишете скрипт на баше для сортировки и сжатия картинок в каталоге «Отпуск в Кандалакше» — YAGNI ваш безоговорочный друг и партнер. Вам не потребуются проверки на правильно переданные параметры, обработка ситуации «директория не существует» и оценка оставшегося места на диске.
Если вы пишете библиотеку общего назначения — YAGNI ваш злейший враг. Вам и в страшном сне не приснится, как именно пользователи будут её использовать, и что попросят в следующем issue.
Проекты на аутсорсе (галере) — скорее сродни скрипту на баше, ну разве что падать в корку с сообщением «Error 0×34785612» не нужно. Проекты в продуктовой компании — больше похожи на библиотеки общего назначения (если вы, конечно, не собираетесь оттуда увольняться и не стараетесь оставить после себя как можно больше выжженой земли).
Как всегда, важнее всего — баланс. Итак, что же обычно можно замести под ковер с тирольским йодлем »YAGNI! », а что лучше бы — нет?
Dependency Injection
Сторонние зависимости никогда (никогда!) нельзя приколачивать гвоздями. Обернуть любую внешнюю библиотеку в свой интерфейс — работа на час с перекурами, зато вы заведомо упростите себе как минимум тестирование и отладку — прямо сейчас. Ну и завтра, когда придется заменить условный парсер джейсона на другой, потому что этот не справляется с юникодом третьего плана — вы скажете себе самому спасибо. Исключение: если вы пишете фронтенд к постгресу — завязанный на уникальные возможности именно этой базы — интерфейсная абстракция над драйвером, скорее всего, будет лишней.
Иными словами, если вы хотя бы секунду колебались в выборе сторонней библиотеки — ей нужен wrapper и она должна инжектиться в качестве зависимости (например, через конфиги). Код должен использовать обертки, прямых вызовов библиотеки быть не должно (кроме как в самой обертке).
Фреймворки можно приколачивать, всё равно подменить его на лету не получится.
Фреймворк отличается от библиотеки тем, что в общем случае он выполняет некую работу сам по себе, обладает внутренним состоянием и (самое главное) — не является полностью черным ящиком с точки зрения приложения. Если невозможно написать обертку без дублирования функциональности библиотеки — это фреймворк. У библиотеки не бывает сайд-эффектов, фреймворк — на них построен.
Парсер джейсона — это библиотека. Драйвер базы данных — библиотека. Реализация кэша — библиотека (да, у него есть внутреннее состояние, но он остается для приложения черным ящиком). ORM, темплейтные движки, реализация FSM на процессах/базе — это фреймворки.
Библиотекам нужны обёртки. Точка. Иначе вы замучаетесь их тестировать, для начала. Даже если план их заменить не просматривается и в дальней перспективе.
Параметры API
Ни в коем случае нельзя писать никакую логику в расчете на завтрашнюю смену курса. Предусматривать корректную отработку пользователя с тремя паролями (или собаки с пятью ногами) — не нужно. raise FeatureNotImplemented
— уместно только, если такая фича уже запрошена и оформлена в задачу, но вам пока не до нее.
Но ни в коем случае нельзя заливать ваше текущее решение эпоксидкой, так, что внесение минимальных логических изменений — превратится в войну с обратной совместимостью и кросс-вызовами. Допустим, ваша функция принимает целое число и возвращает его строковое представление. Не пишите так (это руби):
def to_string(number, base)
# сделали строку и вернули её
end
Напишите так:
def to_string(number, **options)
Когда завтра эта функция должна будет принимать числа с плавающей точкой, ограничивать количество знаков после точки в строковом представлении, возвращать строку «двадцать пять» для number = 25
, и еще исполнять триста фантазий стейкхолдеров — обратная совместимость не поломается, вы просто обработаете новые допустимые options
.
Даже в джаваскрипте, где функция может принимать сколько угодно аргументов, имеет смысл передавать объект {base: 10}
, чтобы потом не пришлось спагеттировать код проверками, сколько же параметров было таки передано.
Колбэки
Никто никогда не может угадать всех пожеланий пользователя, но одно появится точно: рано или поздно вам потребуются метрики и такая модная нынче observability. Но пока до этого далеко. Предусмотреть ли такую возможность? — И да, и нет.
Втыкать метрики на каждый чих в MVP — глупо и бессмысленно. Это куча лишнего кода, ради ничего (в данный момент). Тем более, прямо сейчас понять, откуда нужно слать метрики, откуда — нет, а откуда вообще может потребоваться отправить сообщение в соседний, пока несуществующий даже в качестве идеи, — микросервис — невозможно.
А вот предусмотреть возможность вызова колбэков — вполне. Колбэки бывают двух типов: одни просто уведомительные (fire and forget), другие — могут предоставить возможность влиять на ход выполнения (эти обычно называются middleware, или типа того).
Если ваш API разрешит передавать колбэк в параметрах, а еще и настраивать его глобально в конфиге, — то вы решите ¾ завтрашних проблем по модернизации прямо сейчас практически без лишнего кода. Вернемся к нашей to_string
функции.
def to_string(number, **opts)
result = "#{number}"
result =
Array(opts[:callbacks]).reduce(result) do |result, callback|
callback.(result)
end if opts.key?(:callbacks)
result
end
Или, на эликсире:
def to_string(number, opts) do
result = "#{number}"
opts
|> Keyword.get(:callbacks)
|> List.wrap()
|> Enum.reduce(result, & &1.(&2))
end
Всё, код полностью готов к любым внешним модификациям. Заодно вы бесплатно упростили отладку (колбэк «вывести в консоль»), разрешили метрики («отправить в графану»), расширили функциональность внешними форматтерами («перевести в слова») и еще чурта в ступе, о котором вы даже пока не подозреваете.
Я настолько часто упирался в тупик с чужими библиотеками, скрывающими от меня всё за своим черноящичным API, что теперь экспортирую колбэки из своего кода буквально везде, где это уместно.
Этот текст уже и так становится слишком длинным, поэтому я прервусь, а на днях покажу, как эти принципы позволили мне написать библиотеку для парсинга нестандартного маркдауна, которая из коробки в несколько раз быстрее аналогов, но может быть расширена по синтаксису практически неограничено.
Удачного молчания YAGNIат.