[Перевод] Сохраняем простоту кода и ускоряем разработку за счет отказа от оверинжиринга

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

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

Делаем приложения со списками дел в спешке, но медленно


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

Некоторые программисты в своем дизайн-документе прежде всего зададутся такими вопросами: «Какой фронтенд-фреймворк мне лучше использовать для рендеринга списков? В какой базе данных следует хранить информацию? Какой ORM может понадобиться, чтобы производить чтение и записи в хранилище? Придется ли виртуализировать списки, если их длина начнет сказываться на производительности?»

Стандартные ответы на эти вопросы, вероятно, будут предполагать выбор самого современного фронтенд-фреймворка, хранение данных в SQLite и получение доступа к ним через «ORM дня». Виртуализацию списка вам, возможно, удастся вычеркнуть, хотя не исключено, что подвернется какая-нибудь простая в использовании библиотека для этой цели. Да что там, раз уж зашел разговор о виртуализации, у вас может появиться искушение заодно контейнизировать всё приложение.

kdw7lsp82y0m9umtpebvj6eghew.png

Приложение со списками дел → Библиотека для виртуализации; Разрозненные вызовы ORM; Фронтенд — фреймворк с оверинжинирингом; База данных SQLite → Бизнес-логика связана с фронтендом; Модель данных тесно связана с SQLite

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

ez2feos4r2lkakefdla7y6oefoo.png

Приложение для составления списков дел выходит в установленный срок, хотя багов в нем больше, чем хотелось бы. К сожалению, на этом ваша работа не закончена. Ваше блестящее применение SQLite и виртуализации позволяет спискам сохранять миллионы элементов без ущерба для производительности, но тут продукт выдает вам сюрприз. Исходя из отзывов пользователей, в следующем обновлении список должен синхронизироваться с облаком. Имплементацию с SQLite можно отправлять на свалку –, а вдобавок нужно еще провести рефакторинг и отловить все разбросанные по коду вызовы ORM. Сделать-то это всё можно, но понадобится время.

Делаем приложения со списком дел быстрее


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

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

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

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

a_s57cub9dfxzqvwyd8ayq-yqwq.png

Бизнес-логика приложения со списками дел → (Минимальный интерфейс) Фронтенд-модуль → (UI/UX логика) UI-фреймворк с возможностью обновления
→ (Минимальный интерфейс) Модуль постоянства данных → (Обработка абстрактных данных) Уровень абстракции базы данных ←→ (Синхронизация данных) Локальное хранилище / Интеграция облачного хранилища

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

Почему оверинжиниринг тормозит дело

vzx7xd6snkxmzwlqhlcptbxrfwe.png

О чем мы мечтаем на старте — Чем приходится довольствоваться при запуске — Что нужно пользователю

Оверинжиниринг неэффективен по двум фундаментальным причинам:

  1. Напрасная трата времени: имплементация любая функциональности, в которой нет необходимости, отнимает время, которое можно было бы потратить на другую, необходимую функциональность.
  2. Замедление разработки: дополнительная функциональность приносит дополнительную сложность. Усложнения в кодовой базе замедляют разработку в перспективе.


Первый пункт самоочевиден, а второй лучше всего раскрыт в книге «Философия дизайна программного обеспечения»:

По мере того, как программа развивается и обретает новую функциональность, она приобретает сложность — между ее компонентами возникают неочевидные связи. Со временем сложность накапливается, и программистам становится всё сложнее удерживать в уме все факторы, имеющие отношение к делу, при модифицировании системы. Это замедляет разработку и порождает баги, которые замедляют разработку еще сильнее и приносят дополнительные расходы. Чем больше программа и чем больше людей над ней работают, тем труднее управляться со сложностью.
Делая базовую архитектуру как можно проще, вы избегаете внедрения лишней сложности. Вы пишете код, не замыкающийся в сеть, и поэтому для каждой новой возможности проще найти место.

Подавляйте сложность как можно дольше


Поразмыслите над этим отрывком из прекрасной книги «Чистая архитектура» о разработке веб-сервера для автоматического тестирования FitNesse:

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

Мы поместили методы доступа к данным в интерфейс с именем WikiPage. Эти методы обеспечивали все необходимое для поиска, извлечения и сохранения страниц. Конечно, мы не реализовали эти методы с самого начала, а просто добавили «заглушки», пока работали над функциями, не связанными с извлечением и сохранением данных.

В действительности в течение трех месяцев мы работали над переводом текста вики-страницы в HTML. Это не потребовало использования какого-либо хранилища данных, поэтому мы создали класс с именем MockWikiPage, содержащий простые заглушки методов доступа к данным.
В какой-то момент этих заглушек оказалось недостаточно для новых функций, которые мы должны были написать. Нам понадобился настоящий доступ к данным, без заглушек. Поэтому мы создали новый класс InMemoryPage, производный от WikiPage. Этот класс реализовал методы доступа к данным в хеш-таблице с вики-страницами, хранящейся в ОЗУ.

Это позволило нам целый год писать функцию за функцией. В результате мы получили первую версию программы FitNesse, действующую именно так. Мы могли создавать страницы, ссылаться на другие страницы, применять самое причудливое форматирование и даже выполнять тесты под управлением FIT. Мы не могли только сохранять результаты нашего труда.

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

Три месяца спустя мы пришли к заключению, что решение на основе простых файлов достаточно хорошее, и решили вообще отказаться от идеи использовать MySQL. Мы отложили это решение до неопределенного будущего и никогда не оглядывались назад.
На этом история закончилась бы, если бы не один из наших клиентов, решивший, что ему очень нужно сохранить свою вики-страницу в MySQL. Мы показали ему архитектуру WikiPages, позволившую нам отложить решение. Через день он вернулся с законченной системой, работавшей в MySQL. Он просто написал производный класс MySqlWikiPage и получил необходимое.

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

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

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

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


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

Основной принцип


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

  • модульные тесты
  • линтеры
  • минимальные интерфейсы
  • низкая связанность
  • понятные названия переменных


Откажитесь от:

  • мощных баз данных
  • сложных API
  • ненужных фреймворков


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

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

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

Пусть мои шрамы послужат вам уроком. Двигайтесь быстро, пишите чисто и любой ценой избегайте оверинжиниринга: простой и качественный код — всегда самый короткий путь.

© Habrahabr.ru