Есть ли альтернатива инъекции ублюдка? (AKA инъекция бедного человека через конструктор по умолчанию) - PullRequest
115 голосов
/ 18 июля 2011

У меня чаще всего возникает соблазн использовать "инъекцию ублюдка" в некоторых случаях.Когда у меня есть «правильный» конструктор внедрения зависимостей:

public class ThingMaker {
    ...
    public ThingMaker(IThingSource source){
        _source = source;
    }

Но тогда для классов, которые я собираюсь использовать как публичные API (классы, которые будут использовать другие команды разработчиков), я могуникогда не находите лучший вариант, чем писать «ублюдочный» конструктор по умолчанию с наиболее вероятной необходимой зависимостью:

    public ThingMaker() : this(new DefaultThingSource()) {} 
    ...
}

Очевидным недостатком здесь является то, что это создает статическую зависимость от DefaultThingSource;в идеале такой зависимости не было бы, и потребитель всегда вводил бы любой IThingSource, который он хотел.Однако это слишком сложно для использования;потребители хотят создать ThingMaker и приступить к созданию вещей, а спустя несколько месяцев, когда возникнет такая необходимость, внедрить что-то еще.Это оставляет всего несколько вариантов на мой взгляд:

  1. Пропустить ублюдочный конструктор;заставить потребителя ThingMaker понять IThingSource, понять, как ThingMaker взаимодействует с IThingSource, найти или написать конкретный класс, а затем внедрить экземпляр в вызов их конструктора.
  2. Пропустите конструктор-ублюдок и предоставьте отдельную фабрику, контейнер или другой класс / метод начальной загрузки;каким-то образом заставить потребителя понять, что ему не нужно писать собственный IThingSource;вынудите потребителя ThingMaker найти и понять фабрику или загрузчик и использовать ее.
  3. Сохраните ублюдочный конструктор, позволяя потребителю «создавать» новый объект и запускать его, а также справляться с необязательной статической зависимостьюна DefaultThingSource.

Мальчик, # 3, конечно, кажется привлекательным.Есть ли другой, лучший вариант?Кажется, что № 1 или № 2 того не стоят.

Ответы [ 12 ]

60 голосов
/ 19 июля 2011

Насколько я понимаю, этот вопрос относится к тому, как выставить слабосвязанный API с некоторыми подходящими значениями по умолчанию. В этом случае у вас может быть хорошее Local Default , и в этом случае зависимость может рассматриваться как необязательная. Один из способов справиться с необязательными зависимостями - это использовать Внедрение свойства вместо Внедрение в конструктор - фактически это своего рода сценарий постера для Внедрения свойства.

Тем не менее, реальная опасность Bastard Injection заключается в том, что по умолчанию используется значение Foreign Default , поскольку это означает, что конструктор по умолчанию перетаскивает нежелательное соединение со сборкой, реализующей стандарт. Однако, как я понимаю этот вопрос, предполагаемый дефолт будет возникать в той же сборке, и в этом случае я не вижу особой опасности.

В любом случае вы могли бы также рассмотреть Фасад, как описано в одном из моих предыдущих ответов: Dependency Inject (DI) "дружественная" библиотека

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

32 голосов
/ 18 июля 2011

Мой компромисс - это вращение @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 и пометить его как обновляемый.

Вы рассмотрели возможности в своем вопросе.Фабричный класс в другом месте для удобства или некоторого удобства внутри самого класса.Единственный другой непривлекательный вариант будет основан на отражениях, скрывая зависимость еще дальше.

8 голосов
/ 18 июля 2011

Одна альтернатива - иметь фабричный метод CreateThingSource() в вашем ThingMaker классе, который создает зависимость для вас.

Для тестирования или если вам нужен другой тип IThingSource тогда вам нужно будет создать подкласс ThingMaker и переопределить CreateThingSource(), чтобы получить нужный конкретный тип.Очевидно, что этот подход того стоит, только если вам, в основном, нужно уметь вводить зависимость для тестирования, но для большинства / всех других целей не требуется другой IThingSource

.
6 голосов
/ 18 июля 2011

Если у вас должна быть зависимость «по умолчанию», также известная как «инъекция зависимостей бедного человека», то вам нужно где-то инициализировать и «привязать» зависимость.

Я оставлю два конструктора, но у меня будет фабрика только для инициализации.

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- СУХОЙ: представьте, что в том же API другие классы также зависят от IThingSource и делают то же самое.Переопределите, как только метод фабрики, возвращающий IThingSource, и все классы в вашем API автоматически начнут использовать новый экземпляр.

Я не вижу проблемы в подключении ThingMaker к реализации IThingSource по умолчанию, если эта реализация имеет смыслдля API в целом, а также вы предоставляете способы переопределить эту зависимость для целей тестирования и расширения.

6 голосов
/ 18 июля 2011

Я голосую за # 3.Вы сделаете свою жизнь - и жизнь других разработчиков - проще.

2 голосов
/ 20 июля 2011

Примеры, обычно относящиеся к этому стилю внедрения, часто бывают чрезвычайно упрощенными: «в конструкторе по умолчанию для класса B вызовите перегруженный конструктор с new A() и будьте в пути!»

Реальность такова, что зависимости часто чрезвычайно сложны для построения. Например, что если B нужна не классовая зависимость, такая как соединение с базой данных или настройка приложения? Затем вы связываете класс B с пространством имен System.Configuration, увеличивая его сложность и сцепление, одновременно снижая его когерентность, и все это для кодирования деталей, которые можно просто экстернализовать, опуская конструктор по умолчанию.

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

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

2 голосов
/ 18 июля 2011

Вы недовольны ОО-примесью этой зависимости, но вы на самом деле не говорите, какую проблему она в конечном итоге вызывает.

  • Использует ли ThingMaker DefaultThingSource каким-либо образом, который не соответствует IThingSource? Нет.
  • Может ли наступить момент, когда вы будете вынуждены покинуть конструктор без параметров? Поскольку в настоящее время вы можете предоставить реализацию по умолчанию, маловероятно.

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

1 голос
/ 18 июля 2011

Для чего бы это ни стоило, весь стандартный код, который я видел в 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. (Не так очевидно , как предоставить это.) Я думаю, что # 3 - это путь, но я подумал, что упомяну это.

0 голосов
/ 18 июля 2011

Я поддерживаю вариант # 1 с одним расширением: сделать DefaultThingSource общедоступным классом . Ваша формулировка выше подразумевает, что DefaultThingSource будет скрыт от публичных потребителей API, но, насколько я понимаю, в вашей ситуации нет причин не раскрывать настройки по умолчанию. Кроме того, вы можете легко задокументировать тот факт, что вне особых обстоятельств, new DefaultThingSource() всегда может быть передано в ThingMaker.

0 голосов
/ 18 июля 2011

Для действительно общедоступных API я обычно работаю с этим, используя двухэтапный подход:

  1. Создайте помощник в API, чтобы позволить потребителю API регистрировать реализации интерфейса "по умолчанию" из API сих контейнер IoC по выбору.
  2. Если желательно разрешить потребителю API использовать API без собственного контейнера IoC, разместите дополнительный контейнер в API, который заполнен теми же реализациями «по умолчанию».

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...