We will build a complete example from Interface to Implementation to examine how Dependency Injection can make your code more easily extensible and testable.
Note: The code sample is .NET Core built with Top-Level statements (very simple) in Program.cs file.
You can also get the source code at my github:
Introduction
Yes, the title of this article was an attempt to get your attention. But, I hope you'll stay with me, because I've been learning & using OOP (Object Oriented Programming) for 30 years and Dependency Injection (DI) still sometimes alludes me and annoys me. I'm very interested in what you think of DI.
Dependency Injection Has Taken Over OOP?
I understand the benefit of using DI but there are places where it feels as if it obscures the clarity of code. At this moment in "Software Development History", it seems that Dependency Injection has taken over almost every other idea in OOP. I want to talk about that and hopefully get feedback from you about how you feel about it.
The promise of Dependency Injection (DI) is that code is more loosely coupled.
Loosely coupled code means you should be able to switch out pieces more easily.
That should mean that extending your code and changing it is easier.
Large Projects With DI Architecture?
However, I don't know if you've been on any larg(er) projects where much of the architecture is based upon Dependency Injection, because they can be very odd to debug and deal with. Especially if there is a IoC (Inversion of Control) Container involved -- the company I work for uses Microsoft Unity (see UnityContainer.org for more) -- this is not the Unity game engine.
Background
In the past 4 - 6 years, the company I work for has pushed DI to the forefront: all code that is created must use DI, there are no exceptions.
Write / Log Anywhere
I've also often thought about a specific solution that I'd like to see which allows me to write code debugging statements to a file, to a database or even post them to a WebAPI.
Request For Comments (RFC)
I finally created a good start that contains full implementation of my idea which is:
- Built on full Dependency Injection concept
- Guides the implementor to use the code to build their implementation
I'm really hoping that you'll take a look at my design and tell me what you think.
I'm hoping that if you've never really dealt with Dependency Injection, it'll open your eyes and make you consider where you might use it.
But I'm also hoping that some of you will give the counterpoint (why you don't like it) also.
Now, let's get started as I try to explain what I'm trying to get there and why I'm using Dependency Injection for this design.
It All Starts With Behaviors
Interfaces are really just contracts of behavior. They have no implementation, but instead are just a promise of some future behavior that will be provided by the classes.
What I Want
I want a way to track the activity that is going on in my code. I want a way to quickly and easily add a line of code which:
- Logs a message that I provide
- timestamps the message
WriteActivity Method
To me, that sounds like a WriteActivity
method.
I'm going to add that to an Interface and here it is:
public interface ITrackable
{
bool WriteActivity(String message);
}
That's it. The method will take a String
message
that will be stored and it will return a bool (true
if successful, otherwise false
).
Forcing the Implementor
Now, keep in mind that this contract of WriteActivity
is really a way to force the implementor (developer who uses Interface) to implement this method.
Next Question: Where Will It Write the Message?
We need a way to tell it to write to Storage of some kind. In this case I'm thinking of three possible choices (there could be more in the future):
- file (need a path and file name)
- database (need a database and table)
- WebAPI endpoint (need a URL)
Different Storage Types: Different Configurations
In each case, I need a different type of configuration.
Well, the first thing I'm thinking here is that I can store the file, the database connection or the URL as a string
. Okay, let's force the implementor to also implement a place to hold the string
which will represent the target storage. Plus we need a Configure()
call to always be called to ensure that the StorageTarget
is set, so let's add those now.
public interface ITrackable {
bool WriteActivity(String message);
String StorageTarget { get; }
bool Configure();
}
So Little, Just An Idea Really
It's odd, because so far, it is not much of anything. There's no implementation and barely anything here. As a matter of fact, the new StorageTarget
variable is read-only and can't even be set.
Very Generic
That's because I want the user to define a String
variable name of their own to hold the StorageTarget
. You see the StorageTarget
may be a file path or it may be a connection string to a database. So I need this thing to be very generic.
Let's keep going and see if it makes any sense.
Forcing the Implementor To Initialize
There's one more thing I want to force the implementor to do when using this design. I want the implementor to always run a Configure()
method before any other code runs.
I want the implementor to do this because there will be different required configuration / initialization for each of the TargetStorage
types.
Example With A File
In the case of a file, we will need a target file path to be set so that when the WriteActivity()
method calls, it will write to the correct (expected) file.
However, it will be different for a database table.
Example With A Database Table
When the TargetStorage
location is a database connection string
, then the configuration will be different. It will consist of a connection string which points to a database and a table and sometimes other information.
Note: In our implementation, I will be using Sqlite database because you will be able to run all of the code very easily -- no special database set up like SQL Server or mysql.
I can't force an implementor to call a method with an Interface. However, I can do so with an abstract
class.
Abstract Class: Part Virtual, Part Implementation
public abstract class Trackable, ITrackable {
public Trackable(){
Configure();
}
public abstract bool Configure();
public abstract bool WriteActivity(String);
public abstract String StorageTarget{get;}
}
We have four methods, three abstract (unimplemented) and the other implemented (constructor):
Trackable()
constructor calls Configure()
- The rest of the methods are the
ITrackable
methods that we are forced to "implement" but we've made them abstract so the target implementation class will have to write those implementations.
Now, let's finally see what an implementor will do to build on top of the ITrackable
and Trackable
classes. After that, we can talk about whether or not it is valuable and helpful.
Finally, A Realized Class: Implementation Rules!
Let's do this in steps. First let's just add the class.
public class FileActivityTracker : Trackable
{
}
A Trackable
is a ITrackable
(via inheritance) so that means our FileActivityTracker
is a ITrackable
. This will become important later.
If you do that and then try building the program, you'll get some errors from the C# compiler that look like:
- error CS0535: '
FileActivityTracker
' does not implement inherited abstract member 'Trackable.Configure()
' - error CS0535: '
FileActivityTracker
' does not implement interface member 'ITrackable.StorageTarget
' - error CS0535: '
FileActivityTracker
' does not implement interface member 'ITrackable.WriteActivity(string)
'
It's warning us that we have not implemented the items which are defined in the abstract
class and Interface.
Let's implement the Configure()
method first.
Note: Linux or Windows
I wrote this code so it runs on both Linux and Windows so you'll see an odd thing where I get the Path.DirectorySeparatorChar
for use in the path (on Windows it is backslash \ but on Linux it's slash /) but that is just overhead and not related to the true Configure()
method.
In reality, you would probably just read the FileName
from a configuration file and that'd be it.
I added some special code here so it will create a file in your userprofile\temp directory.
- On Windows, that is \users\<user.name>\temp\
- On Linux, that is /home/<user.name>/temp/
Then, after that line, I've added a Console.WriteLine()
which will let us know when the Configure()
method runs. This is just for demonstration purposes, because I want you to see that the method runs before the implementation class (FileActivityTracker
) runs.
Real Implementation, Probably Uses App.Config
My point is that in the real implementation, the only code that would really be in the Configure()
method is the code to set up the FileName
that will be written to. And that would probably come from some startup config (app.config) that is read when the app starts.
And, oh yeah, keep your finger on that FileName
variable because it is really a backing field to the class TargetStorage
Property that we need to add (below). Hopefully, it'll make more sense in a moment.
public override bool Configure(){
char pathSlash = Path.DirectorySeparatorChar;
Console.WriteLine
($"{DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss.fff tt")} : Configure() method called! ");
FileName = @$"{Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)}
{pathSlash}temp{pathSlash}InterfaceStudy.log";
return true;
}
We are also missing the implementation of StorageTarget
so let's add that property now.
StorageTarget: Just A Pass-thru Property
In an attempt to force the implementor to understand that she will need a place to store the file name, connection string, etc. I've added this StorageTarget
thing. However, it is really just a common variable name that we use to get to the true value that holds the value of target.
The user will create a private backing field which has a more definitive name for what the StorageTarget
is. In my example, I name that backing field FileName
. Here's the code that will be added to our FileActivityTracker
class.
private overrid String FileName;
public override string StorageTarget
{
get
{
return FileName;
}
}
The FileName
variable cannot be touched externally and can only be set from within the FileActivityTracker
class
Finally, we get to see the implementation of the WriteActivity(String message)
method and I believe that will all make it a bit clearer.
public override bool WriteActivity(String message)
{
try
{
File.AppendAllText(StorageTarget, $"{DateTime.Now.ToLongTimeString()}:
{message} {Environment.NewLine}");
return true;
}
catch (Exception ex)
{
return false;
}
}
Keeping It Simple
The WriteActivity
method is very simple. It is just a call to File.AppendAllText
(part of .NET libraries) to write a line of text to a text file.
File.AppendAllText
takes two parameters:
- Full file path to the file the method will open and write to
- An output
string
to write to the target file
The magic in our implementation is that the first parameter is our StorageTarget
. It is our generically named String
which was configured to point to our target file.
The output just contains some additional information which is added to the user's message
variable. In this case, we add a DateTime
stamp and a newline so that each time we write to the file, it will be on a new line.
How Do We Use the Class?
Seeing how to use the class brings it all home.
Hiding the Details of the Implementation
One of the key features of seeing a simple usage is that we discover that the details of all of this implementation are hidden to the user and that is a huge benefit. The user of the final implementation class doesn't have to think about all of this.
ITrackable fat = new FileActivityTracker();
fat.WriteActivity("The app is running.");
All the user needs to do is:
new
up a FileActivityTracker
- Call the
WriteActivity
method to write a message.
In our case with the example, the output will be stored in a file named InterfaceStudy.log and the output will look like:
5:26:38 PM: The app is running.
ITrackable: Our FileActivityTracker is ITrackable
It is very important to notice that our FileActivityTracker
is actually an ITrackable
. That means we could replace this ITrackable
implementation with any other ITrackable
implementation. We will see how this is important when we create our SqliteActivityTracker
and how this allows Dependency Injection.
We Could End the Article Here
I could stop now and you'd have a full example of designing toward the idea of Dependency Injection except we haven't exactly injected anything, have we?
To really understand why this technique of designing to an Interface is important, we need to see how The FileActivityTracker
could be easily replaced by another class. We also need to see the actual concept of injection.
Constructor Injection
What does injection mean anyways? In our case, it is going to mean injecting a particular type into a class that will use our type. How will we inject the type? We will send it into the class via the constructor. This is called constructor injection.
What is the Point?
But, what is the point of all of this? Let's say you have a class that has a method named WriteActivity()
-- same name as the one that we have in our Interface. Let's say that you've implemented the WriteActivity()
method so it will write to a file. Again, just as we have in our Interface. Now, further imagine that your method is called all over the place so that users can write messages to the file.
The Change Comes & Destroys Your Code
Then someone at a high level says, "We can't write files to the user's drive. We have to store that data in a database." Now, you'd have to go in and alter the class where you've written your WriteActivity()
algorithm and you'd have to test all the places that call the code to make sure it works.
Let's say your class and class usage looks something like the following:
class FileWriter{
String FileName {get;set;}
WriteActivity(String message){
File.AppendAllText(FileName, message);
}
class Thing1 {
private FileWriter fw = new FileWriter();
Thing1(){
fw.WriteActivity("In the Thing1 ctor...");
}
}
class Thing2 {
private FileWriter fw = new FileWriter();
Thing2(){
fw.WriteActivity("In the Thing1 ctor...");
}
}
This is a very simple example but really take a look at it and think about it for a moment.
Imagine if you change the FileWriter
WriteActivity
method. First of all, if you have other classes which are depending upon the class still writing to a file and you update the method to write to a Database table, then you are going to mess up all that other code.
Or, as another example, imagine if the Thing2
class needs to write to the database but the Thing1
should still write to the File. What could you do then? Maybe create an fake overloaded method for WriteActivity
that looks like WriteActivity(String message, bool useDB)
just so you have a different method signature. That's not great.
Design to An Interface: Design to Behavior
However, if we design to behaviors and then allow just the behaviors to change, we make a much easier way forward. Let's see how we might use Dependency Injection with the classes above and our ITrackable
, Trackable
solution.
class Thing1 {
private ITrackable fat;
Thing1(ITrackable tracker){
fat = tracker;
}
int DoingWork(){
fat.WriteActivity("In the DoingWork() method...");
}
}
So, if we inject our FileActivityTracker
into Thing1
it will write to a file.
Our calling code might look like:
Thing1 t = new Thing1(new FileActivityTracker());
We can change that at any time without changing any code. That's the value of Dependency Injection.
Probably Set Up In App Configuration
And, when you really carry this out, the call to new FileActivityTracker()
would be set up in a app configuration so that when the app starts, all of these types are set.
Let's implement a ITrackable
that writes to Sqlite now so you can see this in action.
SqliteActivityTracker Implementation Code
public SqliteActivityTracker()
{
try{
connection.Open();
File.AppendAllText(@$"{rootPath}{pathSlash}{tempDir}
{pathSlash}InterfaceStudy.log", $"{DateTime.Now.ToLongTimeString()}:
{Environment.CurrentDirectory} {Environment.NewLine}");
FileInfo fi = new FileInfo(@$"{rootPath}{pathSlash}{tempDir}{pathSlash}tracker.db");
if (fi.Length == 0){
connectionString = fi.Name;
foreach (String tableCreate in allTableCreation){
command.CommandText = tableCreate;
command.ExecuteNonQuery();
}
}
Console.WriteLine(connection.DataSource);
}
finally{
if (connection != null){
connection.Close();
}
}
}
public override bool WriteActivity(String message)
{
Command.CommandText = @"INSERT into Task (Description)values($message);
select * from task where id =(SELECT last_insert_rowid())";
Command.Parameters.AddWithValue("$message",message);
try{
Console.WriteLine("Saving...");
connection.Open();
Console.WriteLine("Opened.");
var id = Convert.ToInt64(command.ExecuteScalar());
Console.WriteLine("inserted.");
return true;
}
catch(Exception ex){
Console.WriteLine($"Error: {ex.Message}");
return false;
}
finally{
if (connection != null){
connection.Close();
}
}
}
protected String [] allTableCreation = {
@"CREATE TABLE Task
( [ID] INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
[Description] NVARCHAR(1000) check(length(Description) <= 1000),
[Created] NVARCHAR(30) default (datetime('now','localtime'))
check(length(Created) <= 30)
)"
};
}
This is The Amazing Part: Change Without Changing Old Code
Now that we've implemented that code, all we have to make our Thing1
use the SqliteActivityTracker
is the following:
Thing1 t = new Thing1(new SqliteActivityTracker());
Now all the internal (to Thing1
) calls to WriteActivity
will write to the Sqlite database instead.
Get the Source Code and Try It
You can get the source code and build it and try it.
You will see that the only calling code is the following four lines:
ITrackable fat = new FileActivityTracker();
fat.WriteActivity("The app is running.");
ITrackable sat = new SqliteActivityTracker();
sat.WriteActivity("This is from the app!");
We could've even made that a bit more generic to show that the same ITrackable
can carry any ITrackable
type by showing the following code:
ITrackable tracker = new FileActivityTracker();
tracker.WriteActivity("The app is running.");
tracker = new SqliteActivityTracker();
tracker.WriteActivity("This is from the app!");
Dependency Injection is Really About Design
As you can see, DI is really about Design and thinking about your classes in a more abstract way.
You have to think about shared behaviors and how you might create classes that support those similar behaviors that are implemented in different ways.
Get the Source & Try It Out
If you get the code, you will see that it will write:
- Write to a InterfaceStudy.log file
- Create and write to a Sqlite db (tracker.db in same location as file)
You'll see output something like the following:
Quote:
2024-02-12 11:14:16.358 AM : Configure() method called!
2024-02-12 11:14:16.394 AM : FileActivityTracker ctor...
/home/<user-name>/temp/tracker.db
/home/<user-name>/temp/tracker.db
Saving...
Opened.
inserted.
New Ideas & Comments?
I hope this has given you some new ideas about this concept and I hope you'll comment about what you like and don't like.
History
- 12th February, 2024: Code & article, first publication