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

от автора

Рассмотрим на простом и наглядном примере реализацию 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, чтение по блокам и другое.


ссылка на оригинал статьи https://habr.com/ru/articles/774842/