Подтвердить что ты не робот

Марк Seemann противоречивые заявления о Bastard инъекции. Нужны некоторые разъяснения

Я читаю его книгу Инъекция зависимостей в сети.

1) Здесь он говорит, что Bastard Injection происходит только тогда, когда мы используем Foreign Default.

Но в его книге иллюстрация на стр. 148 показывает, что Bastard Injection возникает, когда реализация по умолчанию зависимостей - это Foreign Default или Local Default:

enter image description here

Таким образом, анти-шаблон Bastard Injection также возникает, когда по умолчанию реализация зависимостей - это Local Default?

2) Here (а также в его книге) он отмечает, что для класса достаточно иметь дополнительную зависимость, при условии, что стандартная реализация этого зависимость хорошая Local Default:

Но в следующем article он, похоже, возражает против наличия необязательных зависимостей вообще, даже если реализация по умолчанию - это Local Default:

private readonly ILog log;
public MyConsumer(ILog log)
{
    this.log = log ??LogManager.GetLogger("My");
}

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

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

3) Pg. 147:

Основная проблема с Bastard Injection заключается в том, что она использует FOREIGN DEFAULT..., мы больше не можем свободно повторно использовать класс, потому что он перетаскивает по зависимости, которую мы не можем хотеть. Также становится труднее сделать параллельное развитие, потому что класс сильно зависит от его ЗАВИСИМОСТЬ.

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

Он также подразумевает, что Foreign Default делает параллельную разработку более сложной, а Local Default - нет? Если это так, то это не имеет смысла, так как я бы предположил, что сложность параллельного развития затруднена, так это то, что сбор потребителя имеет жесткую ссылку на сборку зависимостей, а скорее тот факт, что потребительский класс зависит от конкретной реализации зависимость?

спасибо

4b9b3361

Ответ 1

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

Синтез

Когда я написал книгу, я в первую очередь попытался описать шаблоны и анти-шаблоны, которые я видел в дикой природе. Таким образом, шаблоны и анти-шаблоны в книге являются в первую очередь описательными и только в меньшей степени предписывающими. Очевидно, что разделение их на шаблоны и анти-шаблоны подразумевает определенную степень суждения:)

Существуют проблемы с Bastard Injection на нескольких уровнях:

  • Искажение пакетов
  • Герметизация
  • Простота использования

Самая опасная проблема связана с зависимостями пакета. Это концепция, которую я попытался сделать более действенным благодаря введению терминов Foreign Default по сравнению с Local Default. Проблема с Foreign Defaults заключается в том, что они перетаскивают жестко зависимые зависимости, которые делают (de/re) композицию невозможной. Хорошим ресурсом, который более конкретно занимается управлением пакетами, является Agile Principles, Patterns and Practices.

На уровне инкапсуляции код, подобный этому, трудно рассуждать:

private readonly ILog log;
public MyConsumer(ILog log)
{
    this.log = log ??LogManager.GetLogger("My");
}

Хотя он защищает инварианты класса, проблема в том, что в этом случае null является допустимым входным значением. Это не всегда так. В приведенном выше примере LogManager.GetLogger("My") может вводить только локальное значение по умолчанию. Из этого фрагмента кода мы не можем узнать, правда ли это, но ради аргумента, допустим, предположим это сейчас. Если по умолчанию ILog действительно является локальным значением по умолчанию, клиент MyConsumer может пройти null вместо ILog. Имейте в виду, что инкапсуляция заключается в том, чтобы облегчить клиенту использование объекта без понимания всех деталей реализации. Это означает, что это все, что видит клиент:

public MyConsumer(ILog log)

В С# (и аналогичных языках) можно передать null вместо ILog, и он собирается скомпилировать:

var mc = new MyConsumer(null);

С приведенной выше реализацией эта компиляция не только компилируется, но также работает во время выполнения. Согласно Postel law, это хорошо, верно?

К сожалению, это не так.

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

private readonly IRepository repository;
public MyOtherConsumer(IRepository repository)
{
    if (repository == null)
        throw new ArgumentNullException("repository");

    this.repository = repository;
}

В соответствии с инкапсуляцией клиент видит это только:

public MyOtherConsumer(IRepository repository)

Основываясь на предыдущем опыте, программист может склоняться к написанию кода следующим образом:

var moc = new MyOtherConsumer(null);

Это все еще компилируется, но не выполняется во время выполнения!

Как вы различаете эти два конструктора?

public MyConsumer(ILog log)
public MyOtherConsumer(IRepository repository)

Вы не можете, но в настоящее время у вас есть непоследовательное поведение: в одном случае null является допустимым аргументом, но в другом случае null приведет к исключению во время выполнения. Это уменьшит доверие, которое будет иметь каждый клиентский программист в API. Быть последовательным является лучшим способом продвижения вперед.

Чтобы упростить использование класса MyConsumer , вы должны оставаться постоянным. Именно по этой причине принятие null - плохая идея. Лучше использовать цепочку конструкторов:

private readonly ILog log;

public MyConsumer() : this(LogManager.GetLogger("My")) {}

public MyConsumer(ILog log)
{
    if (log == null)
        throw new ArgumentNullException("log");

    this.log = log;
}

Теперь клиент видит это:

public MyConsumer()
public MyConsumer(ILog log)

Это согласуется с MyOtherConsumer, потому что если вы попытаетесь передать null вместо ILog, вы получите ошибку времени выполнения.

Пока это технически все еще Bastard Injection, Я могу жить с этим дизайном для локальных значений по умолчанию; на самом деле, я иногда проектирую API как это, потому что это хорошо известная идиома на многих языках.

Для многих целей это достаточно хорошо, но по-прежнему нарушает важный принцип дизайна:

Явный лучше, чем неявный

В то время как цепочка конструкторов позволяет клиенту использовать MyConsumer со значением по умолчанию ILog, нет простого способа выяснить, каков будет экземпляр по умолчанию ILog. Иногда это тоже важно.

Кроме того, наличие конструктора по умолчанию предоставляет риск того, что кусок кода будет ссылаться на этот конструктор по умолчанию за пределами Composition Root. Если это произойдет, вы преждевременно связаны с объектами друг с другом, и как только вы это сделаете, вы не сможете отделить их от корня композиции.

Таким образом, меньший риск связан с использованием простой инсталляции конструктора:

private readonly ILog log;

public MyConsumer(ILog log)
{
    if (log == null)
        throw new ArgumentNullException("log");

    this.log = log;
}

Вы все еще можете составить MyConsumer с регистратором по умолчанию:

var mc = new MyConsumer(LogManager.GetLogger("My"));

Если вы хотите, чтобы локальное значение более доступно для обнаружения, вы можете найти его как Factory где-нибудь, например. в самом классе MyConsumer:

public static ILog CreateDefaultLog()
{
    return LogManager.GetLogger("My");
}

Все это создает предпосылки для ответа на конкретные вопросы в этом вопросе.

1. Существует ли анти-шаблон Bastard Injection, когда стандартная реализация зависимостей является локальным значением по умолчанию?

Да, технически, это так, но последствия менее суровые. Bastard Injection - это, прежде всего, описание, которое позволит вам легко идентифицировать его, когда вы его встретите.

Обратите внимание, что приведенная выше иллюстрация из книги описывает, как реорганизовать от Bastard Injection; а не как его идентифицировать.

2. [Следует ли избегать необязательных зависимостей с локальными значениями по умолчанию [...]?

С точки зрения зависимости от пакета вам не нужно их избегать; они относительно мягкие.

С точки зрения использования, я все же стараюсь избегать их, но это зависит от того, что я создаю.

  • Если я создам повторно используемую библиотеку (например, проект OSS), которую будут использовать многие люди, я все равно могу выбрать Constructor Chaining, чтобы упростить работу с API.
  • Если я создаю класс только для использования внутри определенной базы кода, я стараюсь полностью избегать необязательных зависимостей и вместо этого сочинять всю экспликацию в корне композиции.

3. Является ли он также подразумевающим, что Foreign Default делает параллельную разработку более сложной, а Local Default - не?

Нет, не знаю. Если у вас есть значение по умолчанию, перед тем, как вы сможете использовать, должно быть установлено значение по умолчанию; это не имеет значения, является ли оно местным или иностранным.