Вопросы по реализации чистой архитектуры
Я впервые пытаюсь применить принципы чистой архитектуры в рабочем проекте и столкнулся с рядом вопросов. Суть задачи:
Есть внешний сервис и CRM. Из внешнего сервиса могут поступать клиенты, которых нужно перенести в CRM. Если клиент уже существует в CRM, необходимо проверить, когда он последний раз изменялся во внешнем сервисе. Если время изменения отличается от данных в базе, нужно обновить информацию в CRM, используя ID клиента.
Мои вопросы по реализации:
Где правильно выполнять запросы к CRM, БД и другим внешним системам? Например, у меня есть имплементация репозитория CRM под названием ACrmClientsRepository(A - условное название CRM), которая выглядит так:
class ACrmClientsRepository implements ClientsRepositoryInterface
{
public function __construct(private readonly ClientsDBInterface $clientsDB)
{
}
public function create(CreateClientDTO $createClientDTO)
{
$createdRecord = ACrmApi::getInstance()->api()->records()->createRecord('Clients', [
// ДАННЫЕ
]);
$this->clientsDB->create([
// ДАННЫЕ НА СОЗДАНИЕ В БД
]);
// ПЕРЕДАЮ ДАННЫЕ ДАЛЕЕ
}
}
Аналогичную структуру я сделал для работы с внешним сервисом. Насколько такой подход соответствует принципам чистой архитектуры? Можно ли делать запросы к CRM и БД прямо в имплементации?
- Я вынес взаимодействие с базами данных в отдельные имплементации и забиндил их в DI-контейнер. Пример:
public function __construct(private readonly ClientsDBInterface $clientsDB)
{
}
Это корректный подход? Правильно ли в каждой имплементации использовать собственную реализацию для работы с БД?
Логика в имплементациях (например, if, else, switch) допустима? Насколько я понимаю, вся бизнес-логика должна быть в Use Cases. Верно ли это?
Какой слой отвечает за обработку данных в цикле? У меня есть задача: обрабатывать клиентов по кругу, проверяя их изменения. В какой слой лучше вынести логику, которая будет выполнять эту операцию?
Структура таблицы клиентов. Для минимизации количества таблиц я добавил в таблицу клиентов следующие поля:
external_id — ID клиента из внешней системы. local_id — ID клиента в текущей системе. service — название сервиса, из которого пришел клиент.
Пример логики:
Получаю данные из AExternalClientsRepository. Проверяю, есть ли клиент в БД. Если клиента нет, создаю запись в БД с service = AExternal, а затем создаю его в CRM через ACrmClientsRepository, добавляя новую запись в БД с service = ACrm. Насколько такая структура данных и подход к реализации выглядят здраво?
Ответы (1 шт):
Есть принципы проектирования ПО, есть в частности Принцип единой ответственности (SRP). Это означает, что компонент должен заниматься и быть ограничен только одной задачей. Если задача репозитория доставать данные клиента из конкретной базы, то только этим он и должен заниматься. Если его задача обращаться к внешнему сервису, то только этим он и должен заниматься. Если стоит задача переноса данных клиентов из одной системы в другую, за это должен отвечать отдельный компонент и называться не репозиторием (task, job, manager, service, etc. в зависимости от вашей структуры).
По сути репозиторий должен заниматься только необходимыми CRUD-операциями и их вариациями/расширениями, поскольку это чистый слой работы с базой данных. Если я правильно понял логику, то вы этот принцип нарушаете в вашем репозитории, выдав ему права на чтении данных из другой ответственности (обращение к другому репозиторию).
Поскольку язык не указан, использую близкий псевдокод. Ваши репозитории могли бы выглядеть так:
CrmClientRepositoryInterface {
getList;
getById;
getBy...;
ifExistsBy...;
update;
insert;
delete;
}
ExternalClientRepositoryInterface {
getList;
getById;
getBy...;
}
ClientRepositoryInterface {
getList;
getById;
getBy...;
ifExistsBy...;
update;
insert;
delete;
}
За перенос должен отвечать отдельный компонент со своей логикой, который уже может выглядеть следующим образом:
TransferClientManager implements TransferClientManagerInterface {
CrmClientRepositoryInterface crmRepository; //из DI контейнера
ExternalClientRepositoryInterface externalRepository; //из DI контейнера
ClientRepositoryInterface clientRepository; //из DI контейнера
public function transferClient() {
$list = externalRepository.getList();
for($client in $list) {
$isExists = crmRepository.ifExistsBy($client.field, ...)
if ($isExists) {
$existedClient = clientRepository.getBy($client.field, ...)
... доп. проверки $client и $existedClient ...
crmRepository.update($client);
clientRepository.update($client);
} else {
crmRepository.insert($client);
clientRepository.insert($client);
}
}
}
}
Остальное зависит от требований и деталей, поэтому однозначно сказать что-то по структуре БД не могу.