Среди IT девелоперов давно стало популярным выражение "слоистая архитектура". Это последний тренд, особенно с подачи таких мэтров программирования, как дядя Боб (Роберт Мартин) с его книгой "Чистая архитектура", Эрик Эванс ("Предметно-ориентированное проектирование (DDD)"), Вон Вернон ("Реализация методов предметно-ориентированного проектирования"), Джимми Нильсон ("Применение DDD и шаблонов проектирования") и многих других.
Однако эти книги читать весьма сложно, когда в голове нет простой и ясной картины, что это такое на самом деле, как выглядит, как это потрогать руками.
Я не стану углубляться в подробности DDD, нагружать вас мудреными терминами плана "Ubiquitous Language", "Domain Model", "Bounded Context" и т.д.Все это подробно описано в книгах, да и в сети полно хороших статей на тему Domain Driven Design.
Я просто хочу показать саму слоистую архитектуру немного со стороны, абстрагировавшись от деталей.
Сложность понимания слоистой архитектуры, на мой взгляд, заключается в визуальном её восприятии. Дело в том, что слои часто изображают в виде концентрических кругов. Есть даже такое понятие, как "луковая архитектура". Самая простая картинка:

На первый взгляд ничего сложного и все понятно. Фреймворк, как большой добрый дядя, заботливо окружает написенное нами приложение, в котором уютненько расположились модели. Даже стрелочки для наглядности есть.
Беда в том, что стрелочками указывается организация зависимости, а не порядок разработки архитектуры. Это говорит о том, что внутренние слои не должны зависеть от внешних. И когда, как гром среди ясного неба мы слышим фразу "разработку нужно вести снизу вверх", наступает когнитивный диссонанс. Как же можно сначала съесть картошку, а потом её пожарить? Как можно написать в прилжении контроллер, если не известно, какой там вообще фреймворк. Как написать модель, если не известно, кто и как её вызовет. Как вообще можно использовать библиотеки фреймворка, ничего о них не зная? И вообще, где тут верх, и где низ?
Давайте немного изменим представление, сделаем слои плоскими. Тогда хотя бы верх-низ станет более понятным.

Подавляющее большинство разработчиков привыкло работать как раз сверху вниз. Сначала ставим фреймворк, потом пишем контроллеры и вьюшки, потом разрабатываем модель. При таком раскладе результат можно в любой момент увидеть в браузере. Легко и просто.
Однако тут и таится самый главный подвох - очень велик соблазн использовать возможности фреймворка в нижних слоях. А это испортит правило зависимости, все станет монолитным и откажется работать в отсутствии фреймворка. Или его замене. Да даже версию фреймворка обновить будет сложно.
Но снизу вверх писать непривычно и не уютно. И вот тут главная фишка.
А не нужно снизу вверх.
Нужно просто перевернуть схему. Так будет намного комфортнее:
Напомню, стрелочками указывается направление зависимости. Доменная модель ни от чего не зависит. Приложение зависит от модели. Фреймворк зависит от приложения. Но разработка теперь идет как положено, сверху вниз. Пока это сложно устаканить в голове, посмотрим, как это смотрится в кодовом представлении.
Для чистоты эксперимента представим, что разработку проекта ведут три независимых компании. Ну или три человека, как угодно. Каждый отвечает за свой слой, и они не знают, что делают остальные. Сначала уточним ответственности.
1. Разработчики модели отвечают за бизнес-логику.
2. Разработчики приложения отвечают за организацию управления моделью и представление.
3. Фреймворку достается роутинг, сборка библиотек и некоторые особенности инфраструктуры (базы данных, логирование и пр)
И так, техническое задание: сделать калькулятор, умножающий произвольное целое число на 2.
Первая контора зсучает рукава и, выбиваясь из сил и дедлайна, погружается в работу. Однако как же проверить результат, если нет никакой инфраструктуры? Для этого придумали методологию TDD (Test-Driven Development). Разработка через тестирование. Эта техника весьма полезна, так как позволяет разработчику обходиться без остальных слоев, а так же заставляет покрывать скрипты тестами, хотелось бы этого или нет.
Если вы пользовались юнит-тестами, это для вас не новость. Остальным советую обратить на них пристальное внимание. Как с ними бороться, как ставить и использовать, да и вообще TDD - большая отдельная тема. Вся информация есть в сети, найти не сложно.
Мы же пока пойдем немного другим путем, максимально все упростив.
Основной принцип TDD - сначала пишется тест, потом сам скрипт. Но сначала организуем файловую структуру. Она может быть произвольной, допустим такой:

В файле Calculator.php обозначаем намерения. Это класс и пока пустой метод удвоения:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class Calculator
{
/**
* Метод умножения произвольного числа на 2
*
* @param int $data
*/
public function doubling(int $data)
{
}
}
Заполнять метод функционалом будем позже, сначала нужно написать тест. Не мудрствуя лукаво напишем так:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class CalculatorTest
{
public function testDoubling()
{
include '../Domain/Calculator.php';
$calculator = new Calculator;
$result = $calculator->doubling(5);
echo "\n- - - - - - - - -\n";
echo ($result === 10) ? 'OK' : 'Test is failed';
echo "\n- - - - - - - - -\n";
}
}
(new CalculatorTest)->testDoubling();
Не думаю, что тут нужно что то объяснять. Мы ждем результат удвоение числа 5, и если он отвечает ожиданиям - тест пройден. Теперь нам для проверки работоспособности функционала самого калькулятора не нужна никакая инфраструктура. Более того, нам даже браузер не нужен: можно запустить тест из консоли. Открываем в папке Tests командную строку и пишем туда эту строчку:
1
2
3
$ php CalculatorTest.php
В итоге получем такой ответ.

Оно закономерно: функционала нет, тест провален.
Вот теперь пора приступать к разработке метода, попутно его тестируя:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
class Calculator
{
/**
* Метод умножения произвольного числа на 2
*
* @param int $data
*
* @return int
*/
public function doubling(int $data)
{
return $data * 2;
}
}
Запускаем тест, всё окей.

Подведем промежуточный итог. Первая компания справилась со своей задачей совершенно не зная, какая будет реализация слоя приложения. Теперь нужно только передать дальше документацию, что для удвоения призвольного целого числа нужно использовать метод doubling() класса Calculator. А что там внутри - никого не касается. Инкапсуляция таки. Правило зависимостей соблюдено - доменная модель ни от чего не зависит, слой приложения будет зависеть от слоя домена.
Теперь в бой вступает вторая контора, которая отвечает за приложение. Сначала добавляет в проект свои файлы и директории:

Пишет незамысловатый контроллер:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
class CalculatorController
{
public function actionDoubling()
{
if (!empty($_POST['num'])) {
include __DIR__ .'/../Domain/Calculator.php';
$calculator = new Calculator;
$result = $calculator->doubling($_POST['num']);
}
include __DIR__ .'/../View/Calculator.tpl';
}
}
и часть вьюшки - шаблон с формой:
1
2
3
4
5
6
7
8
<form action="" method="post">
Число
<br />
<input name="num" type="text" /><br />
<input type="submit" value="Умножить на 2" />
</form>
<?php if (!empty($result)) echo 'Результат: '. $result; ?>
Теперь возникает вопрос - а как это протестировать? Роутинга нет, так как нет пока фреймворка, а юнит-тесты тут не особо годятся. К счастью есть еще функциональные тесты. Это тоже большая отдельная тема, я покажу самый простой способ. Напишем простенький скрипт теста, который запустит контроллер и вызовет action
CalculatorFunctionalTest.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class CalculatorFunctionalTest
{
public function calculatorRun()
{
include __DIR__ .'/../protected/Controllers/CalculatorController.php';
$controller = new CalculatorController;
$controller->actionDoubling();
}
}
(new CalculatorFunctionalTest)->calculatorRun();
Я положу этот тест в папку FunctionalTests, которая будет находится в месте, доступном по HTTP протоколу. Потому что в директорию protected обычно такой доступ бывает запрещен:

Теперь можно запустить приложение из браузера, этот тест будет выполнять роль роутера. В итоге имеем:

Второй промежуточный итог: команда разработчиков приложения бодро рапортует - пятилетка выполнена за 4 года! Контроллер и представления готовы, работоспособны и протестированы! Хотя фреймворком до сих пор и не пахло. Опять соблюдено правило зависимости, слой приложения зависит от слоя домена, но не зависит от слоя фреймворка.
Э, нет! Скажите вы. Это читерство. Подключить файл шаблона инклюдом и дурак сможет. А вот попробуйте-ка заюзать рендер фреймворка, да еще и с шаблонизатором, вот тогда поговорим. И будете совершенно правы.
Но умные головы для решения таких проблем придумали паттерн "адаптер". Это ни что иное, как обертка над функционалом следующего слоя. Промежуточное звено. Дополнительный слой. В итоге имеем следующую картину:
Почему слой адаптеров расположен так странно? Я специально разместил слои не друг над другом по двум причинам. Первая - фреймворк может зависеть от приложения как напрямую, так и через адаптеры. А о второй расскажу чуть позже.
Доведем до логического завершения работу второй команды. Исправим контроллер:
CalculatorController.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
class CalculatorController
{
public function actionDoubling()
{
if (!empty($_POST['num'])) {
include __DIR__ .'/../Domain/Calculator.php';
$calculator = new Calculator;
$result = $calculator->doubling($_POST['num']);
}
include __DIR__ .'/../Adapters/CalculatorViewAdapter.php';
(new CalculatorViewAdapter)->render($result);
}
}
Добавим подслой адаптеров:

Ну и сам адаптер:
1
2
3
4
5
6
7
8
9
10
11
<?php
class CalculatorViewAdapter
{
public function render($result)
{
include __DIR__ .'/../View/Calculator.tpl';
}
}
Теперь можно в этом методе задействовать рендер фреймворка, или любой другой способ отображения, шаблонизатор к примеру.
А сейчас о том, почему слой адаптеров находится не между приложением и фреймворком. Это станет понятно из следующей картинки:

Дело в том, что доменный слой не должен зависеть ни от чего. Но ему могут понадиться библиотеки или функционал фреймворка. Так что эта связь решается тоже через слой адаптеров. А чтобы разработчик адаптера узнал, чего ждет от фреймворка домен, используют так называемые порты. Это ничто иное, как обычный интерфейс.
Допустим потребуется не только умножить на два, но еще возвести в степень. Представим, что функции pow() в PHP нет, а эту роль выполняет библиотека фреймворка. Или какая-нибудь сторонняя либа. Разработчик адаптера (в данном случае это третья компания, заведующая фреймворком) не может лезть в реализацию домена, тем более навязывать свой функционал. Но разработчик домена делает "заявку", мол мне нужна такая библиотека, кторая сможет возвести число в степень. И делает он эту заявку в виде интерфейса.
MathAdapterInterface.php
1
2
3
4
5
6
7
8
9
10
11
12
<?php
interface MathAdapterIntrface
{
/**
* Возведение числа $base в степень $exp
*
* @return int
*/
public function pow($base, $exp);
}
Теперь можно приступать к работе третьей команде - разработчиков (ну или уверенных пользователей) фреймворка. Они внедряют в почти готовый проект инфраструктуру (роутинг, библиотеки, базы данных и так далее). И делают это на основании портов/интерфейсов домена и приложения. Это уже детали, скучно и не интересно.
Такой порядок разработки может показаться довольно сложным, но это огромный задел на будущее. При такой архитектуре можно свободно менять библиотеки, поддерживая актуальные версии, и даже всеь фреймворк целиком, не затрагивая код приложения и домена. Нужно подменить только необходимый адаптер. А значит такой проект будет жить долго и счастливо, не требуя особых усилий для подержания в тренде последних технологий.
Разумеется все это очень упрощенно, одни инклюды в классах чего стоят. И конечно тут я рассказал далеко не все, это просто первый толчек в направлении изучения тонкостей "слоистой архитектуры". Тонкостей много - работа с хранилищами данных (репозитории и пр), можно юзать паттерны "команда" и "шина команд" для еще большей дезинтеграции, для общения между слоями используются DTO, доменный слой делится на контексты, и куча прочих изысков, описанных в умных книгах, перечень которых я привел в самом начале.
Но надеюсь моя статья даст возможность взглянуть на это уже не через призму непонятных терминов и тумана структуры, а смелым взглядом разработчика, идущего по тернистому пути чистой архитектуры.
Роман