Как организовать внутреннюю структуру классов?
Доброго времени суток.
есть система учёта заказов (crm), в ней есть заказы, клиенты, склад, прочие прелести.
есть модели (сущности), каждой таблице в бд соответствует свой одноимённый класс унаследованный от
Model.есть контроллеры для каждой сущности, в них методы для создания, редактирования, удаления, методы для вывода данных (возвращают
Viewс наборами данных, все данные берутся с помощью ORM прямо "на месте").
В данной схеме напрашиваются классы для сущностей, что бы к ним был доступ из любого контроллера (например, в контроллере заказа получить объект клиента по его id) (это и так можно сделать, но суть ниже).
Я предполагаю что нужно создать отдельную папку с классами и переносить туда всю логику с сущностями (что бы максимально освободить контроллер оставив там только вызов методов). Но в моделях так же можно создавать методы и работать с ними, а можно создать отдельный класс который моделью являться не будет, он будет в роли прослойки между контроллером и моделью.
Вопрос вот в чём: где лучше описывать методы сущностей? Если делать прослойку о которой написал выше, то как грамотно называть классы, ведь имена классов, например, Clients уже заняты моделями и какие методы должны быть в этой прослойке, а какие в модели? Либо вообще как то по другому сделать?
P.S. Выбрал следующую тактику: сделал классы под названиями classnameService, в них вся бизнес-логика(никаких orm-конструкций), а в них вызываются методы модели которые взаимодействуют с бд. А сами эти классы вызываются из контроллеров
Дополнение для конкурса:
- Зачем нужны репозитории, если есть модели ?
- Почему в laravel нет сеттеров-геттеров в традиционном их понимании? (нет методов для возвращения полей, они доступны по
$object->field, IDE их к выбору не предлагает т.к. они protected, ещё и в массиве) - Как организовать цепочную схему, что бы, например, можно было писать вызовы так:
$object->getClient()->checkRegularCustomer()объединив в них методы моделей (для выборки данных) и сервисов (для бизнес логики)? в данный момент у меня есть модели и сервисы, модели отвечают за взаимодействие с бд (CRUD), а сервисы за бизнес-логику (получить товары по заказу, получить их остаток на складе, etc), но в цепочку можно вернуть только один объект. - Как, в общем случае, делится ответственность классов и уровней? пример: есть метод создания клиента. что бы это сделать нужно взять номер телефона и привести к виду "89998887766" удалив всё лишнее что может быть. Это не вопрос валидации, а вопрос нормализации, за это должен отвечать контроллер, сервис или модель?
Ответы (2 шт):
Очень хороший вопрос:
Как найти баланс между реализацией необходимой функциональности по обработке данных между контроллером и моделью?
Исходя из моего опыта разработки подобных систем (а у меня их было много) я могу дать несколько советов (чисто мое субъективное мнение):
- Добавлять лишние сущности-прослойки это значит усложнять понимание системы. Лучше этого не делать без особой необходимости. Необходимость в таких классах может появиться в случае появления дополнительных требований к системе по обработке данных, например, при необходимости вести какую-то синхронизацию с другими системами или логирование... Но это все равно очень сильно усложняет систему и нужно, по возможности, избегать создание новых сущностей.
- Обработку данных в модели нужно делать в случае, если эта обработка касается прямого доступа к полям и функциональности доступа к данным. Например, делаются дополнительные SQL запросы к базе при формировании ответа модели контроллеру. Основные критерии помещения функциональности в модель это: производительность, использование внутренней реализации модели и скрытие приватной реализации от контроллера.
- Обработку данных в контроллере нужно делать в случае обобщенной обработки нескольких моделей или создание дополнительной функциональности по контролю связанных данных в модели. Критерии помещения кода в контроллер: невозможность или сложность объединения нескольких сущностей в модели, дополнительная обработка данных не связанная напрямую с функциональностью модели и обобщение данных модели для отображения.
зачем нужны репозитории, если есть модели ?
Репозитории отвечают за взаимодействие с уровнем данных(бд в вашем случае). Они обеспечивают инкапсуляцию. Если вам вдруг нужно будет поменять хранилище, вы реализуете другой репозиторий с таким же интерфейсом, который будет ходить в другое хранилище. Это удобно. Ваша бизнес логика ничего не знает о том, где хранятся данные и это хорошо.
Модели - описывают поля одного объекта из коллекции. Ничего другого они делать не должны. Да в laravel легко делать запросы Order::query, но неверно делать их в модели. Это нарушение SRP. Операции с наборами данных из хранилища должны быть в репозиториях.
почему в laravel нет сеттеров-геттеров в традиционном их понимании?
Странный вопрос, возможно вы имели ввиду, что они не генерируются автоматический? Использование геттеров и сеттеров хорошая практика и я вам рекомендую работать с полями модели именно через них.
как организовать цепочную схему, что бы, например, можно было писать вызовы так: $object->getClient()->checkRegularCustomer()...
Не вижу проблемы. Давайте приведу пример:
class Order
{
private OrderModel $model;
private ClientRepository $clientRepository;
private ClientFactory $clientFactory;
public function __construct(
OrderModel $model,
ClientRepository $clientRepository,
ClientFactory $clientFactory
) {
$this->model = $model;
$this->clientRepository = $clientRepository;
$this->clientFactory = $clientFactory;
}
public function getClient(): Client
{
$clientModel = $this->clientRepository->getActiveClient($this->model->getId());
return $this->clientFactory->create($clientModel);
}
}
class Client
{
private ClientModel $model;
public function __construct(OrderModel $model)
{
$this->model = $model;
}
public function checkRegularCustomer(): void;
}
Классы Order и Client являются классами доменного уровня, то есть реализуют бизнес логику. При этом они не ходят сами в бд(это делает репозиторий). Модель имеет только геттеры и сеттеры и не содержит никаких запросов и бизнес логики.
как, в общем случае, делится ответственность классов и уровней? пример: есть метод создания клиента. что бы это сделать нужно взять номер телефона и привести к виду "89998887766" удалив всё лишнее что может быть. это не вопрос валидации, а вопрос нормализации, за это должен отвечать контроллер, сервис или модель?
Нет какого то универсального решения для деления, но у каждого класса должна быть своя ответственность.
В данном конкретном примере могу предложить такой вариант:
- Контроллер принимает запрос и передает управление валидатору.
- В случае успешной валидации, данные передаются сервису.
- Сервис сам не содержит бизнес логики, но знает как ее вызвать.
- Сервис передает управление классу PhoneNormalizer.
- После успешного выполнения PhoneNormalizer->normilize сервис передает результат работы в метод createClient(в котором через репозиторий происходит сохранение)
- После успешного выполнения метода createClient сервис возвращает результат контроллеру.
выбрал следующую тактику: сделал классы под названиями classnameService
Да, сервисы могут содержать бизнес логику, но я бы рекомендовал использовать для этого отдельные классы. Не модели, не репозитории, не контроллеры. Просто классы, в которых будет только бизнес логика.
Так же, рекомендовал бы вам делать сервисы исходя их бизнес сущностей, а не моделей.
Но в моделях так же можно создавать методы и работать с ними, а можно создать отдельный класс который моделью являться не будет, он будет в роли прослойки между контроллером и моделью.
Лучше создавать отдельный класс, который не будет прослойкой, а будет содержать только бизнес логику без лишних подробностей(не будет знать что такое http запрос, что такое база данных и т д). Частой проблемой является, то что по мере увеличения кодовой базы бизнес логика расползается по всему проекту - это плохо. "Если вы не найдете место бизнес логике, она найдет его сама".
где лучше описывать методы сущностей?
В отдельных классах если это бизнес логика. В модели только описание полей.
как грамотно называть классы, ведь имена классов, например, Clients уже заняты моделями и какие методы должны быть в этой прослойке, а какие в модели?
Они могут называться одинаково, но находится в разных неймспейсах:
App\Domain\Client
App\Service\Client
App\Model\Client
App\Repository\Client
Но я бы рекомендовал еще и использовать постфиксы:
App\Service\ClientService
App\Model\ClientModel
App\Repository\ClientRepository
все данные берутся с помощью ORM прямо "на месте"
Контроллеры - это уровень взаимодействия с пользователем. В них должно быть минимум логики и вообще не должно бизнес логики. В идеале, в методе контроллера, должен быть вызван метод сервиса и все. То есть контроллер знает только кому передать управление.
Итого советую вам:
- Не писать запросы в моделях, использовать репозитории.
- Бизнес логику описывать в отдельных классах, доступ к этим классам получать через сервисы.
- Держать контроллеры "тонкими".
Все эти рекомендации можно не выполнять и проблем не будет если у вас маленький проект. Но если выполнять, то при увеличении кодовой базы у вас не возникнет проблем. Основной плюс соблюдения принципа единой ответственности и разбиение все на классы - это проще тестировать и проще заменять. Если проект активно развивается - бизнес требования меняются часто. Надо делать так чтобы каждое такое изменение требовало минимум трудозатрат в будущем.