SOLID design principles in .NET: the Dependency Inversion Principle Part 5, Hello World revisited
September 9, 2013 4 Comments
Introduction
I realise that the previous post should have been the last one on the Dependency Inversion Principle but I decided to add one more, albeit a short one. It can be beneficial to look at one more example where we take a very easy starting point and expand it according to the guidelines we’ve looked at in this series.
The main source in the series on Dependency Injection is based on the work of Mark Seemann and his great book on DI.
Demo
The starting point of the exercise is the good old one-liner Hello World programme:
static void Main(string[] args) { Console.WriteLine("Hello world"); }
Now that we’re fluent in DIP and SOLID we can immediately see a couple of flaws with this solution:
- We can only write to the Console – if we want to write to a file then we’ll have to modify Main
- We can only print Hello world to the console – we have to manually overwrite this bit of code if we want to print something else
- We cannot easily extend this application in a sense that it lacks any seams that we discussed before – what if we want to add logging or security checks?
Let’s try to rectify these shortcomings. We’ll tackle the problem of message printing first. The Adapter pattern solves the issue by abstracting away the Print operation in an interface:
public interface ITextWriter { void WriteText(string text); }
We can then implement the Console-based solution as follows:
public class ConsoleTextWriter : ITextWriter { public void WriteText(string text) { Console.WriteLine(text); } }
Next let’s find a solution for collecting what the text writer needs to output. We’ll take the same approach and follow the adapter pattern:
public interface IMessageCollector { string CollectMessageFromUser(); }
…with the corresponding Console-based implementation looking like this:
public class ConsoleMessageCollector : IMessageCollector { public string CollectMessageFromUser() { Console.Write("Type your message to the world: "); return Console.ReadLine(); } }
These loose dependencies must be injected into another object, let’s call it PublicMessage:
public class PublicMessage { private readonly IMessageCollector _messageCollector; private readonly ITextWriter _textWriter; public PublicMessage(IMessageCollector messageCollector, ITextWriter textWriter) { if (messageCollector == null) throw new ArgumentNullException("Message collector"); if (textWriter == null) throw new ArgumentNullException("Text writer"); _messageCollector = messageCollector; _textWriter = textWriter; } public void Shout() { string message = _messageCollector.CollectMessageFromUser(); _textWriter.WriteText(message); } }
You’ll realise some of the most basic techniques we’ve looked at in this series: constructor injection, guard clause, readonly private backing fields.
We can use these objects from Main as follows:
static void Main(string[] args) { IMessageCollector messageCollector = new ConsoleMessageCollector(); ITextWriter textWriter = new ConsoleTextWriter(); PublicMessage publicMessage = new PublicMessage(messageCollector, textWriter); publicMessage.Shout(); Console.ReadKey(); }
Now we’re free to inject any implementation of those interfaces: read from a database and print to file; read from a file and print to an email; read from the console and print to some web service. The PublicMessage class won’t care, it’s oblivious of the concrete implementations.
This solution is a lot more extensible. We can use the decorator pattern to add functionality to the text writer. Let’s say we want to add logging to the text writer through the following interface:
public interface ILogger { void Log(); }
We can have some default implementation:
public class DefaultLogger : ILogger { public void Log() { //implementation ignored } }
We can wrap the text printing functionality within logging as follows:
public class LogWriter : ITextWriter { private readonly ILogger _logger; private readonly ITextWriter _textWriter; public LogWriter(ILogger logger, ITextWriter textWriter) { if (logger == null) throw new ArgumentNullException("Logger"); if (textWriter == null) throw new ArgumentNullException("TextWriter"); _logger = logger; _textWriter = textWriter; } public void WriteText(string text) { _logger.Log(); _textWriter.WriteText(text); } }
In Main you can have the following:
static void Main(string[] args) { IMessageCollector messageCollector = new ConsoleMessageCollector(); ITextWriter textWriter = new LogWriter(new DefaultLogger(), new ConsoleTextWriter()); PublicMessage publicMessage = new PublicMessage(messageCollector, textWriter); publicMessage.Shout(); Console.ReadKey(); }
Notice that we didn’t have to do anything to PublicMessage. We passed in the interface dependencies as before and now we have the logging function included in message writing. Also, note that Main is tightly coupled to a range of objects, but it is acceptable in this case. We construct our objects in the entry point of the application, i.e. the composition root which is the correct place to do that. We don’t new up any dependencies within PublicMessage.
This was of course a very contrived example. We expanded the original code to a lot more complex solution with a lot higher overhead. However, real life applications, especially enterprise ones are infinitely more complicated where requirements change a lot. Customers are usually not sure what they want and wish to include new and updated features in the middle of the project. It’s vital for you as a programmer to be able to react quickly. Enabling loose coupling like that will make your life easier by not having to change several seemingly unrelated parts of your code.
View the list of posts on Architecture and Patterns here.
Pingback: Architecture and patterns | Michael's Excerpts
Hi Andras,
First of all, I’m a subscriber because of your great articles. I was reading this article: “Rewriting Hello World according to SOLID in .NET” and this one, but they seem to be the same. Is it correct?
Hello Javier, yes, they are the same articles. It happens that I have no time to produce new material so I recycle some old but still relevant one. This is also so that new readers, especially those who follow the blog only by email notifications, become aware of an old post they may find interesting.
//Andras
Hey Andras, really good article thanks a lot!
I suspect Shout() method of PublicMessage class knows too much. Literally, its name implies that it’s only responsible for output. But it also grabs input – hence double-duty – wouldn’t it be a violation of SRP?
Seems like Mediator or Observer are only 2 ways to make it 100% SOLID?