Для начала хотелось бы затронуть так называемую «микросервисную архитектуру», которая стала довольно популярной. Однако называть её архитектурой не совсем корректно, как заметил Роберт Мартин в своей книге «Чистая архитектура». Микросервис — это один из способов представления компонента общего приложения. Но архитектура, взаимодействие между компонентами, при этом может остаться такой же. Под компонентом понимается наименьшая единица развёртывания — пакет, библиотека или отдельное приложение, сервис, микросервис. В книге критикуется данная «микросервисная архитектура», а точнее, неправильное и неуместное её применение, которое есть во многих случаях и влечёт за собой негативные или крайне негативные последствия для проекта, и происходит «благодаря» низкой компетентности разработчиков или управляющих, которые хотят найти несуществующую здесь «серебряную пулю».
В книге «Чистая архитектура» выделяется два основных слоя: инфраструктура и бизнес-логика. В различной литературе выделяют и другие слои, но какой-то относительно чёткой информации об этом я не встречал. Под инфраструктурой будем понимать логику более характерную для многих приложений, не только для данного, какие-то конкретные интеграции со сторонними компонентами, а также подготовку данных для бизнес-логики. Под бизнес-логикой — логику более характерную для данного приложения она и представляет наибольшую ценность, её нужно стараться выделять, делать более независимой от инфраструктуры. Во-первых, чтобы было проще её читать, тестировать, модифицировать, а во-вторых, чтобы была возможность с наименьшими затратами заменить инфраструктуру или её части, если потребуется.
Примеры будут для PHP и Symfony, но и для других языков и систем программирования многое может быть похожим. Примеры абстрактные, и в разных приложения может всё отличаться. В ранних версиях Symfony была рекомендация делить приложение на бандлы-плагины. Но начиная с версии 3.4 рекомендовано делить приложение через пространства имён. Это практичнее. Структура проекта будет стандартной. В App\Service\
, будет находиться основная логика приложения, бизнес-логика.
Пусть, нужно сделать возможность оформления заказа. Для этого можно создать App\Service\Order\Checkout\OrderCheckoutService
. Здесь будет общая логика оформления заказа. Входные данные можно принимать через параметры функции, но часто удобнее передавать структуру с данными, DTO. Могут понадобиться App\Service\Order\Checkout\InputDto
, возможно, и InputDtoInterface
. Он может содержать массив, коллекцию, из OrderInputDto
, если заказов сразу несколько, например. Похожим образом обстоит дело и с выходными данными.
Если в OrderCheckoutService
много разнообразной логики, можно выносить её в другие классы с разными названиями. Чтобы следовать принципу единой ответственности можно разделить его на несколько классов, например, OrderCheckoutAcmeFeature1Service
, OrderCheckoutAcmeFeature2Service
и так далее.
Ещё понадобится хранить данные заказов в БД, поэтому создадим сущность по зеркальному к сервису пути App\Entity\Order\Order
. Соответственно будет создан и репозиторий App\Repository\Order\OrderRepository
. Имеет смыл делать логику независимой от Entity и Repository. Проще всего это сделать через интерфейсы в App\Service\Order\Checkout\
: OrderInterface
(или CheckoutOrderInterface
) и OrderRepositoryInterface
(или CheckoutOrderRepositoryInterface
). OrderInterface
должен содержать только геттеры и сеттеры данных, необходимых для оформления заказа. OrderRepositoryInterface
должен содержать только нужные запросы к БД и логику связанную с построением запросов. Сущность и репозиторий реализуют эти интерфейсы соответственно. Можно вместо интерфейсов использовать и промежуточную структуру, DTO, но такой способ будет более объёмным в реализации и иметь некоторые другие минусы.
Таким образом, принцип единой ответственности из SOLID можно легко реализовать не привязываясь к структуре БД. Также, реализуется принцип разделения интерфейсов: класс (сервис) не должен ничего знать о данных и функциях, которые ему не нужны. Логика не привязана к конкретной сущности, её можно применить к любым другим данным — принцип инверсии зависимости. Класс с реализацией можно легко подменить через сервис-контейнер — принцип открытости-закрытости. Такой код проще в поддержке, каждый разработчик может выполнять свою задачу независимо от других разработчиков проекта.
Ещё понадобится фабрика или creator для создания экземпляра конкретной сущности. Для этого создадим интерфейс App\Service\Order\OrderFactoryInterface
(или OrderCreatorInterface
, как кому нравится). Обязанность создания можно возложить, например, на класс App\Factory\Order\OrderFactory
, реализующий этот интерфейс и другие подобные интерфейсы, если появятся. Фабрика может содержать единственный метод create
. Фабрику можно положить и в App\Service\Order\OrderFactory
, но нужно помнить, что она относится к инфраструктуре, в отличие от интерфейса.
Надо находить наиболее важную логику приложения, так называемую бизнес-логику, и выделять её в отдельные классы, делать более независимой от других частей приложения, библиотек. Для этого понадобится делать обёртки, адаптеры, посредники, применять инверсию зависимости.
Если интерфейс реализован только в одном месте, сервис-контейнер Symfony, сам подставит соответствующую реализацию в конструктор сервиса. Если реализаций несколько, то нужно уточнить конкретную в файле конфигурации контейнера. У контейнера есть много возможностей, которые полезно знать.
И так, у нас есть OrderCheckoutService
, содержащий бизнес-логику оформления заказа. Он может вызывать напрямую другие сервисы, выше уровнем. Или через инверсию зависимости другие, более конкретные реализации, менее важные детали. Например, может вызвать App\Service\Order\Delivery\OrderDeliveryService
, содержащий логику связанную с доставкой заказа. И эта логика может относиться не только к оформлению. В таком случае мы подразумеваем, что OrderDeliveryService
— это более важная логика, более высокого уровня, чем OrderCheckoutService
, бизнес-логика оформления заказа зависит от бизнес-логики доставки заказа. OrderDeliveryService
, в свою очередь, может вызвать уже конкретный расчёт через сторонний API, например, с помощью App\Service\Delivery\DeliveryCalculatorInterface
с реализацией App\Service\Delivery\DeliveryCalculator
. Здесь калькулятор находится в App\Service\Delivery
потому, что он содержит логику доставки, которая может быть использована отдельно от Order
. Интерфейс использован, чтобы не было зависимости от инфраструктурного кода. Если есть какая-то общая бизнес-логика, которая относится к доставке Order
и к доставке чего-то ещё не являющегося Order
, что сложно представить, то можно поместить её, например, в App\Service\Delivery\DeliveryService
.
Здесь DeliveryCalculator
зависит от бизнес-логики, он реализует её интерфейс. А если понадобится сделать калькулятор независимым от данного приложения и, возможно, вынести его в отдельный пакет и использовать в других приложениях, можно выделить его логику и поместить, например, в App\Util\Delivery\DeliveryCalculator
. А в калькуляторе приложения уже просто вызывать его. Тогда класс App\Service\Delivery\DeliveryCalculator
становится конкретной реализацией адаптера. К нему может добавиться ещё некоторая логика, тогда его уже можно назвать декоратором. А если он будет обращаться к нескольким классам, то это уже посредник. Но смысл один и тот же. Не помешает так же создать обёртки над другими сторонними компонентами, например, над EntityManager. Если есть необходимость в ещё более чётком отделении бизнес-логики, можно весь подобный инфраструктурный код, соединяющий инфраструктуру с бизнес-логикой, перенести из App\Service\
, например, в App\ServiceBridge\
c зеркальной к App\Service\
структурой. И в App\Service\
тогда останется чистая бизнес-логика.
Ещё может быть логика оформления заказа пользователем на сайте и администратором в панели управления, она может в чём-то совпадать, в чём-то отличаться. Тогда можно создать App\Service\Order\Checkout\UserOrderCheckoutService
и App\Service\Order\Checkout\AdminOrderCheckoutService
со своей логикой. Они могут обращаться к общему OrderCheckoutService
, который будет более важен, выше уровнем в таком случае. А могут быть и самостоятельными, тогда их следует перенести на уровень выше, например, в App\Service\Order\UserCheckout\
. Логику, связанную с взаимодействием с пользователем, со страницей оформления заказа можно вынести в App\Service\Order\UserCheckout\UserOrderCheckoutInteractor
, который будет обращаться к UserOrderCheckoutService
. И уже в контроллере вызывать сервис/интерактор. Так же и с AdminOrderCheckoutService
.
Надо следить за зависимостями, не следует допускать двухсторонних, циклических зависимостей между модулями. Иначе их не получится рассматривать каждый как отдельный компонент системы, они все вместе будут представлять собой единый компонент.
Что касается сокрытия классов, в PHP нет такой возможности. Есть какие-то сторонние решения. Но в общем случае делать этого не нужно, так как должно быть понятным из документации или из примеров, как использовать какой-либо компонент, библиотеку. Если этого нет, то это уже проблема другого уровня, профессионального, организационного, и методом сокрытия классов её не решить. Зато сокрытие классов может создать проблемы в использовании пакета. Можно помечать некоторые классы и функции, которые нежелательно переопределять, аннотацией internal
, этого должно быть достаточно. Также, при создании пакета не следует злоупотреблять final
, и private
, а использовать их только там, где это действительно надо, если есть другое, более правильное решение. Если private
используется в коде самого приложения, поправить его будет легко, а вот в стороннем пакете это сделать уже сложнее.
В директории config
можно создать поддиректорию и подключить всё её содержимое в основном файле конфигурации фреймворка services.yaml
. Тогда проще будет разделять конфигурацию на части по файлам, чтобы соотносить с частями приложения. Подобным образом следует делить и другие директории, если они есть: templates, translations, docs, tests, App\Controller, App\EventListener
и другие.
С возможностями современных IDE выделить какой-то определённый компонент из такой системы будет не сложно, если это, конечно, потребуется.
UPD:
Всегда применять описанные практики не стоит, лучше делать это выборочно. Иначе код будет переусложнён, много лишнего придётся писать.