Есть ли альтернатива инъекции ублюдка? (Инъекция Ака бедных людей через конструктор по умолчанию)

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

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

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

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

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

  • Опустить конструктор ублюдка; заставить потребителя ThingMaker понимать IThingSource, понимать, как ThingMaker взаимодействует с IThingSource, находить или писать конкретный класс, а затем вставлять экземпляр в свой вызов конструктора.
  • Опустить конструктор bastard и предоставить отдельный factory, контейнер или другой класс/метод начальной загрузки; каким-то образом заставить потребителя понять, что им не нужно писать собственный IThingSource; заставить потребителя ThingMaker находить и понимать factory или загрузчик и использовать его.
  • Храните конструктор bastard, позволяя потребителю "обновлять" объект и работать с ним и справляться с дополнительной статической зависимостью от DefaultThingSource.

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

Ответ 1

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

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

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

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

Ответ 2

Мой компромисс - это вращение на @BrokenGlass:

1) Единственный конструктор - это параметризованный конструктор

2) Используйте метод factory для создания ThingMaker и передайте этот источник по умолчанию.

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

  public static ThingMaker CreateDefault() {
    return new ThingMaker(new DefaultThingSource());
  }
}

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

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

Ответ 3

Один из вариантов заключается в использовании factory method CreateThingSource() в вашем классе ThingMaker, который создает для вас зависимость.

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

Ответ 4

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

Ответ 5

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

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

public class ThingMaker
{
    private IThingSource _source;

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

    public ThingMaker() : this(ThingFactory.Current.CreateThingSource())
    {
    }
}

Теперь в factory создайте экземпляр по умолчанию и разрешите переопределить метод:

public class ThingFactory
{
    public virtual IThingSource CreateThingSource()
    {
        return new DefaultThingSource();
    }
}

Update:

Зачем использовать два конструктора: Два конструктора ясно показывают, как класс предназначен для использования. Конструктор без параметров утверждает: просто создайте экземпляр, и класс выполнит все его обязанности. Теперь второй конструктор утверждает, что класс зависит от IThingSource и предоставляет способ использования реализации, отличной от стандартной.

Зачем использовать factory: 1- Дисциплина: создание новых экземпляров не должно быть частью обязанностей этого класса, класс factory более уместен. 2- DRY: Представьте, что в том же API другие классы также зависят от IThingSource и делают то же самое. Переопределите, как только метод factory, возвращающий IThingSource, и все классы вашего API автоматически начнут использовать новый экземпляр.

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

Ответ 6

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

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

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

Ответ 7

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

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

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

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

Ответ 8

Для чего это стоит, весь стандартный код, который я видел в Java, выглядит так:

public class ThingMaker  {
    private IThingSource  iThingSource;

    public ThingMaker()  {
        iThingSource = createIThingSource();
    }
    public virtual IThingSource createIThingSource()  {
        return new DefaultThingSource();
    }
}

Любой, кто не хочет объект DefaultThingSource, может переопределить createIThingSource. (Если возможно, вызов createIThingSource был бы где-то иным, чем конструктором.) С# не поощряет переопределять, как это делает Java, и это может быть не так очевидно, как в Java, которое пользователи могут и, возможно, должны предоставить собственной реализации IThingSource. (Не так очевидно, как это сделать.) Я предполагаю, что №3 - это путь, но я подумал, что я бы сказал об этом.

Ответ 9

Просто идея - возможно, немного более элегантная, но, к сожалению, не избавляется от зависимости:

  • удалите "конструктор bastard"
  • в стандартном конструкторе вы делаете исходный параметр default равным null
  • тогда вы проверяете, является ли источник нулевым, и если это так, вы назначаете его "new DefaultThingSource()" otherweise независимо от того, что потребитель вводит

Ответ 10

У вас есть внутренний factory (внутренний для вашей библиотеки), который сопоставляет DefaultThingSource с IThingSource, который вызывается из конструктора по умолчанию.

Это позволяет вам "обновлять" класс ThingMaker без параметров или каких-либо знаний IThingSource и без прямой зависимости от DefaultThingSource.

Ответ 11

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

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

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

Ответ 12

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