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

16c28025b2e11d808981ca92fbe8a0ba

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

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

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

© Habrahabr.ru