Как стать автором
Обновить

Реализация SOLID на примере

Уровень сложности Сложный
Время на прочтение 3 мин
Количество просмотров 7.9K

Рассмотрим на простом и наглядном примере реализацию SOLID на Symfony. Будет так же ссылка на Github.

Допустим, нужно реализовать импорт товаров из внешнего сервиса. Получится примерно такой код (на основе документации Doctrine):

namespace App\Service\Product\Import;

use App\Entity\Product\Product;
use App\Repository\Product\ProductRepository;
use Doctrine\ORM\EntityManagerInterface;

class ImportService
{
    private const SOURCE_PATH = 'http://somedomain.com/products/';

    public function __construct(
        private EntityManagerInterface $em,
        private ProductRepository $productRepository
    )
    {
    }

    public function import(): void
    {
        $em = $this->em;
        $productsData = json_decode(file_get_contents(self::SOURCE_PATH), true);
        $i = 0;
        foreach ($productsData as $productData) {
            $product = $this->productRepository->findOneBy(['sku' => $productData['sku']]);
            if (!$product) {
                $product = new Product();
            }

            $product->setSku($productData['sku']);
            $product->setName($productData['name']);
            //...set other fields

            $em->persist($product);

            $i++;
            if ($i % 100 == 0) {
                $em->flush();
                $em->clear();
            }
        }

        $em->flush();
        $em->clear();
    }
}

И обычно этого достаточно. Но пойдём дальше, сделаем вторую версию реализации. Попробуем отделить логику импорта от остальной логики:

namespace App\Utils\Importer;

use Doctrine\ORM\EntityManagerInterface;

class Importer
{
    public function __construct(
        private EntityManagerInterface $em,
    )
    {
    }

    public function import(
        ImportableRepositoryInterface $importableRepository,
        ImportableFactoryInterface $importableFactory,
        ImportMapperInterface $importMapper,
        ImportReceiverInterface $importReceiver,
        string $identityFieldName,
        int $blockSize = 100
    ): void
    {
        $em = $this->em;
        $importData = $importReceiver->receive();
        $i = 0;
        foreach ($importData as $importItemData) {
            $identityFieldValue = $importItemData[$identityFieldName];
            $importable = $importableRepository->findOneByImportIdentity($identityFieldValue);
            if (!$importable) {
                $importable = $importableFactory->create();
            }

            $importMapper->map($importable, $importItemData);
            $em->persist($importable);

            $i++;
            if ($i % $blockSize == 0) {
                $em->flush();
                $em->clear();
            }
        }

        $em->flush();
        $em->clear();
    }
}

Теперь вместо самого товара имеем ImportableInterface. Так же для получения данных извне имеем ImportReceiverInterface, для маппинга этих данных на сущность ImportMapperInterface, и интерфейс для создания сущности, фабрику. Но общая логика осталась такая же.

Это инверсия зависимости по отношению к логике импорта. Если рассматривать её как отдельный модуль, то получается все верно, это и есть предметная область для этого модуля. Но в контексте нашего приложения это менее важная деталь. Поэтому нужна ещё одна инверсия зависимости.

Это сервис, содержащий логику импорта товара, предметную область:

namespace App\Service\Product\ImportV2;

class ImportService
{
    public function __construct(
       private ImporterInterface $importer
    )
    {
    }

    public function import(): void
    {
        $this->importer->import();
        //...do other things
    }
}

А вот уже конкретика:

namespace App\Service\Product\ImportV2\Importer;

use App\Entity\Product\Product;
use App\Utils\Importer\Importer as BaseImporter;
use App\Service\Product\ImportV2\ImporterInterface;
use App\Service\Product\EntityFactory\ProductFactory;

class Importer implements ImporterInterface
{
    public function __construct(
        private BaseImporter $importer,
        private Mapper $mapper,
        private Receiver $receiver,
        private ImportableProductRepository $productRepository,
        private ProductFactory $productFactory
    )
    {
    }

    public function import(): void
    {
        $this->importer->import($this->productRepository, $this->productFactory, $this->mapper,
            $this->receiver, Product::IMPORT_IDENTITY_FIELD, 200);
    }
}

Код остальных классов можно посмотреть в исходном коде на гитхаб. Как можно заметить, так же применился и принцип единой ответственности, принцип открытости-закрытости, принцип разделения интерфейсов, в общем, весь SOLID.

Конечно, это только пример, но в теории, такой компонент импорта может дальше быть развит и использоваться в разных проектах. Можно, например, внедрить в него использование Symfony Serializer, получение данных через REST API, чтение по блокам и другое.

Теги:
Хабы:
Всего голосов 19: ↑1 и ↓18 -17
Комментарии 24
Комментарии Комментарии 24

Публикации

Истории

Работа

PHP программист
155 вакансий