Чистая архитектура. Роберт Мартин

Это первая из 3 частей конспекта. В ней мы затронем понятие архитектуры, обзор парадигм программирования и объяснение принципов SOLID.

Предисловие и введение

Коротко:

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

Железо меняется радикально и быстро, софт — не меняется. Он состоит из условий, присвоений и циклов. Есть разные парадигмы и приёмы, но в конечном счёте всё преобразовывается в условия, присвоения и циклы. И именно поэтому правила построения архитектуры универсальны.

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

Глава 1. Что такое дизайн и архитектура

Коротко:

  • Цель хорошей архитектуры — минифицировать потребные ресурсы для поддержки и улучшения софта.
  • Говнокод тормозит разработку не только в будущем, но и прямо сейчас.

Разницы между дизайном и архитектурой софта нет. На примере архитектора (не программного): он продумывает технические аспекты здания, но также описывает то, как здание выглядит.

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

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

Подход «почистим позже, вначале выкатим фичу» не работает, потому что фичи не заканчиваются. А говнокод при этом тормозит разработку. Единственный способ разрабатывать быстро — разрабатывать хорошо.

Глава 2. Сказка о двух ценностях

Коротко:

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

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

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

Что важнее? На примерах:

  1. Если есть программа, которая идеально отвечает требованиям, но её невозможно поддерживать, то при поступлении новых требований, их будет невозможно удовлетворить. Следовательно программа не изменится, а значит станет бесполезной.
  2. Если же есть программа, которая работает не совсем правильно, но её легко изменить, то её и легко привести к соответствию требованиям. А значит она останется полезной.

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

Глава 3. Обзор парадигм

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

Глава 4. Структурное программирование

Коротко:

  • Понимать программу полностью сложно, её надо разбивать.
  • Необдуманный прямой контроль (goto) — плохо.
  • Тесты помогают найти и доказать, что баг есть. Они не доказывают, что багов нет.

Дейкстра заметил, что понимать программу полностью — сложно, а goto мешают разбивать её на более мелкие части. При этом простые управляющие конструкции — if/else, do/while — наоборот, облегчали работу. Из таких элементарных управляющих конструкций может быть построена любая программа.

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

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

Глава 5. Объектно-ориентированное программирование

Коротко:

  • ООП: инкапсуляция, полиморфизм, наследование?
  • Делайте важное ядром, а всё остальное плагинами к нему.

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

Наследование по сути просто переобъявление каких-то полей или методов. Есть не только в ОО языках, хотя в них наследование работает удобнее.

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

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

Интерфейс и БД зависят от бизнес-правил, а не наоборот
Интерфейс и БД зависят от бизнес-правил, а не наоборот

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

Глава 6. Функциональное программирование

Коротко: иммутабельность данных — хорошо.

ФП учит иммутабельности данных. Но есть несколько компромиссов. Например, наличие в приложении как чистых компонентов, так и компонентов с сайд-эффектами.

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

Глава 7. Принцип единой ответственности

Коротко: функция должна решать только одну задачу.

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

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

Глава 8. Принцип открытости-закрытости

Коротко:

  • Поведение сущности должно быть расширяемым без необходимости менять саму сущность.
  • Если компонент А должен быть защищён от изменений в компоненте Б, то компонент Б должен зависеть от компонента А.

Пример: допустим есть штука, которая генерирует банковский отчёт для веба. В отчёте могут быть отрицательные числа, они помечаются красным. Страница скроллится, если строчек много.

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

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

Глава 9. Принцип подстановки Барбары Лисков

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

Типичный пример нарушения принципа: допустим есть класс Rectangle, от него наследуется класс Square. Это не совсем верно, так как у прямоугольника стороны могут меняться независимо, а у квадрата они должны меняться вместе. Узнать снаружи о том, экземпляром какого класса является объект, можно только, если напрямую спросить об этом.

Глава 10. Принцип разделения интерфейса

Коротко: зависимость от чего-то, что вы на самом деле не используете, может вызвать проблемы, которых вы не предвидите.

Если представить ситуацию, где User1 зависит только от op1, то первая архитектура первой диаграммы может привести к лишним перекомпиляциям:

Прямое наследование может привести к перекомпиляции
Прямое наследование может привести к перекомпиляции

Архитектура на второй диаграмме эту проблему решает:

Наследование через интерфейс
Наследование через интерфейс

Глава 11. Принцип инверсии зависимостей

Коротко:

  • Модули должны зависеть от абстракций, а не от деталей реализации.
  • Абстракции не должны зависеть от деталей реализации.

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

Что дальше?

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

В третьей обсудим бизнес-правила, уровни архитектуры, немного поговорим о шаблонах и тестах.