When we apply the SOLID principles, the result is usually a lot of small classes, each having a single responsibility, along with interfaces that have a number of smaller methods.
Dependency Injection (DI) is the preferred way of making these classes interact with each other. Classes declare their dependencies in their constructors, and a separate entity is responsible for providing these dependencies. This entity is the Composition Root.
The Composition Root is the place where we create and compose the objects resulting in an object-graph that constitutes the application. This place should be as close as possible to the entry point of the application.
When we apply the Open/Closed Principle (the O in SOLID), the normal change process during maintenance involves creating a new class or classes that implement the new or modified system behavior. We then go to the Composition Root and make sure that an instance of this class or these classes are injected in the appropriate location in the object graph. This process should involve no modification of existing classes (other than the Composition Root). This process makes SOLID code append-only.
This article is published from the DNC Magazine for Developers and Architects. Download this magazine from here [Zip PDF] or Subscribe to this magazine for FREE and download all previous and current editions
DI containers are tools that intend to help developers compose the object graph easily. However, the usage of such tools is optional. Pure DI is the application of Dependency Injection without using a DI container. When we use Pure DI, we construct the objects manually and inject their dependencies manually.
It’s important to make sure that the Composition Root is easy to read and navigate, so that the change process is done in an easy and quick manner.
In this article, I am going to show how we can use Pure DI and clean code practices to create large Composition Roots that are readable and maintainable. I will show how a normal change process would look like.
An example: initial build
Let’s say that we are building a document indexing application. This application is required to collect documents from some source, store them in a database, and index them so that other applications can allow users to search the documents in this database.
A high level design of such an application is expected to look like the following:
Basically, the Document Grabber component pulls documents from some source. It then gives them to the Document Processor component, which in turn processes them and sends them to the Document Store.
In this article, I am going to use the term component to refer to a group of classes (or even a single class) in the object graph that is responsible for some function in the system.
So we start building the application, and a set of classes and interfaces emerge. The following figure shows some of the classes and how they are supposed to be wired together in the Composition Root:
Basically, the DocumentGrabber class requires an IDocumentSource object to pull documents from, and an IDocumentProcessor object to which it will give the documents for processing. When we compose the application, we use the FileSystemDocumentSource class as our document source. This is because the system is required to pull documents from the file system only. As a document processor, we use the DocumentProcessor class. This class uses a DocumentIndexer to extract information about the document (e.g. words in the document). It also depends on IDocumentStore which is used to store documents with their corresponding indexing information.
Now, here is how we can use Pure DI to wire these objects:
static void Main(string[] args)
{
//...
var documentGrabber = new DocumentGrabber(
new FileSystemDocumentSource(path),
new DocumentProcessor(
new DocumentIndexer(),
new DatabaseDocumentStore(storeConnectionString)));
//...
}
The variables ‘path’ and ‘storeConnectionString’ are settings that come from the configuration file.
Currently, this Composition Root is very small. We simply have it in a single method.
New requirements
Now, we get a new requirement to add support for pulling documents from an FTP server and from some database that belongs to another application. Here is how such a change process looks like:
We first create new implementations of the IDocumentSource interface to support this new behavior. Namely, we create the FtpServerDocumentSource and DatabaseDocumentSource classes.
Secondly, since the DocumentGrabber class depends on a single IDocumentSource, we need to create an implementation of such an interface that allows us to collect documents from multiple sources. We use the composite design pattern to create a CompositeDocumentSource. This class takes an array of IDocumentSource objects in the constructor and implements the IDocumentSource interface by pulling documents from all the injected document sources. Here is how the application graph looks like now:
Finally, we create instances of the new classes in the Composition Root and then compose them using Pure DI like this:
static void Main(string[] args)
{
//...
var documentGrabber = new DocumentGrabber(
new CompositeDocumentSource(
new FileSystemDocumentSource(path),
new DatabaseDocumentSource(otherApplicationConnectionString),
new FtpServerDocumentSource(ftpServerAddress)),
new DocumentProcessor(
new DocumentIndexer(),
new DatabaseDocumentStore(storeConnectionString)));
//...
}
Now, the Composition Root is getting bigger. Let’s refactor it. One advice from the Clean Code book by Robert C. Martin is that functions should have a single level of abstraction. We can apply this principle for object composition. The main method should still create the DocumentGrabber, but let’s extract the code that creates the document source sub-graph into a separate method. Here is how our code looks like now:
static void Main(string[] args)
{
//...
var documentGrabber = new DocumentGrabber(
CreateDocumentSource(),
new DocumentProcessor(
new DocumentIndexer(),
new DatabaseDocumentStore(storeConnectionString)));
//...
}
static IDocumentSource CreateDocumentSource()
{
//...
return new CompositeDocumentSource(
new FileSystemDocumentSource(path),
new DatabaseDocumentSource(otherApplicationConnectionString),
new FtpServerDocumentSource(ftpServerAddress));
}
Now, the CreateDocumentSource method is responsible for creating the document sources and composing them together.
Yet another requirement
The system has received yet another requirement. The system needs to support any number of document sources. The administrator should be able to define a list of document sources inside some XML configuration file. What should we do next?
The first thing we do is visit the Composition Root. This is because the Composition Root constitutes the application. We should be able to understand the structure of the application from there.
So, we go to the Composition Root, and we take a look at the main method. From there it is easy to see that the CreateDocumentSource method should be our next place to visit. We go to such a method and find that we explicitly create 3 document sources. What we need to do next is to change the configuration system (which I am not showing) to support specifying a list of document sources. Here is how a fragment of the configuration file would look like:
<DocumentSources>
<DocumentSource Type="FileSystem" Path="c:\\test"/>
<DocumentSource Type="FileSystem" Path="c:\\test2"/>
<DocumentSource Type="FtpServer" Address="ftpserver1"/>
<DocumentSource Type="Database" ConnectionString="Server=..."/>
</DocumentSources>
Now, assuming that our configuration system has already been modified, we modify the CreateDocumentSource method like this:
static IDocumentSource CreateDocumentSource ()
{
List<SourceSettings> sourceSettingEntries = GetSourceSettingEntries();
return new CompositeDocumentSource(
sourceSettingEntries
.Select(entry => CreateDocumentSourceFromSettingsEntry(entry))
.ToArray());
}
static IDocumentSource CreateDocumentSourceFromSettingsEntry(SourceSettings entry)
{
if(entry.Type == DocumentSourceType.FileSystem)
return new FileSystemDocumentSource(entry.Path);
if (entry.Type == DocumentSourceType.FtpServer)
return new FtpServerDocumentSource(entry.Address);
if (entry.Type == DocumentSourceType.Database)
return new FileSystemDocumentSource(entry.ConnectionString);
throw new Exception("Invalid source type");
}
The examples I gave are simple examples. But the idea here is that the main method of the Composition Root should only contain code that creates the high-level components of the application. This code is usually method call invocations to create components. Then, inside each of these methods, you find code at a lower level of abstraction. For example, for the document source component, you find code that creates individual classes that act as document sources.
As you saw, the complexity of creating the document source component increased with time. As the complexity increases, we extract more methods so that code in each method is at the same level of abstraction. If we follow this rule, then the Composition Root would look like this:
Basically, the Composition Root becomes a tree of components. At the root of the tree, you can see the high-level components of the system. As you go a single level down the tree, you see the sub-components of the components in the level above. In this way, the Composition Root constitutes the application. If you need to understand the structure of the application on different levels, you can go to the Composition Root.
The tree-like structure of the Composition Root makes it navigatable. When you are modifying the application, you can easily navigate from the main method to the method that is responsible for creating the sub-object-graph that you need to modify. For instance, you look at the main method and you find that it calls three methods; CreateDocumentSource, CreateDocumentProcessor, and CreateDocumentStore. These methods create the high-level components of the system. From there, you can pick the component under which the new/modified behavior should be. You then go to the corresponding method to see what sub components are there and do the same thing. You should always give names to these methods that makes them easy to understand and thus makes the navigation process easier.
The tree-like structure for Composition Roots is great. We should always try to make the Composition Root structure that way. However, this is not always possible. The main reason for this is shared components.
Code sharing versus Resource/state sharing
We should always try to make the object graph that we create, tree-like. If two components of the system depend on the same class, we should prefer to give them two different instances of the class. This allows us to make the graph tree-like. This is only possible if such class does not hold resources that the two components would like to share. I call this code sharing. Consider the following figure:
In this figure, two components are using two different instances of SomeClass class. They share the code of this class, but they don’t share any single resource. SomeClass could have some state, but component1 and component2 don’t need to share it, i.e., each component has its own copy of the state.
Resource sharing on the other hand is when you want to have two or more components use the same instance of a class. This is needed in some cases where that class holds a shared resource like shared state. For example, consider a system where you want some components to temporarily pause the whole system in case of an error. You would need a class or a group of classes to manage such pause/resume process. Particularly, some class needs to hold the state of whether the system is currently paused. An instance of such a class needs to be shared across all components that need to pause/resume the system or query the state of the system.
How to deal with resource sharing
In cases where it is required to share an instance of a class between components, we should make sure that we create such an instance at the latest possible point. Consider the following object graph as an example:
In this graph, green objects are instances that are shared between multiple objects. These are the objects that make the graph not tree-like. What we can do is to create object 6 and object 10 in the main method, and then pass them down the methods that create the different components. While this works, it will force us to make many methods on the way down, have parameters to deliver shared dependencies to components. These parameters might not make sense at the upper levels, but we are forced to have them.
To mitigate this issue, we can only create these objects (6 and 10) at the point when they are needed. For object 6, we need to do it before we create object1 because we have to inject it into objects 2, 3, and 4 which are direct dependencies of object 1. However, for object 10, we can move its creation to the method that creates object 4 because it is only needed for objects that are direct dependencies of object 4. Here is how the Composition Root would look like in this case:
static void Main(string[] args)
{
var object6 = new Object6(
new Object9());
var object1 =
new Object1(
CreateObject2(object6),
CreateObject3(object6),
CreateObject4(object6));
}
static Object4 CreateObject4(Object6 object6)
{
var object10 = new Object10();
return new Object4(
object6,
new Object7(
object10),
new Object8(
object10));
}
The creation of object6 in our case is done in the same method that creates object1. Although this might violate the rule of a single level of abstraction per function, we are forced to do it.
Please note that object10 is not created in the main method but in the CreateObject4 method. This can be done since object10 is not needed outside the scope of the sub-object-graph that starts at object4.
Shared dependencies as private fields in the Composition Root
Imagine in the last example that the same instance of object10 that is used by object7 and object8, is also required by object5 and object6. This means that we have to create object10 in the main method and pass it multiple levels down.
One approach to solve this issue is to create object10 in the main method, but save it as a private field. This way, methods at the lower levels can access it without having many methods pass such a dependency. However, at some point, when you have multiple shared instances of the same type, it might become hard to manage them and reason about them.
DI containers
DI containers are tools that aim to help developers create Composition Roots. In my experience, when used with big applications, they actually make the Composition Root less readable and maintainable. For more information about this point of view, you can read my article.
Summary:
In this article, I have shown how we can use Pure DI and the single level of abstraction per function rule to create Composition Roots that we can understand and navigate easily. This will help a lot with the maintenance process. A normal change process involves creating new classes for the new or modified behavior and then going to the now easy-to-navigate Composition Root and make sure that we inject instances of the new classes into the appropriate location.
We should always try to make the structure of the Composition Root, tree-like. This helps a lot with keeping each method concerned with a single level of abstraction. We should prefer to give different components different instances of dependencies, as long as they don’t require resource sharing.
This article has been editorially reviewed by Suprotim Agarwal.
C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.
We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).
Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.
Click here to Explore the Table of Contents or Download Sample Chapters!
Was this article worth reading? Share it with fellow developers too. Thanks!
Yacoub Massad is a software developer and works mainly on Microsoft technologies. Currently, he works at Zeva International where he uses C#, .NET, and other technologies to create eDiscovery solutions. He is interested in learning and writing about software design principles that aim at creating maintainable software. You can view his blog posts at
criticalsoftwareblog.com. He is also the creator of DIVEX(
https://divex.dev), a dependency injection tool that allows you to compose objects and functions in C# in a way that makes your code more maintainable. Recently he started a
YouTube channel about Roslyn, the .NET compiler.