The principles of Dependency Inversion provide excellent ways to streamline and test code changes, encouraging behavior that reduces the risks of unintended consequences when later maintained. Combined with a thorough set of tests this can stave off “code-rot” by improving the readability greatly.
The Dependency Inversion Principle (DIP) has two parts.
- High level objects should not depend on low level objects, any reference between them should be to abstractions.
- Those abstractions should not depend on details. Rather the details should depend on the abstractions
While in C# you could easily replace the name “abstraction” with “interface”, this isn’t intended to be applied purely at the object level, abstraction could refer to the overall design before you get even so far as writing even interfaces. Even with those interfaces we shouldn’t directly pass through every single property or method but instead look at what is absolutely needed by the parent object to perform the task. Including too much detail couples the classes together too tightly, making swapping one implementation out for another very involved.
Another parallel concept that brings something to the mix is Inversion of Control (IoC). Like DIP this can be applied both at higher level and lower level as you’re implementing. Traditional functional programming puts most of the domain-specific logic near the heart of the program, meaning that to change or swap out any of it, you’re going to have to touch the code in many places. When you invert that control model, you end up with relatively simple code running at the heart of it that mostly is there to pass messages between dependencies that now contain most of the specific logic broken into distinct chunks based on what it does.
Now that we’ve been covering such high level topics, how about an example?
| Traditional Way | DIP/IoC way |
class FileStore {
public void Open(string filename)
{ ... }
public Record Read()
{...}
public void Write(Record row)
{ .. }
public void Close()
{ ... }
}
|
interface IRecordStore {
IRecord Read();
void Write(IRecord row);
}
class FileRecordStore:IRecordStore
class DBRecordStore:IRecordStore
class TapeRecordStore:IRecordStore
|
In our example, the traditional way if we forgot to call open when we started or close when finished, it could cause major problems. Also if we decide we want it to write to a database or tape drive instead, you’ll have to rewrite a lot of places.
While DIP and IOC form a nice abstract pattern for us to aspire to. Dependency Injection helps answer the “how does this actually work”. Dependency Injection (DI) helps solve the problem of how to interconnect the different objects while still giving enough flexibility to build tests that work at the unit level. To do so, it separates the concerns (or actions) of creating instances and using them. Using our Record storage example, perhaps you have a Table object that needs to pull it’s data via the IRecordStore’s methods. If you hard-code a creation of a FileRecordStore or DatabaseRecordStore anywhere in the Table object then suddenly you’ve lost the ability to easily swap it out for testing or for a different Record Store. This will mean for all your work, the two classes have become tightly coupled again. Instead, what you need is a mechanism to create, then pass these dependencies down to the other objects who will actually use them. This neatly brings us to the 3 methods Dependency Injection use to accomplish this:
- Constructor Injection
This method passes the dependent to the parent as the parent is constructed. It doesn’t care where they were created because that is outside it’s concerns. This is the most common way of doing it, especially in cases where an object cannot function without it’s dependencies. A constructor example:
public TableClass(IRecordStore dataStore)
- Setter or Property Injection
This method a property or set function to set the object’s internal instance of a dependency to whatever is passed to it. This is common in cases where the dependency in question is expensive to create and thus you don’t want to create them before they are needed.
public void SetRecordStore(IRecordStore dataStore)
This makes it possible to create and try to use an object before all it’s dependent’s are created. This is a real disadvantage as it requires additional error checking code to verify that objects were set before they were accessed internally.
- Method Injection
Sometimes the object doesn’t need to maintain an internal reference to a dependency, either because it’s only used in a single method or because the implementation of the dependency can change quickly based on outside requirements. In this case you can pass the dependent directly to the function that uses it as an additional argument
public void WriteAllRows(IRecordStore dataStore)
Now if you’re like me, you might immediately see how useful this is but get hung up on a rather important detail: What creates these dependencies? At some point in here, something has to create it, and that something has to be able to decide which of the specific implementations to create. In a more complex system where there might be dozens of these that’s a lot of objects to be throwing references around via one of the 3 methods above. Especially if you need them several levels down in the hierarchy, passing them through every object in between looks as clunky as it sounds. This is where Dependency Injection libraries take over the load, and yes we’re going to cover that as well. That however is a post that even paired down to what you need to get a good start, is too long for today. So tune in later, for part 2 of this post where we will talk about a Microsoft library that helps answer those remaining questions.