Структура приложения и пространств имён при DDD в Laravel

Я обычно разделяю большие приложения на 4-5 пространств имён, которые привязаны к глобальному app пространству имён. Например, я хочу создать приложение ToDo List, так что базовое пространство имён будет ToDo.

Затем у меня есть 3 пространства имён внутри этого:

  • Applaravel-специфичная функциональность — классы валидаторы, сервис-провайдеры базовой модели и тд;
  • Domain — вся моя бизнес-логика, такая как сущности, интерфейсы репозиториев, сервисы домена;
  • Infrastructure — вся базовая логика. Это включает в себя реализации репозиториев, декораторы кэша и тд;

В дополнение к этому, у меня есть по крайней мере одно пространство имён для взаимодействия с внешним миром. Для типичного веб-приложения это Http, для REST API это Api, для команд Artisan это CLI. Итоговая структура приложения может быть примерно такой:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

app/

—-ToDo/

———App/

————Providers/

—————-ToDoServiceProvider.php

—————-ConfigServiceProvider.php

————Validators/

—————-LaravelValidator.php

————ValueObject.php

————BaseModel.php

———Domain/

————List/

—————-EloquentList.php

—————-ListRepository.php

—————-ListService.php

—————-ListValidator.php

—————-Priority.php

————Task/

—————-EloquentTask.php

—————-TaskRepository.php

—————-TaskService.php

—————-TaskValidator.php

———Http/

————Lists/

—————-ListController.php

—————-ListPresenter.php

—————-ListViewComposer.php

————Tasks/

—————-TaskController.php

———Infrastructure/

————Lists/

—————-ListRepositoryCacheDecorator.php

—————-EloquentListRepository.php

————EloquentTaskRepository.php

Пространство имён App

Это то что я рассматриваю как основную точку взаимодействия между моей бизнес-логикой, логикой инфраструктуры, взаимодействием с внешним миром (web, REST API, CLI), и Laravel. Это то место где я настраиваю все мои зависимости и храню классы которые расширяются потом кодом в домене, и не содержат никакой логики домена в себе. Основное правило здесь — большинство классов будут абстрактными.

  • Связывание TaskRepository и EloquentTaskRepository через сервис-провайдер
  • Связывание конфигурации из одного из конфигов Laravel и класса, который требует её как часть конструктора[1]
  • Создание абстрактного класса LaravelValidator, который содержит в себе интерфейс без реальных правил валидации бизнес-логики
  • Создание класса BaseModel который предоставляет общую функциональность моим сущностям
  • Создание специфичных сервисов уровня приложения(не сервисов домена), которые зависимы от Laravel, например утилиты для перемещения файлов[2]
  • Создание абстрактных классов которые используются различными портами(web, REST API и тд), например ApiController который имеет вспомогательные методы для трансформации ответов сервера, возвращения API-специфичных кодов ошибок, внедрения meta-информации и тд.

[1] Например, у меня может быть Validator, который зависит от массива правильныхTask Categories, которые определены в файле конфигурации. Конструктор принимает массив категорий и я использую сервис-провайдер для для связи:

1

2

3

4

5

6

7

8

9

10

//ToDo/App/Providers/ConfigServiceProvider.php

public function register()

{

    $categories = $this->app[‘config’]->get(‘todo.categories’);

    $this->app->bind(‘ToDo\Domain\Tasks\TaskValidator.php’, function($app)

    {

        $categories = $this->app[‘config’]->get(‘todo.categories’);

        $factory = $this->app->make(‘Illuminate\Validation\Factory’);

        return new TaskValidator($factory, $categories);

}

[2] Здесь возможно расхождение во мнениях, но я верю что моя логика домена не должна заботиться о том в какой директории файл, до тех пор пока она не получит доступ к этому файлу. Поэтому сервис для определения расположения файлов это слой инфраструктуры или приложения.

Пространство имён Domain

Тут просто, это место где живёт вся моя бизнес-логика. Сущности, Объекты-значения, Валидаторы, Спецификации, вся функциональность которая доставляет результат клиенту находится здесь. В идеале мои сущности должны быть POPO(Plain Old PHP Objects) и не зависеть от Eloquent, но пока это сложно заставить работать хорошо, я имею склонность использовать Eloquent для моих сущностей — это единственное место где я сильно нарушаю принцип разделения ответственности SRP. Но пространство имёнToDo\Domain\List будет содержать следующее:

  • EloquentList — сущность List, и я обычно использую мутаторы для объектов-значений. Если нам нужно загрузить в сущность List все задания, это будет сделано автоматически.
  • Priority — самовалидирующийся объект-значение, который содержит приоритет сущности List. Он может принимать цифровое значение от 1 до 10, или что-то простое типа «низкий», «средний», «высокий».
  • ListRepository — интерфейс репозитория с методами такими как напримерfindById($id), store($list), setPerPage($perPage = 0), sortBy($field, $direction = 'DESC'), existsByName($name) и тд.
  • ListService — это главный класс с которым будет взаимодействовать мой порт. Если функциональность достаточно сложная, я разобью его на несколько классов.
  • ListValidator — это валидатор который ответственный за реализацию валидации и ему нужны внешние данные о том что мы можем делать с сущностями и объектами-значениями, а что нет. Например, мы должны удостовериться что название для Listуникальное среди остальных сущностей List(что требует репозиторий как зависимость так как это нельзя сделать в рамках только одной сущности List).

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

Пространство имён Http

Это место где находится вся логика которая связывает моё приложение с внешним миром(например браузером). Всё что отвечает за HTTP запросы должно жить здесь. Это может включать в себя следующее:

  • ListController — достаточно самообъясняюще. Он может расширяться из абстрактного контроллера в пространстве имён App если нужно, и может иметьListService внедрённым в себя. Он может быть ответственным за ловлю исключений валидации которые приходят из класса ListService, их обработку путём редиректа и отправки ошибок во вьюху.
  • ListPresenter — если нам нужно добавить html-специфичное форматирование для списков, например форматирование дат или преобразование цифр приоритета в более читаемый label, эта функциональность должна быть здесь. Контроллер должен быть ответственным за создание экземпляра объекта, его преобразования и доставки его во вьюху.
  • ListViewComposer — если есть какая либо дополнительная информация которую мы должны прикрепить к одной или нескольким вьюхам списков, это будет жить здесь. Здесь может находиться всё и даже могут быть внедрены различные сервисы приложения или домена.

Это только один «порт» моего приложения. Если у меня есть команды Artisan или REST API, то для них будет своё пространство имён с соответствующими классами.

Пространство имён Infrastructure

Всё что относится к базовому состоянию находится здесь. Самая очевидная вещь которая должна быть здесь это реализации репозиториев. Например:

  • EloquentListRepository — Eloquent-специфичная реализация интерфейсаListRepository в пространстве имён Domain.
  • ListRepositoryCacheDecorator — это реализация того же интерфейса что и у репозитория и она является декоратором который добавляет функциональность кеширования.

Другие вещи которые могут быть здесь: класс ListDatabaseMapper, если используется паттерн Data Mapper вместо Eloquent.