DI anti-patterns
5.3 Constrained Construction
The biggest challenge of properly implementing DI is getting all classes with DEPEN-
DENCIES moved to a COMPOSITION ROOT. When we accomplish this, we’ve already come a long way.
Even so, there are still some traps to look out for. A common mistake is to require all DEPENDENCIES to have a constructor with a particular signature. This normally origi-
nates from the desire to attain late binding so that DEPENDENCIES can be defined in an external configuration file and thereby changed without recompiling the application.
NOTE The so-called Provider pattern7 used in ASP.NET is an example of C ON-
STRAINED CONSTRUCTION, because Providers must have default constructors.
This is normally exacerbated by the Provider’s constructor attempting to read from the application configuration file. Often, the constructor throws an exception if the required configuration section isn’t available.
NOTE This section applies only to scenarios where late binding is desired. In scenarios where we directly reference all DEPENDENCIES from the application’s root, we don’t have this problem—but then again, we don’t have the ability to replace DEPENDENCIES without recompiling, either.
In chapter 3, we briefly touched on this issue. This section examines it more carefully.
5.3.1 Example: late-binding ProductRepository
In the sample commerce application, some classes depend on the abstract Product- Repository class. This means that to create those classes, we first need to create an instance of ProductRepository. At this point, you’ve learned that a COMPOSITION ROOT is
7 Rob Howard, “Provider Model Design Pattern and Specification, Part 1,” 2004, http://msdn.microsoft.com/
the correct place to do this. In an ASP.NET application, this means Global.asax; the fol- lowing listing shows the relevant part that creates an instance of ProductRepository.
string connectionString = ConfigurationManager.ConnectionStrings ["CommerceObjectContext"].ConnectionString; string productRepositoryTypeName = ConfigurationManager.AppSettings ["ProductRepositoryType"]; var productRepositoryType = Type.GetType(productRepositoryTypeName, true); var repository = (ProductRepository)Activator.CreateInstance( productRepositoryType, connectionString);
The first thing that should trigger suspicion is that a connection string is read from the web.config file. Why do you need a connection string if you plan to treat a ProductRepository as an ABSTRACTION? Although it’s perhaps a bit unlikely, you could
choose to implement a ProductRepository with an in-memory database or an XML
file. A REST-based storage service such as the Windows Azure Table Storage Service offers a more realistic alternative, but once again this year, the most popular choice seems to be a relational database. The ubiquity of databases makes it all too easy to forget that a connection string implicitly represents an implementation choice.
To late-bind a ProductRepository, you also need to determine which type has been chosen as the implementation. This can be done by reading an assembly- qualified type name from web.config and creating a Type instance from that name. This in itself isn’t problematic—the difficulty arises only when you need to create an instance of that type.
Given a Type, you can create an instance using the Activator class. The Create- Instance method invokes the type’s constructor, so you must supply the correct con- structor parameters to prevent an exception from being thrown. In this case, you supply a connection string
B
.If you didn’t know anything else about the application other than the code in list- ing 5.3, you should by now be wondering why a connection string is passed as a con- structor argument to an unknown type. It wouldn’t make a lot of sense if the implementation was based on a REST-based web service or an XML file.
Indeed, it doesn’t make sense, because this represents an accidental constraint on the DEPENDENCY’s constructor. In this case, you have an implicit requirement that any
implementation of ProductRepository should have a constructor that takes a single string as input. This is in addition to the explicit constraint that the class must derive from ProductRepository.
NOTE The implicit constraint that the constructor should take a single string still leaves us a great degree of flexibility, because we can encode a lot of Listing 5.3 Implicitly constraining the ProductRepository constructor
Create instance of concrete type
151
Constrained Construction
different information in strings to be decoded later. Imagine instead that the constraint was a constructor that takes a TimeSpan and a number, and you can begin to imagine how limiting that would be.
You could argue that a ProductRepository based on an XML file would also require a string as constructor parameter, although that string would be a file name and not a connection string. However, conceptually it would still be weird, because you would have to define that file name in the connectionStrings element in web.config (and in any case, I think such a hypothetical XmlProductRepository should take an Xml- Reader as constructor argument instead of a file name).
Modeling DEPENDENCY construction exclusively on explicit constraints (interface or
base class) is a much better and more flexible option.
5.3.2 Analysis
In the previous example, the implicit constraint required implementers to have a con- structor with a single string parameter. A more common constraint is that all imple- mentations should have a default constructor so the simplest form of Activator .CreateInstance will work:
var dep = (ISomeDependency)Activator.CreateInstance(type);
Although this can be said to be the lowest common denominator, the cost in flexibility is too high.
IMPACT
No matter how we constrain object construction, we lose flexibility. It might be tempt- ing to declare that all DEPENDENCY implementations should have a default construc-
tor—after all, they could perform their initialization internally, like reading configuration data such as configuration strings directly from the .config file. How- ever, this would limit us in other ways, because we might want to be able to compose an application of layers of instances that encapsulate other instances. In some cases, for example, we might wish to share an instance between different consumers, as illus- trated in figure 5.4.
Figure 5.4 In this example, we wish to create a single instance of the
ObjectContext class and inject the same instance into both Repositories. This is possible only if we can inject the instance from the outside.
When we have more than one class requiring the same DEPENDENCY, we may want to
share a single instance among all those classes. This is possible only when we can inject that instance from the outside. Although we could write code inside each of those classes to read type information from a configuration file and use Activator .CreateInstance to create the correct type of instance, we could never share a single instance this way—instead, we would have multiple instances of the same class, taking up more memory.
NOTE Just because DI allows us to share a single instance among many con- sumers doesn’t mean we should always do it. Sharing an instance saves mem- ory but may introduce interaction-related problems such as threading issues. Whether we wish to share an instance is closely related to the concept of
OBJECT LIFETIME, which is discussed in chapter 8.
Instead of imposing implicit constraints on how objects should be constructed, we should rather implement our COMPOSITION ROOT so that it can deal with any kind of
constructor or factory method we may throw at it.
REFACTORINGTOWARD DI
How can we deal with having no constraints on components’ constructors when we need late binding? It may be tempting to introduce an Abstract Factory that can be used to create instances of the required ABSTRACTION and then require that implemen- tations of such Abstract Factories have default constructors, but doing so is likely to move the underlying problem around without solving it.
WARNING Although we can use Abstract Factories to successfully implement late binding, doing so requires discipline. In general, we’re better off with a proper DI CONTAINER; but I’ll sketch out how to do it the hard way nonetheless.
Let’s briefly examine such an approach. Imagine that you have a service ABSTRACTION
imaginatively called ISomeService. The Abstract Factory scheme dictates that you also need an ISomeServiceFactory interface. Figure 5.5 illustrates this structure.
Now let’s assume that you wish to use an implementation of ISomeService that requires an instance of ISomeRepository to work, as shown in the following listing.
Figure 5.5 ISomeService represents the real DEPENDENCY. However, to keep its implementers free of implicit constraints, you attempt to solve the late- binding challenge by introducing the ISomeServiceFactory that will be used to create instances of ISomeService. And you will require of any factories that they have a default constructor.
153
Constrained Construction
public class SomeService : ISomeService {
public SomeService(ISomeRepository repository) {
} }
The SomeService class implements the ISomeService interface, but requires an instance of ISomeRepository. Because the only constructor isn’t the default construc- tor, the ISomeServiceFactory will come in handy.
Currently, you want to use an implementation of ISomeRepository that’s based on the Entity Framework. You call this implementation SomeEntityRepository, and it’s defined in a different assembly than SomeService.
Because you don’t want to drag a reference to the EntityDataAccess library along with SomeService, the only solution is to implement SomeServiceFactory in a differ- ent assembly than SomeService, as shown in figure 5.6.
Even though ISomeService and ISomeServiceFactory look like a cohesive pair, it’s important to implement them in two different assemblies, because the factory must have references to all DEPENDENCIES to be able wire them together correctly.
By convention, the ISomeServiceFactory implementation has a default construc- tor, so you can write the assembly-qualified type name in a .config file and use Activator.CreateInstance to create an instance. Every time you need to wire together a new combination of dependencies, you must implement a new ISome- ServiceFactory that wires up exactly that combination and then configure the appli- cation to use that factory instead of the previous one. This means you can’t define arbitrary combinations of DEPENDENCIES without writing and compiling code, but you
can do it without recompiling the application itself.
Listing 5.4 SomeService that requires ISomeRepository
Figure 5.6 The SomeServiceFactory class must be implemented in a separate assembly than SomeService, to prevent coupling the
Essentially, such an Abstract Factory becomes an Abstract COMPOSITION ROOT that’s
defined in an assembly separate from the core application. Although this is certainly a viable approach, it’s generally much easier to utilize a general-purpose DI CONTAINER
that can do all this for us out of the box based on configuration files.
The CONSTRAINED CONSTRUCTION anti-pattern only really applies when we employ
late binding, because when we utilize early binding the compiler ensures that we never introduce implicit constraints on how components are constructed.
The last pattern applies much more generally—some people even consider it a proper pattern instead of an anti-pattern.