Представьте, что перед вами чертежи здания. Они подготовлены архитектором и представляют собой планы здания. О чем они вам говорят?
Если это чертежи коттеджа, вы наверняка увидите на них парадный вход, вестибюль, ведущий в гостиную и, может быть, в столовую. Рядом со столовой, скорее всего, будет расположена кухня. Рядом с кухней, вероятно, будет малая столовая и где-то поблизости — общая комната. При рассмотрении этих планов у вас не возникнет сомнений, что вы видите коттедж для одной семьи. Архитектура как бы кричит: «Это коттедж».
А теперь представьте, что вы рассматриваете архитектуру библиотеки. Вы наверняка увидите центральный вход, места для размещения библиотекарей, читальные залы, небольшие конференц-залы и галереи для хранения книг. Архитектура кричит: «Это библиотека».
А что кричит архитектура вашего приложения? Увидев высокоуровневую структуру каталогов и пакетов с исходным кодом, услышите ли вы, как она кричит: «Это медицинская система», или «Это система учета», или «Это система управления складским хозяйством»? Или вы услышите: «Rails», или «Spring/Hibernate», или «ASP»?
Тема архитектуры
Отвлекитесь и прочитайте фундаментальный труд Ивара Якобсона об архитектуре программного обеспечения: книгу Object Oriented Software Engineering[43]. Обратите внимание на подзаголовок книги: A Use Case Driven Approach[44]. В этой книге Якобсон подчеркивает, что архитектура программного обеспечения — это структура, поддерживающая варианты использования системы. В точности как план библиотеки или коттеджа кричит о назначении здания, архитектура программного обеспечения должна кричать о вариантах использования приложения.
Архитектуры не связаны (и не должны быть связаны) с фреймворками. Архитектура не должна определяться фреймворками. Фреймворки — это инструменты, а вовсе не аспекты, определяющие черты архитектуры. Если ваша архитектура опирается на фреймворки, она не сможет опираться на варианты использования.
Цель архитектуры
Хорошие архитектуры опираются на варианты использования и помогают архитекторам описывать структуры, поддерживающие эти варианты использования, не связывая себя фреймворками, инструментами и окружениями. Взгляните на план коттеджа еще раз. Одной из первоочередных забот архитектора является удобная планировка дома, а не выбор материала для стен. Действительно, архитектор прилагает максимум усилий, чтобы дать домовладельцу возможность самому выбрать материал (кирпич, камень или дерево), потом, когда планы, соответствующие вариантам использования, уже будут начерчены.
Хорошая архитектура позволяет отложить решение о выборе фреймворков, баз данных, веб-серверов и других инструментов и элементов окружения. Фреймворки относятся к возможностям, которые должны оставаться открытыми. Хорошая архитектура позволяет отложить выбор Rails, Spring, Hibernate, Tomcat или MySQL на отдаленный срок. Хорошая архитектура позволяет с легкостью менять подобные решения. Хорошая архитектура подчеркивает варианты использования и отделяет их от второстепенных проблем.
А что насчет Веб?
Веб — это архитектура? Зависит ли архитектура системы от того факта, что свои услуги она предоставляет через Веб? Конечно нет! Всемирная паутина (или Веб) — это механизм доставки, устройство ввода/вывода. И архитектура вашего приложения должна интерпретировать его именно так, а не иначе. Факт предоставления услуг через Веб — это деталь, и она не должна определять структуру системы. В действительности решение о предоставлении услуг через Веб относится к разряду решений, которые должны откладываться. Архитектура системы должна быть максимально нейтральной к механизмам доставки услуг. У вас должна иметься возможность реализовать систему в форме консольного приложения, веб-приложения, толстого клиента или даже веб-службы без чрезмерного усложнения или изменения основной архитектуры.
Фреймворки — это инструменты, а не образ жизни
Фреймворки могут быть очень мощными и удобными. Авторы фреймворков часто глубоко убеждены в своих творениях. Примеры, которые они пишут, рассказывают об использовании их фреймворков с точки зрения истинно верующих. Другие авторы, пишущие книги о фреймворках, тоже часто стоят на позициях истинной веры. Они показывают способы использования фреймворков, часто с позиции всеобъемлющего, всепроникающего и повсеместного применения.
Вы не должны вставать на эту позицию.
Рассматривайте фреймворки с холодком. Смотрите на них скептически. Да, они могут помочь, но какой ценой? Спросите себя, как бы вы их использовали и как защитились бы от них. Подумайте, как сохранить приоритет вариантов использования для архитектуры. Разработайте стратегию, не позволяющую фреймворку влиять на архитектуру.
Тестируемые архитектуры
Если архитектура вашей системы основана исключительно на вариантах использования и вам удалось удержать фреймворки на расстоянии, у вас должна иметься возможность протестировать все эти варианты использования без привлечения фреймворков. Вы не должны испытывать потребности в веб-сервере для выполнения тестов. Вы не должны испытывать потребности в подключении к базе данных для выполнения тестов. Ваши сущности должны быть самыми обычными объектами, не зависящими от фреймворков, баз данных или чего-то другого. Ваши объекты вариантов использования должны координировать действия сущностей. Наконец, должна иметься возможность протестировать их вместе без привлечения любых фреймворков.
Заключение
Ваша архитектура должна рассказывать о системе, а не о фреймворках, использованных в системе. Если вы работаете над системой для медицинского учреждения, тогда первый же взгляд на репозиторий с исходным кодом должен вызывать у новых программистов, подключающихся к проекту, мысль: «Да, это медицинская система». Новые программисты должны иметь возможность изучить все возможные варианты использования, даже не зная, как будут доставляться услуги системы. Они могут подойти к вам и сказать:
«Мы видим что-то похожее на модели, но где контроллеры и представления?»
А вы должны ответить:
«Это такая мелочь, которая нас пока не заботит. Мы решим этот вопрос позже».
Глава 22. Чистая архитектура
За последние несколько десятилетий мы видели целый ряд идей об организации архитектур. В том числе:
• Гексагональная архитектура (Hexagonal Architecture, также известна как архитектура портов и адаптеров), разработанная Алистером Кокберном и описанная Стивом Фриманом и Натом Прайсом в их замечательной книге Growing Object Oriented Software with Tests.
• DCI[45], предложенная Джеймсом Коплиеном и Трюгве Реенскаугом.
• BCE[46], предложенная Иваром Якобсоном в книге Object Oriented Software Engineering: A Use-Case Driven Approach.
Несмотря на различия в деталях, все эти архитектуры очень похожи. Они все преследуют одну цель — разделение задач. Они все достигают этой цели путем деления программного обеспечения на уровни. Каждая имеет хотя бы один уровень для бизнес-правил и еще один для пользовательского и системного интерфейсов.
Каждая из этих архитектур способствует созданию систем, обладающих следующими характеристиками:
• Независимость от фреймворков. Архитектура не зависит от наличия какой-либо библиотеки. Это позволяет рассматривать фреймворки как инструменты, вместо того чтобы стараться втиснуть систему в их рамки.
• Простота тестирования. Бизнес-правила можно тестировать без пользовательского интерфейса, базы данных, веб-сервера и любых других внешних элементов.
• Независимость от пользовательского интерфейса. Пользовательский интерфейс можно легко изменять, не затрагивая остальную систему. Например, веб-интерфейс можно заменить консольным интерфейсом, не изменяя бизнес-правил.
• Независимость от базы данных. Вы можете поменять Oracle или SQL Server на Mongo, BigTable, CouchDB или что-то еще. Бизнес-правила не привязаны к базе данных.
• Независимость от любых внешних агентов. Ваши бизнес-правила ничего не знают об интерфейсах, ведущих во внешний мир.
Диаграмма на рис. 22.1 — это попытка объединить все эти архитектуры в одну практически осуществимую идею.
Рис. 22.1. Чистая архитектура
Правило зависимостей
Концентрические круги на рис. 22.1 представляют разные уровни программного обеспечения. Чем ближе к центру, тем выше уровень. Внешние круги — это механизмы. Внутренние — политики.
Главным правилом, приводящим эту архитектуру в действие, является правило зависимостей (Dependency Rule):
Зависимости в исходном коде должны быть направлены внутрь, в сторону высокоуровневых политик.
Ничто во внутреннем круге ничего не знает о внешних кругах. Например, имена, объявленные во внешних кругах, не должны упоминаться в коде, находящемся во внутренних кругах. Сюда относятся функции, классы, переменные и любые другие именованные элементы программы.
Точно так же форматы данных, объявленные во внешних кругах, не должны использоваться во внутренних, особенно если эти форматы генерируются фреймворком во внешнем круге. Ничто во внешнем круге не должно влиять на внутренние круги.
Сущности
Сущности заключают в себе критические бизнес-правила уровня предприятия. Сущность может быть объектом с методами или набором структур данных и функций. Сама организация не важна, если сущности доступны для использования разными приложениями на предприятии.
Если вы пишете только одно приложение и не для предприятия, эти сущности становятся бизнес-объектами приложения. Они инкапсулируют наиболее общие и высокоуровневые правила. Их изменение маловероятно под влиянием внешних изменений. Например, трудно представить, что эти объекты изменятся из-за изменения структуры навигации или безопасности страницы. Никакие изменения в работе любого конкретного приложения не должны влиять на уровень сущностей.
Варианты использования
Программное обеспечение на уровне вариантов использования содержит бизнес-правила, характерные для приложения. Оно инкапсулирует и реализует все варианты использования системы. Варианты использования организуют поток данных в сущности и из них и требуют от этих сущностей использовать их критические бизнес-правила для достижения своих целей.
Изменения внутри этого уровня не должны влиять на сущности. Аналогично изменения во внешних уровнях, например в базе данных, пользовательском интерфейсе или в любом из общих фреймворков, не должны влиять на этот уровень. Уровень вариантов использования изолирован от таких проблем.
Но изменения в работе приложения безусловно повлияют на варианты использования и, соответственно, на программное обеспечение, находящееся на этом уровне. Изменение деталей вариантов использования определенно затронет некоторый код на этом уровне.
Адаптеры интерфейсов
Программное обеспечение на уровне адаптеров интерфейсов — это набор адаптеров, преобразующих данные из формата, наиболее удобного для вариантов использования и сущностей, в формат, наиболее удобный для некоторых внешних агентов, таких как база данных или веб-интерфейс. Именно на этом уровне целиком находится архитектура MVC графического пользовательского интерфейса. Презентаторы, представления и контроллеры — все принадлежат уровню адаптеров интерфейсов. Модели — часто лишь структуры данных, которые передаются из контроллеров в варианты использования и затем обратно из вариантов использования в презентаторы и представления.
Аналогично на этом уровне преобразуются данные из формата, наиболее удобного для вариантов использования и сущностей, в формат, наиболее удобный для инфраструктуры хранения данных (например, базы данных). Никакой код, находящийся в других внутренних кругах, не должен ничего знать о базе данных. Если данные хранятся в базе данных SQL, тогда весь код на языке SQL должен находиться именно на этом уровне, точнее, в элементах этого уровня, связанных с базой данных.
Также в этом уровне находятся любые другие адаптеры, необходимые для преобразования данных из внешнего формата, например полученных от внешней службы, во внутренний, используемый вариантами использования и сущностями.
Фреймворки и драйверы
Самый внешний уровень модели на рис. 22.1 обычно состоит из фреймворков и инструментов, таких как база данных и веб-фреймворк. Как правило, для этого уровня требуется писать не очень много кода, и обычно этот код играет роль связующего звена со следующим внутренним кругом.
На уровне фреймворков и драйверов сосредоточены все детали. Веб-интерфейс — это деталь. База данных — это деталь. Все это мы храним во внешнем круге, где они не смогут причинить большого вреда.
Толька четыре круга?
Круги на рис. 22.1 лишь схематически изображают основную идею: иногда вам может понадобиться больше четырех кругов. Фактически нет никакого правила, утверждающего, что кругов должно быть именно четыре. Но всегда действует правило зависимостей. Зависимости в исходном коде всегда должны быть направлены внутрь. По мере движения внутрь уровень абстракции и политик увеличивается. Самый внешний круг включает низкоуровневые конкретные детали. По мере приближения к центру программное обеспечение становится все более абстрактным и инкапсулирует все более высокоуровневые политики. Самый внутренний круг является самым обобщенным и находится на самом высоком уровне.
Пересечение границ
Справа внизу на рис. 22.1 приводится пример пересечения границ кругов. На нем изображены контроллеры и презентаторы, взаимодействующие с вариантами использования на следующем уровне. Обратите внимание на поток управления: он начинается в контроллере, проходит через вариант использования и завершается в презентаторе. Отметьте также, как направлены зависимости в исходном коде: каждая указывает внутрь, на варианты использования.
Обычно мы разрешаем это кажущееся противоречие с использованием принципа инверсии зависимостей (Dependency Inversion Principle). В таких языках, как Java, например, мы можем определять интерфейсы и использовать механизм наследования, чтобы направление зависимостей в исходном коде было противоположно направлению потока управления на границах справа.
Допустим, что вариант использования должен вызвать презентатора. Такой вызов нельзя выполнить непосредственно, потому что иначе нарушится правило зависимостей: никакие имена, объявленные во внешних кругах, не должны упоминаться во внутренних. Поэтому вариант использования должен вызвать интерфейс (на рис. 22.1 подписан как «Порт вывода варианта использования»), объявленный во внутреннем круге, а презентатор во внешнем круге должен реализовать его.
Тот же прием используется для всех пересечений границ в архитектуре. Мы используем преимущество динамического полиморфизма, чтобы обратить зависимости в исходном коде в направлении, противоположном потоку управления, и тем самым соблюсти правило зависимостей при любом направлении потока управления.
Какие данные пересекают границы
Обычно через границы данные передаются в виде простых структур. При желании можно использовать простейшие структуры или объекты передачи данных (Data Transfer Objects; DTO). Данные можно также передавать в вызовы функций через аргументы. Или упаковывать их в ассоциативные массивы или объекты. Важно, чтобы через границы передавались простые, изолированные структуры данных. Не нужно хитрить и передавать объекты сущностей или записи из базы данных. Структуры данных не должны нарушать правило зависимостей.
Например, многие фреймворки для работы с базами данных возвращают ответы на запросы в удобном формате. Их можно назвать «представлением записей». Такие представления записей не должны передаваться через границы внутрь. Это нарушает правило зависимостей, потому что заставляет внутренний круг знать что-то о внешнем круге.
Итак, при передаче через границу данные всегда должны принимать форму, наиболее удобную для внутреннего круга.
Типичный сценарий
Диаграмма на рис. 22.2 показывает типичный сценарий работы веб-системы на Java, использующей базу данных. Веб-сервер принимает исходные данные от пользователя и передает их контроллеру Controller в левом верхнем
Рис. 22.2. Типичный сценарий работы веб-системы на Java, использующей базу данных
углу. Контроллер упаковывает данные в простой Java-объект InputData и передает его через интерфейс InputBoundary классу UseCaseInteractor. Класс UseCaseInteractor интерпретирует данные и использует их для управления действиями сущностей Entities. Он также переносит данные из базы данных Database в память сущностей Entities через интерфейс DataAccessInterface. По завершении UseCaseInteractor забирает данные из сущностей Entities и конструирует из них другой простой Java-объект OutputData. Затем объект OutputData передается через интерфейс OutputBoundary презентатору Presenter.
Презентатор Presenter переупаковывает данные из объекта OutputData в объект ViewModel, еще один простой Java-объект, содержащий в основном строки и флаги, используемые представлением View для отображения данных. Объект OutputData мог бы содержать объекты типа Date, но Presenter преобразует их в процессе формирования ViewModel в строковые значения, пригодные для отображения. То же произойдет с объектами типа Currency и любыми другими бизнес-данными. Имена кнопок Button и пунктов меню MenuItem помещаются в ViewModel вместе с флагами, сообщающими представлению View, следует ли отобразить эти кнопки и пункты меню в неактивном состоянии.
В результате представлению View практически ничего не приходится делать, кроме как перенести данные из ViewModel в HTML-страницу.
Обратите внимание на направления зависимостей. Все они пересекают линии границ только в направлении внутрь, следуя правилу зависимостей.
Заключение
Следование этим простым правилам не требует больших усилий и поможет сохранить душевный покой в будущем. Разделив программное обеспечение на уровни и соблюдая правило зависимостей, вы создадите систему, которую легко протестировать — со всеми вытекающими из этого преимуществами. Когда какой-либо из внешних элементов системы, например база данных или веб-фреймворк, устареет, вы сможете без всякой суеты заменить его.
Глава 23. Презентаторы и скромные объекты