Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mark Seemann's conflicting statements about Bastard Injection. Need some clarifications

Tags:

I'm reading his book Dependency Injection in Net.

1) Here he's saying that Bastard Injection occurs only when we use Foreign Default.

But in his book, the illustration on page 148 shows that Bastard Injection occurs when the default implementation of the dependency is either Foreign Default or Local Default:

enter image description here

So does Bastard Injection anti-pattern also occur when default implementation of dependency is a Local Default?

2) Here ( and also in his book ) he notes that it's ok for a class to have an optional dependency, provided that a default implementation of this dependency is a good Local Default:

But in next article he seems to object to having optional dependencies at all, even if the default implementation is a Local Default:

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

In terms of encapsulation, the main problem with such an approach is that it seems like the MyConsumer class can't really make up its mind whether or not it controls the creation of its log dependency. While this is a simplified example, this could become a problem if the ILog instance returned by LogManager wraps an unmanaged resource which should be disposed when it's no longer needed.

Are his arguments in the above excerpt also valid when default implementation of dependency is local? If so, then optional dependencies with local defaults should also be avoided?

3) Pg. 147:

The main problem with Bastard Injection is its use of a FOREIGN DEFAULT ... , we can no longer freely reuse the class because it drags along a dependency we may not want. It also becomes more difficult to do parallel development because the class depends strongly on its DEPENDENCY.

Foreign Default is an implementation of a dependency that's used as a default and is defined in a different assembly than its consumer. Thus with Foreign Default, consumer's assembly will also drag along dependency's assembly.

Is he also implying that Foreign Default makes parallel development more difficult, while Local Default doesn't? If he is, then that doesn't make sense, since I would assume that what makes parallel development difficult is not so much that consumer's assembly has hard reference to dependency's assembly, but rather the fact that consumer class depends on a concrete implementation of a dependency?

thanks

like image 823
bckpwrld Avatar asked Jan 27 '14 18:01

bckpwrld


1 Answers

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:

  • Package dependency
  • Encapsulation
  • Ease of use

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.

  • If I create a reusable library (e.g. an OSS project) that many people will use, I may still choose Constructor Chaining in order to make it easier to get started with the API.
  • If I'm creating a class only for use within a particular code base, I tend to entirely avoid optional dependencies, and instead compose everything explicity in the Composition Root.

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.

like image 61
Mark Seemann Avatar answered Oct 05 '22 06:10

Mark Seemann