My First DI Application
We start our example with a service class in which the concerns are not separated. Then we will improve maintainability step-by-step, by first separating concerns and then programming to interface in order to make our modules loosely coupled. At the final point, we will have our first DI application. The source code for all the examples of this book is available for download on the publisher's website.
The main responsibility of this service is to send an e-mail using the information provided. In order to make the example simple and clear, client initialization is omitted.
class MailService { public void SendEmail(string address, string subject, string body) { var mail = new MailMessage(); mail.To.Add(address); mail.Subject = subject; mail.Body = body; var client = new SmtpClient(); // Setup client with smtp server address and port here client.Send(mail); } }
Then, we add some logging to it, so that we know what is going on in our service:
class MailService { public void SendMail(string address, string subject, string body) { Console.WriteLine("Creating mail message..."); var mail = new MailMessage(); mail.To.Add(address); mail.Subject = subject; mail.Body = body; var client = new SmtpClient(); // Setup client with smtp server address and port here Console.WriteLine("Sending message..."); client.Send(mail); Console.WriteLine("Message sent successfully."); } }
After a little while, we find it useful to add time to our logs. In this example, sending the mail message and logging functionality are two different concerns which are addressed in a single class, and it is not possible to change the logging mechanism without touching the MailService
class. Therefore, in order to add time to our logs, we have to change the MailService
class. So, let's re-factor this class and separate the concern of logging from sending a mail prior to adding the time functionality:
class MailService { private ConsoleLogger logger; public MailService() { logger = new ConsoleLogger(); } public void SendMail(string address, string subject, string body) { logger.Log("Creating mail message..."); var mail = new MailMessage(); mail.To.Add(address); mail.Subject = subject; mail.Body = body; var client = new SmtpClient(); // Setup client with smtp server address and port here logger.Log("Sending message..."); client.Send(mail); logger.Log("Message sent successfully."); } }
The ConsoleLogger
class is only responsible for the logging mechanism, and this concern is removed from MailService
. Now, it is possible to modify the logging mechanism without affecting MailService
:
class ConsoleLogger { public void Log(string message) { Console.WriteLine("{0}: {1}", DateTime.Now, message); } }
Now, we need to write our logs in Windows Event Log rather than showing them in console. Looks like we need an EventLogger
class as well:
class EventLogger { public void Log(string message) { EventLog.WriteEntry("MailService", message); } }
Although the concern of sending mail and logging are now separated in two different classes, MailService
is still tightly coupled to the
ConsoleLogger
class, and it is not possible to replace its logger without modifying it. We are just one step away from breaking the tight coupling between the MailService
and Logger
classes. We should now introduce the dependencies as interfaces rather than concrete classes:
interface ILogger { void Log(string message); }
Both the ConsoleLogger
and EventLogger
classes should implement this interface:
class ConsoleLogger:ILogger { public void Log(string message) { Console.WriteLine("{0}: {1}", DateTime.Now, message); } } class EventLogger:ILogger { public void Log(string message) { EventLog.WriteEntry("MailService", message); } }
Now, it is time to remove the references to the concrete ConsoleLogger
class and address ILogger
instead:
private ILogger logger; public MailService() { logger = new ILogger(); }
But the previous code won't compile because it doesn't make sense to instantiate an interface. We should introduce this dependency as a constructor parameter and have the concrete object injected into it by a third party:
public MailService(ILogger logger) { this.logger = logger; }
At this point, our classes are loosely coupled and we can change the loggers freely without affecting the
MailService
class. Using DI, we have also separated the concern of creating a new instance of the logger class, which includes the concern of deciding what concrete logger to use from the main responsibility of MailService
, which is sending an e-mail:
internal class Program { private static void Main(string[] args) { var mailService = new MailService(new EventLogger()); mailService.SendMail("someone@somewhere.com", "My first DI App", "Hello World!"); } }
The main method of this application is where we decide what concrete objects to inject in our dependent classes. This (preferably) unique location in the application where modules are composed together is named Composition Root
by Mark Seemann. For more information on DI, Dependency Injection in .NET, by Mark Seemann is recommended.