As promised, a simple example of Dependency Injection
Note: I'm using the PHP code tags simply to add colour; the code is C#
We're going to start with a simple Employee class, that we instantiate for the employee "Jack Taylor"
PHP:
using System;
public class Employee
{
public string firstname { get; }
public string surname { get; }
public Employee(string firstname, string surname)
{
this.firstname = firstname;
this.surname = surname;
}
}
namespace DI
{
class MainClass
{
public static void Main(string[] args)
{
Employee employee1 = new Employee("Jack", "Taylor");
}
}
}
Then after a while we realise we need to some type of logging mechanism to track what is going on within our code, so we alter the class to add a toLog() method. Which we then can call whenever we want to log employee data to the console.
PHP:
public class Employee
{
public string firstname { get; }
public string surname { get; }
public Employee(string firstname, string surname)
{
this.firstname = firstname;
this.surname = surname;
}
public void toLog()
{
string output = string.Format("[firstname: {0}, surname: {1}]", this.firstname, this.surname);
Console.WriteLine("{0:yyyy-M-d dddd hh:hh:mm:ss tt zz} : {1}", DateTime.Now, output);
}
}
namespace DI
{
class MainClass
{
public static void Main(string[] args)
{
Employee employee1 = new Employee("Jack", "Taylor", new LoggerTest());
employee1.toLog();
}
}
}
Now our codebase starts growing and we keep adding this toLog() method to every new class we create; so no problem, well that is until some bright spark pipes up and says:
- Let's change the format of the log output to include .......
- or.... let's log this in production to a dedicated logging server, and in testing to the console.
Whew at which point you're probably doing your nut, because you've just realised there are over 100s of classes that have a toLog() method.
Question: So how could we have avoided this pain? This is a good (and simple) example of where Dependency Injection techniques can help.
Dependency Injection
First off we need to define our generic requirements for logging; remember we're not at this point in time concerned about the how, but rather the what we need. Why, simply because as the above example showed, some bright spark will always come up with some new way to do the logging, and your code structure will need to be flexible enough to handle those changes.
Interfaces
We going to define our logging requirement using an interface, because as I said we're less interested in how the logging is done, and more interested in what it needs to do. Interfaces are perfect for that.
PHP:
public interface ILog
{
void WriteToLog(String text);
}
Ok, that's quite short and sweet; we've created a ILog interface which specifies that any class adopting this Interface must create a
void method called
WriteLog with a single String parameter
text.
So how do we use this?
Let's start by rewriting our employee class to use this interface.
PHP:
public class Employee
{
public string firstname { get; }
public string surname { get; }
private ILog log;
public Employee(string firstname, string surname, ILog log)
{
this.firstname = firstname;
this.surname = surname;
this.log = log;
}
public void toLog()
{
string output = string.Format("[firstname: {0}, surname: {1}]", this.firstname, this.surname);
this.log.WriteToLog(output);
}
}
As you can see we've added another property called
log which is of type
ILog; the interface we just defined. Similarly we added another parameter to our default Constructor, expecting anyone who instantiates employees to provide a class that adheres to ILog (i.e. a class that has a "
void WriteToLog(String text)" method)
Next you can see in the toLog() in the employee class, we are now calling that method "
this.log.WriteToLog(output);" as opposed to writing the output directly to the console.
Now let's create a class that complies to the ILog interface and just writes output to the Console?
Basically we're going to create a class that does exactly what the "
Console.Writeline..." was doing previously.
PHP:
public class LogConsole : ILog
{
public void WriteToLog(string text)
{
Console.WriteLine("{0:yyyy-M-d dddd hh:hh:mm:ss tt zz} : {1}", DateTime.Now, text);
}
}
namespace DI
{
class MainClass
{
public static void Main(string[] args)
{
Employee employee1 = new Employee("Jack", "Taylor", new LoggerConsole());
employee1.toLog();
}
}
}
As you can see it's a pretty simple class that adds conformance to an interface "
public class LogConsole : ILog", internally in accordance with interface contract it provides an implementation matching the "
public void WriteToLog(string text)", except that it now includes the how part i.e. writing this out to the console.
Then in the Main method when we instantiate the employee "Jack" we are now required to provide an object that adheres to the
ILog interface, the result is exactly the same; same commands, same output.
Now let's look at a scenario where we want differentiate actions between testing and production. Simple we create two classes that adhere to ILog interface, namely
LoggerProduction and
LoggerTest.
PHP:
public class LoggerProduction : ILog
{
public void WriteToLog(string text)
{
Console.WriteLine("{0:yyyy-M-d dddd hh:hh:mm:ss tt zz} : {1}", DateTime.Now, text);
}
}
public class LoggerTest : ILog
{
public void WriteToLog(string text)
{
Console.WriteLine("{0} : {1}", "** Testing " + new string('*', text.Length - 9), text);
}
}
As you can see the how part varies slightly between
LoggerProduction and
LoggerTest. Using this is quite easy, as we simply have to provide the correct one when we instantiate the employee instance, for example:
PHP:
namespace DI
{
class MainClass
{
public static void Main(string[] args)
{
Employee employee1 = new Employee("Jack", "Taylor", new LoggerTest());
employee1.toLog(); // This writes using the LoggerTest write to log method
Employee employee2 = new Employee("Jack", "Taylor", new LoggerProduction());
employee2.toLog(); // This writes using the LoggerProduction write to log method
}
}
}
Easy enough ???
Naturally you also wouldn't want to have to change every instantiation line of code every time you switch between production, test, and other loggers. i.e. to avoid that you could define a global instance variable for logging, for example: a singleton, or just chain the instance through to the point where you need.
That global instance variable would then probably have some compiler logic allowing you to specify whether you're testing or running in production i.e. automatic adaption.
...and when the bright spark encourages the manager to do something else, you'll easily be able to accomplish it by simply creating a new class implementation that conforms to the ILog interface.
Anyway hope that helps.