Статьи

Почему @Inject — плохая идея

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

Одна из главных идей, которые у нас были до сих пор в проекте Qi4j, заключается в том, что если «что-нибудь может значить что-либо», то это то же самое, что «ничто не значит ничего». В частности, если все, что вам нужно, это POJO для реализации сущностей, ценностей и сервисов, то действительно очень трудно обеспечить применение любой значимой семантики. Вот почему мы специально разделяем понятия «Переходный составной», «Составной объект», «Составной объект» и «Составной сервис». Потому что они разные, и важно, что они разные. Отношение к ним по-разному, в отличие от «все одинаково», дает нам преимущества. Вы можете легко сопоставить это с проблемами с SOAP, где все «это просто POST для URL», который затем полностью удаляет все достоинства HTTP и URL.

Точно так же мы должны придать значение наличию аннотаций для инъекций, которые действительно что-то значат. Таким образом, вместо того, чтобы иметь что-то вроде общей аннотации @Inject, которая может означать что угодно и, следовательно, ничего, мы решили использовать аннотации, которые определяют область действия целевого объекта. Поэтому мы используем основные аннотации @Service, @Uses и @This. @Service означает, что служба должна быть введена. @Uses означает, что будет добавлен экземпляр в графе экземпляров (например, M в настройке MVC). @ Это означает, что я хочу получить ссылку на «себя», но приведу к определенному интерфейсу, что имеет смысл в Composite, состоящем из множества миксинов. Иногда внедрение интерфейса для определенного типа может быть разрешено как @Service (= «Я хочу внешний сервис»), так и @This (= «Я являюсь сервисом и хочу назвать этот интерфейс для себя «), поэтому различие между ними является существенным.

Все они имеют разные значения, и все они не только помогают инфраструктуре выполнять инъекции, но и делают код более легким для чтения. Вот пример:

@Inject
void init(Foo foo, Bar bar, Some some)
{
...
}

Какие из вышеперечисленных уколов относятся к услугам? Ты не знаешь? Ну, конечно, нет, потому что информации, которую нужно знать, там нет. «Это так гибко, потому что Foo, Bar и Some могут быть чем угодно!». И именно поэтому вы не можете сделать что-нибудь полезное с ними. Если это сервис, то у Foo не будет никакого состояния, и вы будете делиться им с множеством других экземпляров. Если Foo является внедренным новым экземпляром, то вы будете его владельцем и можете относиться к нему совсем иначе, чем к сервису. И так далее. Но поскольку этой информации нет, вы не можете точно знать, что вы можете с ней сделать.

В отличие от этого, как это будет выглядеть в Qi4j:

void init(@Service Foo foo, @Service Bar bar, @Uses Some some)
{
...
}

Теперь ясно, из какой области должен вводиться каждый параметр, что не только облегчает чтение кода (то есть аннотации являются документацией, а не только инструкциями фреймворка), но и в этом есть некоторая безопасность, так как нет способа Foo может быть случайно предоставлен экземпляром, а некоторые не могут быть предоставлены службой. Параметры имеют значение вне их типа: вы ЗНАЕТЕ, что от них можно ожидать!

Это главная причина, по которой я думаю, что общие аннотации типа @Inject — ужасная идея. Я бы не против иметь стандартную аннотацию, с помощью которой я могу аннотировать свои аннотации @Service, @Uses и @This. Это имело бы смысл, и IntelliJ было бы легче знать, что поле, которое я только что отметил @Service, будет введено, поэтому не показывайте предупреждение, если оно не инициализировано.

Тогда есть ряд деталей о предложении, которые также не совсем продуманы. Как видите, аннотации Qi4j применяются для каждого параметра, а не для метода. Это позволяет нам отмечать возможность для отдельных инъекций, а не для всего этого. Вот как вы должны сделать это с предложенными аннотациями @Inject:

 

@Inject(optional=true)
void init(Foo foo, Bar bar, Some some)
{
...
}

@Inject
void init(Foo foo, Bar bar)
{
...
}

Напротив, вот эквивалентный метод Qi4j:

 

void init(@Service Foo foo, @Service Bar bar, @Optional @Uses Some some)
{
...
}

Есть только один метод, и я могу пометить внедрение @Uses как необязательное. Если совпадение не найдено, оно просто будет установлено на ноль. То же самое касается конструкторов. Вышесказанное даже пригодится, если я вообще использую этот класс без DI, поскольку аннотации — это не только инструкции по внедрению, но и своего рода документация о том, как их использовать. Аннотация @Optional может использоваться как для обычных вызовов методов, так и для свойств. Это значительно упрощает реализацию и документирование нулевых параметров и свойств (sidenote: я думаю, что @Optional НАМНОГО лучше, чем предложение @Nullable в JSR-305).

Вот почему аннотация @Scope полностью обратная. Он установлен на объекте, который реализует тип, а не точку внедрения. Это делает его инструкцией для структуры, а не декларативным ограничением того, что может быть введено. Это в сочетании с общей аннотацией @Inject позволяет вам делать действительно странные вещи. Пример:

 

interface Log {
void log(String message);
void setThreshold(int threshold);
}
@Singleton
class LogService implements Log {
void log(String message) { ... }
void setThreshold(int threshold) { ... }
}

class LogImpl implements Log {
void log(String message) { ... }
void setThreshold(int threshold) { ... }
}

@Inject
void init(Log log) {
log.setThreshold(3);
}

Может кто-нибудь сказать мне результат установки порога? Это только для экземпляра Log, который я внедрил, или это глобально? Ты не знаешь? Это потому, что если «все может быть чем угодно», у вас нет возможности узнать! Вы зависите от того, кто бы это ни написал, и тот, кто это сделал, может не быть тем, кто написал точку инъекции, поэтому они могут делать разные предположения о том, что вводит «Журнал» и устанавливает для него порог. Так как нигде ничего не говорит об этом, все идет. Какой беспорядок! Если бы @Scope вместо этого находился в точке инъекции, это было бы кристально ясно как для внедряемого класса, так и для класса, обеспечивающего реализацию.

@Named одинаково проблематичен. Вы можете подумать, что это просто: если есть много экземпляров, которые реализуют интерфейс, просто выберите тот, который вы хотите, указав его имя. Это, однако, также обратное: точка внедрения сильно зависит от того, что должно быть введено, и нет способа изменить это без изменения кода. У вас также возникают проблемы, если есть две точки внедрения, которые объявляют имя внедренной вещи, но они используют два разных имени, даже если вы, как сборщик приложений, хотите, чтобы они указывали на один и тот же экземпляр. Что делать? Молитесь, чтобы ваш фреймворк позволял вам задавать два имени для одного экземпляра, и это единственный способ решить эту проблему.

Напротив, в Qi4j экземпляру действительно может быть задан набор имен, но на него редко следует ссылаться в коде. Вместо этого есть также возможность установить теги (обратите внимание на множественное число) в сервисе, что облегчает выполнение декларативных инъекций. Одна точка внедрения может сказать «Мне нужен Service Foo с тегом xyz», а другая точка ввода может сказать «Мне нужен Service Foo с тегом abc». С тегами это легко решается, так как вы просто добавляете xyz и abc в качестве тегов в сервис. Проблема решена! К счастью, эту проблему с предложением @Inject можно решить тривиально: просто переименуйте @Named в @Tagged и все готово!

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

@Inject Provider<Foo> foo;

foo.get () теперь можно использовать для отложенной загрузки экземпляра или создания новых экземпляров Foo. Но допустим, я ожидаю, что он будет использоваться для отложенной загрузки сервисов. Затем, если фреймворк во время инициализации обнаружит, что нет сервиса, реализующего Foo, я хочу прервать его. Т.е. вышесказанное близко к обычному сервисному внедрению, только с ленивой настройкой. Но фреймворк не может знать, что именно это и задумал разработчик, и поэтому он с радостью запустится, и только после foo.get () вы получите исключение.

Напротив, в Qi4j ссылка на службу отложенной загрузки будет выглядеть так:

@Service ServiceReference<Foo> foo;

Теперь ясно, что если нет доступных служб Foo, приложение не должно даже запускаться: оно должно генерировать исключение типа «необязательное внедрение службы не разрешено». Если бы я вместо этого хотел перечислить все службы, реализующие Foo, которые могут быть равны нулю, я бы вместо этого сделал:

@Service Iterable<Foo> foo;

Это позволяет мне вызвать foo.iterator () и пройтись по экземплярам службы, которые могут быть равны нулю. Если их нет, приложение все равно будет нормально запускаться. Это более ясно показывает намерение разработчика и то, что ожидается от внедрения, а не является общим «дай мне то, что может что-то дать», что, опять же, ничего не значит.

Среди перечисленных мною проблем есть общая тема: все они проистекают из того факта, что на аннотации сильно влияют потребности фреймворка, а не помогают описать то, что пытается сделать ваш код. Они являются императивными, а не декларативными. «Сделай это» против «Это то, что мне нужно». Это больше, чем просто техническая деталь: это фундаментальный принцип. И из того, что я вижу прямо сейчас, все это необходимо.

В заключение, в то время как Боб Ли утверждал в объявлении, что это будет не спорная спецификация, я бы скорее сказал, основываясь на вышеизложенном, что все это задом наперед и должно быть переосмыслено с нуля. Конечно, есть большая проблема с этим: существует много инвестиций в технологии, которые поддерживают это предложение, и поэтому переосмысление в соответствии с предложенным мною предложением вряд ли будет осуществимо. Я уважаю это, хотя я явно не согласен с технической стороной. Лучшее решение, которое я могу придумать сейчас, это просто отказаться от спецификации. Зачем стандартизировать то, что, как известно, является плохими идеями, даже если они очень часто используются? Опять же, люди склонны делать то, что они хотят делать, независимо от того, хорошая это идея или нет. Время покажет на этом.

С http://www.jroller.com/rickard