#Project manager
Архитектура
Модули
Код приложения должен разбиваться на модули. Каждый модуль располагается в корневой папке modules, как например сейчас там расположен модуль ProjectManager. Каждый модуль должен содержать корневой файл модуля, например ProjectManagerModule.php, который по сути является сервис провайдером, и отвечает за загрузку частей этого модуля. Чтобы подключить модуль целиком, достаточно будет зарегистрировать его в главном сервис провайдере приложения AppServiceProvider.
Слои и Use Case
Код каждого модуля должен разделятся на слои (layers), каждый из которых предназначен для выполнения определённого рода кода:
-
Application layer
- содержит лоигку, не относящуюся напрямую к бизнес логике. НапримерApplication Use Case
- вариант использования приложения, который реализует конкретный случай, как приложние может быть использовано. Т.е. по сути определяет доступный функционал. Use Case уже использует сущности/сервисы и их методы из слоя основной бизнес логики Domain, и лишь передаёт в них необходимые параметры, полученные, например через контролллер http, и регулирует процесс выполнения кода (например использует нескольо Domain сервисов/сущностей), необходимого для получения результата данного функционала. Например CreateProjectUseCase, показывает, что приложение может создать проект, и определяет входящие параметры в виде имени, описания, и идентификатора пользователя, и далее использует Domain сущность Projcet, для создания проекта. А уже внутри этой доменной сущности, описывается процесс создания проекта с точки зрения бизнеса. Так же Application layer (т.е. и Use Case) может использовать другие слои, например Infrastructure layer, чтобы начать транзакцию, или сделать какое-то логирование, т.е. операции, которые не относятся напрямую к бизнес логике и соответственно не должны присутствовать в Doman layer. -
Domain layer
- слой с основной бизнес логикой, должен сожердать ТОЛЬКО её и абстрагироваться от других слоёв, например такого рода вещей, как взаимодействие с базой данных, за что должен отвечать Infrastructure layer. Работу с хранилищем данных должен осуществялть через такие паттерны, как репозиторий, или любым другим способом используя инверсю зависимостей (IoD). Осуществляется это путём определения необходимого интерфейса (абстрактного класса), и работы с ним. Далее в конфигурации модуля абстрактный класс заменяется конкретной реализацией этого интерфейса, которая уже может находится в другом слое, например Infrastructure. -
Infrastructure layer
- содержит код для работы с инфраструктурой, т.е. внешними источниками данных, как например базы данных, файловое хранилище, различные протоколы, например взаимодействие через http, и всё что с этим связано (например контроллеры), различные сервисы и классы для работы со внешним API и прочее. Тут содержаться уже конкретные реализации абстракций из Domain layer.
Нюансы
Использование return DomainError
, а не throw DomainError
Можно заметить, что в бизнес слое (domain) в моделях (entity) или типах ошибки DomainError или ValidationError не бросяются (throw) а возвращаются. Использование throw нужно, чтобы бросить исключение (exception) а не ошибку (error). Разница в том, что throw используется в исключительной ситуации, чтобы резко остановить выполнение программы, т.к. эта ситуация не была предусмотрена бизнесом или приложением (потому и называется исключительной). Таким образом исключить непредусмотренное поведение. В то время как Domain/Application Error - это предусмотренное поведение, и программа может и должна его учитывать как часть логики, а не исключительную ситуацию. По мимо этого throw ведёт себя подобно goto меткам, и не всегда известно где и какой блок try/catch его обработает. В то время как возвращаемая ошибка приложения передаётся только на уровень вверх, тому коду, который вызвал данный метод. Таким образом получаем лучший контроль над ходом вполнения приложения. В тестовом приложении, при возникновении ошибки она сразу возвращается с помощью return, в то время как в более сложном приложении ошибки могут собираться по ходу выполнения программы (например валидация сразу нескольких полей, или выполнение нескольких связанных бизнес действий) и возвращаются сразу несколько ошибок, чего не сделать через throw. В более поздних версия php (8+) возвращаемое значение метода можно указать в виде объединения (union), таким образом перечислив все возможные варианты исхода выполнения методы, включая ошибочные. Например someMethod(): User | UserCreationError | AccountVerificationError
таким образом более наглядно определив возможные результаты, включая и ошибочные.
Domain model(entity) vs ORM Eloquent Model
Часто всю логику связанную с пользователем запихивают в Eloquent Model. В итоге получаем суперобъект, который отвечает и за работу с базой данных, и за бизнес операции. В этом проекте сделано разделение: отдельно бизнес модели, содержащие методы сожержащие бизнес логику, и соответсвенно находящихся в Domain layer; и отдельно модели базы данных, которые просто содеражт описания таблиц, связей между ними и методы для работы с ними. Т.е. по сути бизнес модели описывают бизнес, а ORM модели описывают структуру хранения данных, что по сути является разными задачами, потому и были разделены. Помимо прочего в Domain model можно более чётко описать данные бизнес модели, и исключить часто не учавствующие в логике поля, например updated_at и прочих. В коде удобнее работать с раздельными моделями, когда я обращаюсь (->) к экземпляру бизнес модели, мне редактор отображает список доступных бизнес действий, когда я обращаюсь к ORM модели - мне отображается список действий для работы с базой, но не всё в перемешку.
Commands and Queries (CQS)
Каждый Use Case приложения в идеале должен быть либо командой(Command), которая меняет состояние системы (например создать пользователя). Либо запросом (Query), которая ничего не меняет, а нужно только для получения данных, чаще всего для отображение на стороне UI. В данном проекте в обоих вариантах используются Domain model. Но в более сложных случаях, для Query можно использовать запросы без участия Domain model. В таких случаях создаются так называемые read model, иногда view model, по сути интерфейс для данных, которые требуются UI, и далее через infrastructure layer получаем нудные данные из базы, и преобразуем их к этой модели (интерфейсу).
Описание
Example of a layered architecture using Laravel. Simple task manager.