На собеседовании кандидат без запинки расшифровывает каждую букву SOLID и рассказывает о Strategy и Observer. Потом получает простое задание: спроектировать модуль заказов для интернет-магазина. И тут пауза. Минута, вторая. На доске появляются пять интерфейсов, две фабрики и ни одного ответа на вопрос, где хранится статус заказа.
Так бывает, когда правила изучены отдельно от контекста, от ответственностей и от того, чем каждое решение обернется через полгода.
Что дают SOLID-принципы и где заканчивается их зона действия
Что такое SOLID, помнит почти каждый, кто готовился к собеседованию на Java или C#. Это пять принципов объектно-ориентированного дизайна, которые систематизировал Роберт Мартин: одна ответственность на класс, расширение без переписывания, корректная подстановка наследников, узкие интерфейсы, зависимость от абстракций. Каждую букву мы уже подробно разобрали в статье про SOLID-принципы и код, который не подводит.
Принципы SOLID работают как линейка. Прикладываешь ее к классу и видишь: вот здесь он делает три вещи одновременно, а здесь модуль верхнего уровня намертво привязан к базе данных. Для оценки кода инструмент очень удобный.
Но линейка не подскажет, что строить. Она не скажет, нужен ли системе отдельный сервис для платежей, должны ли данные обновляться синхронно, можно ли допустить задержку в несколько секунд. Каждый принцип SOLID отвечает на вопрос о форме кода. Вопросы о форме системы остаются открытыми.
Почему знание паттернов еще не означает умения проектировать систему
Паттерны GoF (23 шаблона проектирования из книги Гаммы, Хелма, Джонсона и Влиссидеса 1994 года) описывают проверенные решения конкретных проблем. Factory прячет сложное создание объектов. Strategy позволяет менять алгоритм, не трогая код, который его вызывает. Observer развязывает связь между тем, кто генерирует событие, и тем, кто на него реагирует.
У каждого из них есть условие применения. Есть проблема, есть решение. Нет проблемы, и паттерн превращается в лишний слой кода.
Типичная ошибка выглядит так. Разработчик неделю назад разобрался со Strategy и теперь видит ее везде. Два варианта расчета скидки? Strategy. Три типа уведомлений? Снова она. Через месяц в проекте десять интерфейсов с одной реализацией у каждого, а новичку в команде нужен день, чтобы найти, где на самом деле считается цена.
Поисковые запросы вроде solid паттерн или паттерн solid хорошо показывают эту путаницу. SOLID ищут как еще один паттерн, хотя это набор принципов, которыми паттерны как раз и оценивают. Почувствовать разницу между знанием шаблона и уместным применением помогает курс GRASP & GoF Design Patterns, где Сергей Немчинский разбирает шаблоны на примерах из реальных проектов.
Где между ООП, SOLID и паттернами появляется архитектурное мышление
Пара ООП SOLID стоит в требованиях почти каждой вакансии, и учат эти темы обычно как отдельные разделы учебника. На самом деле они складываются в цепочку, где каждое звено опирается на предыдущее:
- ООП дает инструменты: классы, инкапсуляцию, полиморфизм.
- GRASP (шаблоны распределения ответственностей, которые описал Крейг Ларман) помогает решить, какой класс что должен делать.
- SOLID задает принципы здорового дизайна, чтобы это распределение не рассыпалось при первых изменениях.
- GoF предлагает готовые решения для типичных ситуаций, которые возникают по дороге.
Безупречное знание всех четырех уровней еще не дает архитектурного мышления. Оно появляется выше, когда разработчик смотрит на систему целиком и видит последствия выбора: что подорожает, что упростится, что сломается первым. Грейди Буч, один из авторов UML, писал:
«Архитектура состоит из значимых проектных решений, которые формируют систему, а значимость измеряется стоимостью изменений».
Как эти уровни соединяются между собой, подробно показано в материале о том, как ООП, GRASP, SOLID и GoF складываются в одну систему.
Как SOLID работает на практике, когда единственного правильного ответа нет
Кто вбивает в поиск solid на практике, обычно хочет увидеть пример кода. Попробуем иначе и возьмем решение, у которого нет эталонного ответа.
Сервис заказов напрямую вызывает API одного платежного провайдера. Бизнес говорит, что когда-нибудь, возможно, добавит второго. Возможно. Когда именно, никто не знает.
Принцип инверсии зависимостей подсказывает спрятать оплату за интерфейс. Это час работы и еще один уровень абстракции, через который придется продираться при каждом дебаге. Если второй провайдер появится через два месяца, решение окупится. Если не появится никогда, команда годами будет поддерживать абстракцию ради гипотетического будущего.
Дальше интереснее. Провайдер иногда отвечает по десять секунд. Ждать ответа в том же запросе или отправлять платеж в очередь и уведомлять пользователя позже? SOLID здесь молчит. Это уже вопрос о скорости, надежности, сложности инфраструктуры и терпении клиентов.
Архитектурное решение всегда является trade-off (компромиссом между несколькими желаемыми, но несовместимыми свойствами). Поддерживаемость против скорости разработки. Гибкость против простоты. Кто этого не видит, тот либо переусложняет все подряд, либо не закладывает ничего.
Какие вопросы задает разработчик с архитектурным мышлением
Опытный архитектор редко начинает с вопроса, какой паттерн сюда вставить. Паттерн появляется в конце, как ответ. Сначала идут другие вопросы, куда более приземленные:
- где должна жить эта ответственность и кто за нее отвечает;
- от чего будет зависеть компонент и что будет, когда эта зависимость изменится;
- что придется переписывать через полгода, когда придут новые требования;
- насколько решение усложняет систему для человека, который откроет код впервые;
- что произойдет, когда нагрузка вырастет вдесятеро;
- решаем ли мы реальную проблему или строим красивую архитектуру для себя.
Последний вопрос самый неприятный. Ответ на него регулярно выбрасывает из проекта половину запланированных абстракций. Зато код, который остается, легче читать, тестировать и менять. Собственно, ради этого когда-то и придумали все эти аббревиатуры.
Перестань собирать аббревиатуры и начни взвешивать решения
Архитектурному мышлению не учатся по конспекту. Оно растет из практики, и эту практику можно организовать осознанно. Разбирай чужие решения в open source проектах и спрашивай себя, почему авторы сделали именно так. Записывай компромиссы, на которые идешь сам, хотя бы двумя предложениями в описании пул-реквеста. Проходи code review с обеих сторон и аргументируй каждое замечание. Учись объяснять, почему паттерн здесь не нужен. Это сложнее, чем объяснить, зачем он нужен.
Теорию тоже стоит перестроить. ООП, GRASP, SOLID и GoF работают как одна система, и изучать их стоит так же. На этом построена книга Сергея Немчинского Фундамент архитектуры: основатель FoxmindEd показывает, где эти подходы пересекаются и где каждый из них заканчивается.
SOLID-принципы и паттерны дают словарь. Архитектура начинается там, где ты взвешиваешь последствия решения и можешь объяснить команде, почему выбрано именно оно. Возьми следующую задачу на работе и сначала ответь на вопросы об изменениях и зависимостях. Паттерн подождет.