Ответ 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 - не?
Нет, не знаю. Если у вас есть значение по умолчанию, перед тем, как вы сможете использовать, должно быть установлено значение по умолчанию; это не имеет значения, является ли оно местным или иностранным.