Привет! Я разрабатываю приложения на Symfony и хочу поделиться проблемами, с которыми сталкивался при использовании Symfony DI, а также дать несколько советов которые, как мне кажется, будут полезны при разработки больших приложений. Кратко я упоминал о них в этой статье, и здесь хочу развернуть мысль и поговорить подробнее.

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

Autowire

Первое препятствие на пути к этой цели это autowire. Автовайринг полезен при быстром прототипировании, так как ускоряет написание di, но в долгой перспективе скорее вредит, приведу небольшие примеры:

services:
    _defaults:
        autowire: true
        
App\Service\Service: ~

App\Component\LockInterface:  
   class: App\Component\Lock 
final class Service {
	public function __construct(private readonly LockInterface $locker)  
	{  
	}
}

Реализация успешно подтягивается в класс сервиса. Дальше сервис растёт, растёт service.yaml, возможно разбивается на более мелкие yaml, появляются новые реализации LockInterface (иначе зачем мы создавали интерфейс). И со временем взглянув на класс при чтении кода становится довольно не просто найти, а какая конкретно реализация сейчас используется.
Ситуация значительно упрощается, если явно указать реализацию: добавив всего одну строчку di при написании, мы облегчаем себе её чтение в будущем.

App\Service\Service:
	arguments:  
	    $locker: '@App\Component\Lock'

Ещё одна опасность это возможные ошибки, так как изменения реализации могут происходить неявно:

App\Component\LockInterface:  
   class: App\Component\MyCustomLock 

Так мы одним махом меняем реализацию во всех местах использования, удобно на маленьком приложении, где мы держим в голове все места использования. Но в большой кодовой базе такое изменение может задеть места, о котором мы не подумали/забыли/не знали. Явное изменение строки di в используемом классе как минимум подсветит это. Да мы потратим больше времени, но зато изменения будет явными.
Не используйте автовайринг.

Классы вместо алиасов

В di лучше придерживаться одного стиля написания, это упрощает чтение и выбор собственно между двумя вариантами:

App\Component\Consumer:

app.component.consumer:
	class: App\Component\Consumer: ~

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

app.component.consumer.email_queue:
	class: App\Component\Consumer:
		arguments: 
			$queueName: 'mail'
			
app.component.consumer.sms_queue:
	class: App\Component\Consumer:
		arguments: 
			$queueName: 'sms'

Так не придётся добавлять новый дублирующий код или что страшнее использовать наследование. Использование обоих способов именования одновременно по необходимости вносит лишний шум в di и мешает чтению.
Используйте алиасы.

Интерфейс вместо реализации

Ещё один странный способ указания зависимостей в di, который я часто встречаю:

app.component.consumer:
	class: App\Component\Consumer:
		arguments: 
			$executor: '@App\Component\ExecutorInterface'

Есть явное указание в аргументах класса, но указание интерфейса. DI container - это про сборку проекта, то место, где мы указываем конкретные реализации интерфейсов, описанных в коде и указание здесь интрфейса просто заставляет разработчика дополнительно тратить время на чтение di в поисках реализации, а сама по себе такая строчка не несёт никакой полезной информации, то что $executor это интерфейс видно и так глядя на код.
Не делайте так.

Правила эти очень субъективные и возможно спорные, они заставляют более дисциплинированно и многословно подходить к написанию di, но по моему мнению упрощают его чтение, что приносит много пользы в долгосрочной перспективе.

Комментарии (4)


  1. bondeg
    01.08.2024 14:43
    +3

    Выглядит по большей части как выдуманные проблемы.

    1. Autowire - никто не запрещает использовать совместно с ним конфиги с явной передачей аргументов. При этом необходимость в таком относительно основной массы бизнес логики как правило ничтожна.

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


    1. Sanchous98
      01.08.2024 14:43

      Плюс выдуманная проблема с разными реализациями. Autowiring не работает с интерфейсами в том смысле, в котором автор пытается их использовать, то есть класс не зарегистрируется под интерфейсом. Это надо будет сделать вручную в любом случае


      1. karrakoliko
        01.08.2024 14:43

        если есть только одна реализация интерфейса - зарегистрируется.

        когда появится вторая - начинается неприятная возня с ручным разруливанием


  1. ToshaSieg
    01.08.2024 14:43

    • "иначе зачем мы создавали интерфейс" - для инверсии зависимостей и независимой разработки юнита.

    • "добавив всего одну строчку di..." - не нужно эту одну строчку добавлять, если она там не нужна.