Рассмотрим на простом и наглядном примере реализацию 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, чтение по блокам и другое.
Комментарии (24)
Farongy
18.11.2023 15:09Так а где изменения направления зависимостей? Т.е. где появляется инверсия?
Я вижу что в первом примере сервис зависит от репозитория и в последнем примере сервис зависит от репозитория.maxkain Автор
18.11.2023 15:09ImportService зависит только от ImporterInterface. А за этим интерфейсом уже детали реализации.
Farongy
18.11.2023 15:09+2Т.е. для вас инверсия это добавление слоя абстракции?
maxkain Автор
18.11.2023 15:09А как ещё? В ImportService можно дописывать логику, которую относим к предметной области. В реализации, в Mapper, например, можно вызвать другие сервисы, относящиеся к предметной области. В общем, ещё много всего можно доделывать.
Farongy
18.11.2023 15:09Слово инверсия значит - наоборот. Т.е. было влево, стало вправо. Было вверх стало вниз.
Инверсия зависимостей - значит зависимости должны как-то повернуться. Собственно и вопрос как они повернулись? От добавления ещё 3-х слоёв абстракций направление не меняется.
lair
18.11.2023 15:09Инверсия зависимостей - значит зависимости должны как-то повернуться.
Применительно конкретно к зависимостям понятие инверсия (точнее Dependency Inversion) означает только и исключительно то, что реализации зависят от абстракций, а не от других реализаций. Если вам так хочется смотреть на это как на поворот, то реализация-реализация - это "вверх", а реализация-абстрация - "вниз".
Glembus
18.11.2023 15:09А для Вас нет? Инверсия как раз и достигается путем использования абстракции для инъекции зависимости. Тоесть эта абстракция имеет инверсию а не ижект в сервис.
Farongy
18.11.2023 15:09Инверсия с инъекцией вообще никак не связана. Вот инъекция да, помогает делать инверсию, но можно и без неё. А можно использовать инъекцию, но без инверсии)
Glembus
18.11.2023 15:09Почитайте определение буквы D в акрониме SOLID. В PHP абстракция это интерфейс, мог конечно завязаться на каком абстрактном классе, но тогда это менее гибкая реализация. Что не так автор написал/имплементил?
zubrbonasus
18.11.2023 15:09-1Принцип единой ответственности говорит что объект должен использовать один актор. Объект импортёр может использовать актор склад, актор логист, актор торговый зал. Как вы реализовывали SRP?
Зачем получать данные в цикле? Может стоить получать данные одним запросом?
maxkain Автор
18.11.2023 15:09-2Первые попавшиеся определения SRP:
обозначает, что каждый объект должен иметь одну обязанность и эта обязанность должна быть полностью инкапсулирована в класс
гласит, что у класса должна быть только одна причина для изменения.
Здесь, в коде, отдельно Mapper, Receiver и т.д.
Зачем получать данные в цикле? Может стоить получать данные одним запросом?
Что-то вы путаете. Здесь данные получаем одним запросом к api, а обрабатываем в цикле.
crackidocky
Есть впечатление, что сложность туториала выставлена в "Сложный" не из-за сложности материала, а из-за того как вы решили отформатировать код????
А если без шуток, то это туториалом нельзя назвать, это скорее заметка, которую вы из Notion/Obsidian/
<вставьте ваше приложение>
выцепили и решили выложить.В чем проблема сразу дать ссылку на Github?
Пожалуйста, используйте Enter для разделения строк кода, не нужно все делать в одну строку. Код под этим текстом выглядит нечитабельно.
Зачем идти дальше? Объясните читателю что не так с первой версией реализации
Если вы делаете сравнение прошлой и следующей реализации, то такие заметки лучше оставлять комментариями в коде, для того чтобы любой желающий смог разобраться. То что вы под большим куском кода (кстати, тоже без форматирования) написали один комментарий - не значит что вы объяснили работу своего кода.
maxkain Автор
Неужели не понятно, что не получилось сразу нормально отформатировать текст в редакторе хабра. Сложность выставлена в соответствии с рекомендациями Хабра. Сразу ссылка на гитхаб? Зачем?
Статья про реализацию SOLID. То есть, первая версия не соответствует SOLID. Неужели это не понятно?
lair
Человеку, который не знает про SOLID, не понятно. А человеку, который знает и понимает, не нужна ваша статья.
maxkain Автор
Уж вы то всё знаете, кому что нужно. Статья про реализацию. Про теорию есть другие статьи. Многие знают про SOLID, но понять и применить его довольно сложно, и я думаю, эта статья многим может быть полезна. Здесь один из вариантов реализации, а разных вариантов может быть много.
lair
Ну вот а я так не думаю. Ваша статья не объясняет, зачем делаются изменения, и почему они именно такие. Очередной карго-культ "отделяй и вбрасывай".
Про SOLID действительно мало хороших статей, потому что очень мало кто удосуживается объяснить, в чем реальная смысл происходящего.
maxkain Автор
Да много где разъяснено. И в этой статье, указано, что мы получили класс, куда можно выделить логику приложения, бизнес-логику. Получили отдельный компонент импорта, который можно повторно использовать в пределах проекта или во многих проектах. Принцип DRY (Don't repeat yourself). Избегаем дублирование логики, что облегчает поддержку проекта. Отделяем основную логику от вторичной.
Простой пример, Security компонент Symfony. Вы берёте его и переиспользуете, прописываете конфиги, кастомизируете по необходимости, и не нужно в каждом проетке с нуля писать логику связанную с авторизацией.
Но есть, конечно, и минус в том, что такой компонент нужно поддерживать, это время, ресурс. Ну и потребуется изучать этот компонент перед использованием. Поэтому нужно взвешивать плюсы и минусы, и думать в какой мере применять те или иные практики в конкретном случае.
Писать компонент импорта, поддерживать его, мне кажется нецелесообразным в большинстве случаев. Но он взят просто как наглядный пример.
lair
А зачем он нужен?
А точно можно? Вы требования на него проанализировали? У него нет неявных ограничений, которые этому помешают?
А самое главное - это нужно?
А еще есть YAGNI. И вот ему вы противоречите.
Если в результате вашего примера получилось что-то, что делать нецелесообразно - это не наглядный пример. И именно в этом проблема вашей статьи, как и многих других статей на эту же тему.
maxkain Автор
В каких-то случаях это может быть целесообразным. Как наглядный пример связан с целесообразностью?
Зачем выделять предметную область? Об этом есть множество книг и статей. Похоже вы не в теме, и вам следовало бы ознакомиться с материалом. Если вам это не нужно, пройдите мимо, и не выносите мозг людям, пожалуйста. На остальное даже не хочу отвечать.
lair
Напрямую. Если из примера непонятно, зачем это делать - зачем этот пример нужен?
Тогда зачем нужна ваша статья?
...с каким, например?
Вы же считаете возможным писать публичные статьи? Ну а я считаю возможных их комментировать. Каждый в своем праве.