Вопросы по реализации чистой архитектуры

Я впервые пытаюсь применить принципы чистой архитектуры в рабочем проекте и столкнулся с рядом вопросов. Суть задачи:

Есть внешний сервис и 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 и БД прямо в имплементации?

  1. Я вынес взаимодействие с базами данных в отдельные имплементации и забиндил их в DI-контейнер. Пример:
public function __construct(private readonly ClientsDBInterface $clientsDB)
{
}

Это корректный подход? Правильно ли в каждой имплементации использовать собственную реализацию для работы с БД?

  1. Логика в имплементациях (например, if, else, switch) допустима? Насколько я понимаю, вся бизнес-логика должна быть в Use Cases. Верно ли это?

  2. Какой слой отвечает за обработку данных в цикле? У меня есть задача: обрабатывать клиентов по кругу, проверяя их изменения. В какой слой лучше вынести логику, которая будет выполнять эту операцию?

  3. Структура таблицы клиентов. Для минимизации количества таблиц я добавил в таблицу клиентов следующие поля:

external_id — ID клиента из внешней системы. local_id — ID клиента в текущей системе. service — название сервиса, из которого пришел клиент.

Пример логики:

Получаю данные из AExternalClientsRepository. Проверяю, есть ли клиент в БД. Если клиента нет, создаю запись в БД с service = AExternal, а затем создаю его в CRM через ACrmClientsRepository, добавляя новую запись в БД с service = ACrm. Насколько такая структура данных и подход к реализации выглядят здраво?


Ответы (1 шт):

Автор решения: Alex Krass

Есть принципы проектирования ПО, есть в частности Принцип единой ответственности (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);
            }
        }
    }
}

Остальное зависит от требований и деталей, поэтому однозначно сказать что-то по структуре БД не могу.

→ Ссылка