Есть ли альтернатива инъекции ублюдка? (Ака инъекция бедняка через конструктор по умолчанию)
я чаще всего испытываю соблазн использовать "ублюдочную инъекцию" в нескольких случаях. Когда у меня есть" правильный " конструктор инъекций зависимостей:
public class ThingMaker {
...
public ThingMaker(IThingSource source){
_source = source;
}
но тогда, для классов я намереваюсь как публичные API (классы, которые будут потреблять другие команды разработчиков), я никогда не могу найти лучшего варианта, чем написать конструктор по умолчанию "ублюдок" с наиболее вероятной необходимой зависимостью:
public ThingMaker() : this(new DefaultThingSource()) {}
...
}
очевидным недостатком здесь является то, что это создает статическое зависимость от DefaultThingSource; в идеале такой зависимости не было бы, и потребитель всегда вводил бы любой IThingSource, который они хотели. Однако это слишком сложно использовать; потребители хотят создать ThingMaker и приступить к работе, делая вещи, а затем через несколько месяцев вводить что-то еще, когда возникает необходимость. Это оставляет всего несколько вариантов на мой взгляд:
- опустите конструктор bastard; заставьте потребителя ThingMaker понять IThingSource, понять, как ThingMaker взаимодействует с IThingSource, находит или пишет конкретный класс, а затем вводит экземпляр в свой вызов конструктора.
- опустите конструктор bastard и предоставьте отдельную фабрику, контейнер или другой класс/метод начальной загрузки; каким-то образом заставьте потребителя понять, что им не нужно писать свой собственный IThingSource; заставьте потребителя ThingMaker найти и понять фабрику или загрузчик и использовать его.
- держите ублюдок конструктор, включение потребитель "создает" объект и работает с ним, а также справляется с необязательной статической зависимостью от DefaultThingSource.
мальчик, #3, конечно, кажется привлекательным. Есть ли другой, лучший вариант? #1 или #2 не стоит.
12 ответов:
насколько я понимаю, этот вопрос относится к тому, как выставить слабо связанный API с некоторыми соответствующими значениями по умолчанию. В этом случае у вас может получиться хороший Локальное Значение По Умолчанию в этом случае зависимость может рассматриваться как необязательные. Один из способов справиться с дополнительные зависимости использовать Инъекции Собственность вместо Конструктор Инъекций - на самом деле, это своего рода сценарий плаката для собственности Инъекция.
однако реальная опасность инъекции ублюдка заключается в том, что по умолчанию это Внешнее Значение По Умолчанию, потому что это означало бы, что конструктор по умолчанию тянет нежелательную связь к сборке, реализующей значение по умолчанию. Однако, как я понимаю этот вопрос, предполагаемый дефолт будет происходить в той же сборке, и в этом случае я не вижу никакой особой опасности.
в любом случае вы также можете рассмотреть фасад, как описано в одном из моих более ранние ответы:Dependency Inject (DI) "дружественная" библиотека
кстати, терминология, используемая здесь, основана на языке шаблонов от моей книги.
мой компромисс-это спин на @BrokenGlass:
1) единственный конструктор параметризованного конструктора
2) Используйте заводской метод для создания ThingMaker и передать в этом источнике по умолчанию.
public class ThingMaker { public ThingMaker(IThingSource source){ _source = source; } public static ThingMaker CreateDefault() { return new ThingMaker(new DefaultThingSource()); } }
очевидно, что это не устраняет вашу зависимость, но это делает его более ясным для меня, что этот объект имеет зависимости, которые вызывающий может глубоко погрузиться, если они хотят. Вы можете сделать этот заводской метод еще более явным, если хотите (CreateThingMakerWithDefaultThingsource), если это помогает с пониманием. Я предпочитаю это переопределению метода фабрики IThingSource, поскольку он продолжает поддерживать композицию. Вы также можете добавить новый метод фабрики, когда DefaultThingSource устарел и имеет четкий способ найти весь код с помощью DefaultThingSource и пометить его для обновления.
вы рассмотрели возможности в вашем вопросе. Фабричный класс в другом месте для удобства или некоторого удобства внутри класса себя. Единственный другой непривлекательный вариант будет основан на отражении, скрывая зависимость еще больше.
одна альтернатива-это метод фабрики
CreateThingSource()
в своемThingMaker
класс, который создает зависимость для вас.для тестирования или если вам нужен другой тип
IThingSource
затем вам нужно будет создать подклассThingMaker
и заменитьCreateThingSource()
чтобы вернуть конкретный тип, который вы хотите. Очевидно, что этот подход только стоит того, если вам в основном нужно иметь возможность вводить зависимость для тестирования, но для большинства / всех других целей не нужно другогоIThingSource
Если у вас должна быть зависимость" по умолчанию", также известная как инъекция зависимости бедного человека, то вам нужно инициализировать и" подключить " зависимость где-то.
Я сохраню два конструктора, но у меня есть фабрика только для инициализации.
public class ThingMaker { private IThingSource _source; public ThingMaker(IThingSource source) { _source = source; } public ThingMaker() : this(ThingFactory.Current.CreateThingSource()) { } }
теперь на заводе создайте экземпляр по умолчанию и разрешите переопределить метод:
public class ThingFactory { public virtual IThingSource CreateThingSource() { return new DefaultThingSource(); } }
обновление:
зачем использовать два конструкторы: Два конструктора ясно показывают, как класс предназначен для использования. Конструктор без параметров заявляет: просто создайте экземпляр, и класс выполнит все свои обязанности. Теперь второй конструктор утверждает, что класс зависит от IThingSource и предоставляет способ использования реализации, отличной от реализации по умолчанию.
почему с помощью фабрики: 1-дисциплина: создание новых экземпляров не должно быть частью обязанностей из этого класса более подходящим является фабричный класс. 2-DRY: представьте, что в том же API другие классы также зависят от IThingSource и делают то же самое. Переопределите один раз заводской метод, возвращающий IThingSource, и все классы в вашем API автоматически начнут использовать новый экземпляр.
Я не вижу проблемы в связывании ThingMaker с реализацией по умолчанию IThingSource, если эта реализация имеет смысл для API в целом, а также вы предоставляете способы переопределить это зависимость для целей тестирования и расширения.
вы недовольны нечистотой этой зависимости, но вы действительно не говорите, какие проблемы она в конечном итоге вызывает.
- использует ли ThingMaker DefaultThingSource каким-либо образом, который не соответствует IThingSource? Нет.
- может ли наступить время, когда вы будете вынуждены отказаться от конструктора без параметров? Поскольку вы можете предоставить реализацию по умолчанию в это время, маловероятно.
Я думаю, что самая большая проблема здесь-это выбор имени, а не использовать ли технику.
примеры, обычно связанные с этим стилем инъекции, часто чрезвычайно просты: "в конструкторе по умолчанию для класса
B
, вызовите перегруженный конструктор сnew A()
и быть на вашем пути!"реальность такова, что зависимости часто чрезвычайно сложны для построения. Например, что если
B
нужна неклассовая зависимость, такая как подключение к базе данных или настройка приложения? Вы тогда свяжите классB
доSystem.Configuration
пространство имен, увеличивая ее сложность и сцепление при снижении его когерентности, все для кодирования деталей, которые могут быть просто экстернализованы, опуская конструктор по умолчанию.этот тип впрыски связывает к читателю что вы признавали преимущества decoupled конструкции но не желаете поручить к ему. Мы все знаем, что когда кто-то видит этот сочный, легкий, низкофрикционный конструктор по умолчанию, они будут называть его независимо от того, насколько жестким он делает их программу с этого момента. Они не могут понять структура их программы без чтения исходного кода для этого конструктора по умолчанию, который не является опцией, когда вы просто распространяете сборки. Вы можете документировать соглашения имени строки подключения и ключа настроек приложения, но в этот момент код не стоит сам по себе, и вы возлагаете ответственность на разработчика, чтобы выследить правильное заклинание.
оптимизация кода, чтобы те, кто его пишет, могли пройти, не понимая, что они говорят, Это песня сирены, Ан анти-паттерн, который в конечном итоге приводит к большему количеству времени, потерянного на разгадывание магии, чем время, сэкономленное на первоначальных усилиях. Либо отделитесь, либо нет; удержание ноги в каждом узоре уменьшает фокус обоих.
для чего это стоит, весь стандартный код, который я видел в Java, делает это так:
public class ThingMaker { private IThingSource iThingSource; public ThingMaker() { iThingSource = createIThingSource(); } public virtual IThingSource createIThingSource() { return new DefaultThingSource(); } }
кто не хочет
DefaultThingSource
объект может переопределитьcreateIThingSource
. (Если это возможно, вызовcreateIThingSource
будет где-то еще, кроме конструктора.) C# не поощряет переопределение, как это делает Java, и это может быть не так очевидно, как это было бы в Java, что пользователи могут и, возможно, должны предоставить свою собственную реализацию IThingSource. (И не так очевидно как to предоставить ее.) Я предполагаю, что № 3-это путь, но я думал, что упомяну об этом.
просто идея-возможно, немного более элегантная, но, к сожалению, не избавляется от зависимости:
- удалить "ублюдок конструктор"
- в стандартном конструкторе вы делаете исходный параметр по умолчанию значение null
- затем вы проверяете, что источник равен нулю, и если это так, вы назначаете ему" new DefaultThingSource () " otherweise независимо от того, что потребитель вводит
есть внутренняя фабрика (внутренняя для вашей библиотеки), которая сопоставляет DefaultThingSource с IThingSource, который вызывается из конструктора по умолчанию.
Это позволяет вам "создать" класс ThingMaker без параметров или каких-либо знаний об IThingSource и без прямой зависимости от DefaultThingSource.
для действительно общедоступных API я обычно обрабатываю это, используя двухчастный подход:
- создайте помощника в API, чтобы позволить потребителю API регистрировать реализации интерфейса "по умолчанию" из API с их контейнером IoC по выбору.
- Если желательно разрешить потребителю API использовать API без собственного контейнера IoC, разместите дополнительный контейнер в API, который заполнен теми же реализациями" по умолчанию".
в действительно сложная часть здесь решает, когда активировать контейнер #2, и лучший подход к выбору будет сильно зависеть от ваших предполагаемых потребителей API.
Я поддерживаю вариант № 1, с одним расширением: сделать
DefaultThingSource
открытый класс. Ваша формулировка выше подразумевает, чтоDefaultThingSource
будут скрыты от публичных потребителей API, но, как я понимаю вашу ситуацию, нет причин не выставлять значение по умолчанию. Кроме того, вы можете легко документировать тот факт, что вне особых обстоятельств, anew DefaultThingSource()
всегда можно передатьThingMaker
.