I\'m reading his book Dependency Injection in Net.
1) Here he\'s saying that Bastard Injection
occurs only when we use Foreign Default
.
Since there are many questions here, I'll first attempt to provide an synthesis on my view on the subject, and then answer each question explicitly based on this material.
Synthesis
When I wrote the book, I first and foremost attempted to describe the patterns and anti-patterns I'd witnessed in the wild. Thus, the patterns and anti-patterns in the book are first and foremost descriptive, and only to a lesser degree prescriptive. Obviously, dividing them into patterns and anti-patterns imply a certain degree of judgement :)
There are problems with Bastard Injection on multiple levels:
The most dangerous problem is related to package dependencies. This is the concept I've attempted to make more actionable by the introduction of the terms Foreign Default versus Local Default. The problem with Foreign Defaults is that they drag along hard-coupled dependencies, which makes (de/re)composition impossible. A good resource that deals more explicitly with package management is Agile Principles, Patterns, and Practices.
On the level of encapsulation, code like this is difficult to reason about:
private readonly ILog log;
public MyConsumer(ILog log)
{
this.log = log ??LogManager.GetLogger("My");
}
Although it protects the class' invariants, the problem is that in this case, null
is an acceptable input value. This is not always the case. In the example above, LogManager.GetLogger("My")
may only introduce a Local Default. From this code snippet, we have no way to know if this is true, but for the sake of argument, let's assume this for now. If the default ILog
is indeed a Local Default, a client of MyConsumer
can pass in null
instead of ILog
. Keep in mind that encapsulation is about making it easy for a client to use an object without understanding all the implementation details. This means that this is all a client sees:
public MyConsumer(ILog log)
In C# (and similar languages) it's possible to pass null
instead of ILog
, and it's going to compile:
var mc = new MyConsumer(null);
With the above implementation, not only will this compile, but it also works at run-time. According to Postel's law, that's a good thing, right?
Unfortunately, it isn't.
Consider another class with a required dependency; let's call it a Repository, simply because this is such a well-known (albeit overused) pattern:
private readonly IRepository repository;
public MyOtherConsumer(IRepository repository)
{
if (repository == null)
throw new ArgumentNullException("repository");
this.repository = repository;
}
In keeping with encapsulation, a client only sees this:
public MyOtherConsumer(IRepository repository)
Based on previous experience, a programmer may be inclined to write code like this:
var moc = new MyOtherConsumer(null);
This still compiles, but fails at runtime!
How do you distinguish between these two constructors?
public MyConsumer(ILog log)
public MyOtherConsumer(IRepository repository)
You can't, but currently, you have inconsistent behaviour: in one case, null
is a valid argument, but in another case, null
will cause a runtime exception. This will decrease the trust that every client programmer will have in the API. Being consistent is a better way forward.
In order to make a class like MyConsumer
easier to use, you must stay consistent. This is the reason why accepting null
is a bad idea. A better approach is to use constructor chaining:
private readonly ILog log;
public MyConsumer() : this(LogManager.GetLogger("My")) {}
public MyConsumer(ILog log)
{
if (log == null)
throw new ArgumentNullException("log");
this.log = log;
}
The client now sees this:
public MyConsumer()
public MyConsumer(ILog log)
This is consistent with MyOtherConsumer
because if you attempt to pass null
instead of ILog
, you will get a runtime error.
While this is technically still Bastard Injection, I can live with this design for Local Defaults; in fact, I sometimes design APIs like this because it's a well-known idiom in many languages.
For many purposes, this is good enough, but still violates an important design principle:
Explicit is better than implicit
While constructor chaining enables a client to use MyConsumer
with a default ILog
, there's no easy way to figure out what the default instance of ILog
would be. Sometimes, that's important too.
Additionally, the presence of a default constructor exposes a risk that a piece of code is going to invoke that default constructor outside of the Composition Root. If that happens, you've prematurely coupled to objects to each other, and once you've done that, you can't decouple them from within the Composition Root.
Thus, there's less risk involved in using plain Constructor Injection:
private readonly ILog log;
public MyConsumer(ILog log)
{
if (log == null)
throw new ArgumentNullException("log");
this.log = log;
}
You can still compose MyConsumer
with the default logger:
var mc = new MyConsumer(LogManager.GetLogger("My"));
If you want to make the Local Default more discoverable, you can expose it as a Factory somewhere, e.g. on the MyConsumer
class itself:
public static ILog CreateDefaultLog()
{
return LogManager.GetLogger("My");
}
All this sets the stage for answering the specific sub-questions in this question.
1. Does Bastard Injection anti-pattern also occur when default implementation of dependency is a Local Default?
Yes, technically, it does, but the consequences are less severe. Bastard Injection is first and foremost a description that will enable you to easily identify it when you encounter it.
Please note that the above illustration from the book describes how to refactor away from Bastard Injection; not how to identify it.
2. [Should] optional dependencies with local defaults [...] also be avoided?
From a package dependency perspective, you don't need to avoid those; they are relatively benign.
From a usage perspective, I still tend to avoid them, but it depends on what I'm building.
3. Is he also implying that Foreign Default makes parallel development more difficult, while Local Default doesn't?
No, I don't. If you have a default, the default must be in place before you can use; it doesn't matter whether it's Local or Foreign.