Использование кинжала для инъекций зависимостей на конструкторах
Итак, я в настоящее время перепроектировал приложение для Android для использования Dagger. Мое приложение большое и сложное, и я недавно натолкнулся на следующий сценарий:
Для объекта A требуется специальный экземпляр DebugLogger, который является идеальным кандидатом для инъекций. Вместо того, чтобы проходить вокруг регистратора, я могу просто ввести его через конструктор. Это выглядит примерно так:
class A
{
private DebugLogger logger;
@Inject
public A(DebugLogger logger)
{
this.logger = logger;
}
// Additional methods of A follow, etc.
}
До сих пор это имеет смысл. Тем не менее, A необходимо построить другим классом B. Множество экземпляров A должно быть построено, поэтому, следуя кинжалу, я просто вставляю a Provider<A>
в B:
class B
{
private Provider<A> aFactory;
@Inject
public B(Provider<A> aFactory)
{
this.aFactory = aFactory;
}
}
Хорошо, хорошо. Но подождите, неожиданно A нуждается в дополнительных входах, таких как целое число, называемое "сумма", которое имеет жизненно важное значение для его построения. Теперь мой конструктор для A должен выглядеть так:
@Inject
public A(DebugLogger logger, int amount)
{
...
}
Внезапно этот новый параметр мешает инъекции. Более того, даже если это действительно сработало, мне не удастся передать "сумму" при получении нового экземпляра от провайдера, если я не ошибаюсь. Там я могу сделать несколько вещей, и мой вопрос в том, какой из них лучше?
Я мог бы реорганизовать A, добавив метод setAmount()
, который, как ожидается, будет вызываться после конструктора. Это уродливо, однако, потому что это заставляет меня отложить построение А до тех пор, пока не будет заполнено "количество". Если бы у меня было два таких параметра: "сумма" и "частота", тогда у меня было бы два сеттера, что означало бы либо сложную проверку, чтобы гарантировать, что построение резюме возобновляется после вызова обоих сеттеров, или мне нужно будет добавить еще третий метод в микс, например:
(Somewhere in B):
A inst = aFactory.get();
inst.setAmount(5);
inst.setFrequency(7);
inst.doConstructionThatRequiresAmountAndFrequency();
Другой альтернативой является то, что я не использую инъекцию на основе конструктора и не использую инъекцию на основе полей. Но теперь я должен сделать свои поля общедоступными. Это не очень хорошо со мной, потому что теперь я обязан раскрыть внутренние данные моих классов другим классам.
До сих пор единственным элегантным решением, которое я могу придумать, является использование полевой инъекции для поставщиков, например:
class A
{
@Inject
public Provider<DebugLogger> loggerProvider;
private DebugLogger logger;
public A(int amount, int frequency)
{
logger = loggerProvider.get();
// Do fancy things with amount and frequency here
...
}
}
Я все еще не уверен в сроках, так как я не уверен, что Кинжал будет вводить провайдера перед вызовом моего конструктора.
Есть ли лучший способ? Я просто что-то пропустил о том, как работает Кинжал?
Ответы
Ответ 1
То, о чем вы говорите, называется вспомогательной инъекцией и в настоящее время не поддерживается кинжалом любым способом.
Вы можете обойти это с помощью шаблона factory:
class AFactory {
@Inject DebugLogger debuggLogger;
public A create(int amount, int frequency) {
return new A(debuggLogger, amount);
}
}
Теперь вы можете ввести этот factory и использовать его для создания экземпляров A
:
class B {
@Inject AFactory aFactory;
//...
}
и когда вам нужно создать A
с вашей "суммой" и "частотой", вы используете factory.
A a = aFactory.create(amount, frequency);
Это позволяет A
иметь final
экземпляры полей журнала, количества и частоты, все еще используя инъекцию, чтобы предоставить экземпляр журнала.
У Guice есть вспомогательный плагин для инъекций, который, по сути, автоматизирует создание этих фабрик для вас. Там были обсуждены в списке рассылки Dagger о том, как их можно добавить, но с момента написания этой статьи ничего не было принято.
Ответ 2
То, что пишет Джейк, совершенно верно. Тем не менее, мы (некоторые из людей Google, которые работают с Guice and Dagger) работают над альтернативной версией "вспомогательной инъекции" или автоматической генерации factory, которую следует использовать Guice или Dagger или автономно - это, он будет генерировать исходный код класса factory для вас. Эти классы factory будут (при необходимости) инъектированы, как и любой стандартный класс JSR-330. Но он еще не выпущен.
В ожидании такого решения целесообразно использовать подход Джейка Уортона.
Ответ 3
У вас возникла проблема, потому что вы смешиваете инъекционные и неинъекционные материалы в своем конструкторе. Общие правила инъекции, которые избавят вас от сердечной боли и сохранят ваш код:
-
Инъекции могут запрашивать другие инъекции в своем конструкторе, но не для новых.
-
Newables может запрашивать другие новинки в своем конструкторе, но не для инъекций.
Инъекции - это объекты типа обслуживания, то есть объекты, которые работают, такие как CreditCardProcessor, MusicPlayer и т.д.
Newables - это объекты типа значения, такие как CreditCard, Song и т.д.
Ответ 4
Сообщение Джейка замечательно, но есть более простой способ. Google создал AutoFactory для создания factory автоматически во время компиляции.
Сначала создайте класс A
с аннотацией @AutoFactory
и аннотацией @Provided
для ввода аргументов:
@AutoFactory
public class A {
private DebugLogger logger;
public A(@Provided DebugLogger logger, int amount, int frequency) {
this.logger = logger;
}
}
Затем библиотека создает класс AFactory
во время компиляции. Поэтому вам нужно просто вставить factory в конструктор класса B
.
public class B {
private final AFactory aFactory;
@Inject
public B(AFactory aFactory) {
this.aFactory = aFactory;
}
public A createA(int amount, int frequency) {
return aFactory.create(amount, frequency);
}
}