SOLID principles in .NET revisited part 7: the Dependency Inversion Principle
May 14, 2015 5 Comments
Introduction
In the previous post we saw the definition of the Interface Segregation Principle. We applied it to a problematic case where a class could not fully implement the IAuthorizationService interface. We then broke up the interface into two parts so that they became more specialised. A consequence of ISP is often a large number of small, very specialised interfaces of 1 or maybe 2 methods. Large, monolithic interfaces are to be avoided as it will be more difficult to find concrete classes that can meaningfully implement all interface methods.
We’ve reached the last letter in the SOLID acronym, i.e. ‘D’ which stands for the Dependency Inversion Principle.
DIP
The DIP states that classes should depend on abstractions for their dependencies. DIP aims to reduce coupling between objects. I personally think the DIP is the largest topic of the 5 within SOLID. There’s a series devoted to DIP on this blog starting here with lots of details: dependency injection, inversion of control, DIP patterns, DIP antipatterns etc. It’s a large topic and this post is only a refresher. It’s not much use going through that material over again in the same depth.
The easiest example of the lack of DIP and tight coupling is the following declaration:
private LoggingService _loggingService; public OnDemandAgentService() { _loggingService = new LoggingService(); }
Why is this bad? The OnDemandAgentService is tightly coupled to the LoggingService object. Also, LoggingService is not an abstraction which makes matters even worse. It’s very difficult to supply a different implementation of the logging mechanism without changing the code manually within OnDemandAgentService.
The solution is to first hide the LoggingService class behind an interface:
public interface ILoggingService { void LogInfo(string info); void LogWarning(string warning); void LogError(string error); }
…and make LoggingService implement the interface:
public class LoggingService : ILoggingService
Then we can declare the type of the private _loggingService field in OnDemandAgentService to be ILoggingService:
private ILoggingService _loggingService; public OnDemandAgentService() { _loggingService = new LoggingService(); }
Is this enough? Not at all, the OnDemandAgentService class is still tied to LoggingService in the constructor. How can we make sure that the client can – and must – provide an implementation of ILoggingService?
ILoggingService is a dependency that OnDemandAgentService needs in order to perform its job. It can create a new one using the “new” keyword like above in a control-freak fashion: I know what I want and I’ll do it myself. Clients have no way of interfering with that. OnDemandAgentService has to trust the client that it will provide the necessary ingredients. OnDemandAgentService will have to open up and provide an entry point for the client. It’s the client’s responsibility to supply a valid concrete implementation for the dependency.
The most straightforward way for such an entry point is a technique called constructor injection. It’s a scary name but all it really means is that we extend the class constructor with parameters for the necessary dependencies. Here’s an example for the LoggingService:
private ILoggingService _loggingService; public OnDemandAgentService(ILoggingService loggingService) { _loggingService = loggingService; }
The clients of OnDemandAgentService will now absolutely have to provide an object which implements ILoggingService when building an OnDemandAgentService object.
There’s one remaining problem. A client can pass in a null which will cause an exception to be thrown in the StartNewOnDemandMachine method. We can add a guard clause to prevent that:
public OnDemandAgentService(ILoggingService loggingService) { if (loggingService == null) throw new ArgumentNullException("Logging service"); _loggingService = loggingService; }
Cleanup according to the DIP
Looking through the current state of OnDemandAgentService we can locate the following dependencies:
- ILoggingService
- ICloudProvider
- IAuthorizationService
- IUnauthorizedAccessPunishmentService
- EmailService
We have no abstraction for EmailService yet, let’s remedy that problem quickly:
public interface IEmailService { void SendEmail(string message, string recipient, string emailHost); } public class EmailService : IEmailService { public void SendEmail(string message, string recipient, string emailHost) { //implementation ignored } }
Here’s the updated form of OnDemandAgentService according to the constructor injection technique:
public class OnDemandAgentService { private ILoggingService _loggingService; private ICloudProvider _cloudProvider; private IAuthorizationService _authService; private IUnauthorizedAccessPunishmentService _punisher; private IEmailService _emailService; public OnDemandAgentService(ILoggingService loggingService, ICloudProvider cloudProvider, IAuthorizationService authService , IUnauthorizedAccessPunishmentService punisher, IEmailService emailService) { if (loggingService == null) throw new ArgumentNullException("Logging service"); if (cloudProvider == null) throw new ArgumentNullException("Cloud provider"); if (authService == null) throw new ArgumentNullException("Authorization service"); if (punisher == null) throw new ArgumentNullException("UnauthorizedAccessPunishmentService"); if (emailService == null) throw new ArgumentNullException("Email service"); _loggingService = loggingService; _cloudProvider = cloudProvider; _authService = authService; _punisher = punisher; _emailService = emailService; } public OnDemandAgent StartNewOnDemandMachine() { _loggingService.LogInfo("Starting on-demand agent startup logic"); try { if (_authService.IsAuthorized(Username, Password)) { _loggingService.LogInfo(string.Format("User {0} will attempt to start a new on-demand agent.", Username)); OnDemandAgent agent = _cloudProvider.StartServer(); _emailService.SendEmail(string.Format("User {0} has successfully started a machine with ip {1}.", Username, agent.Ip), "admin@mycompany.com", "email.mycompany.com"); return agent; } else { bool lockedOut = _punisher.LockOut(Username); _loggingService.LogWarning(string.Format("User {0} attempted to start a new on-demand agent. User locked out: {1}", Username, lockedOut)); throw new UnauthorizedAccessException("Unauthorized access to StartNewOnDemandMachine method."); } } catch (Exception ex) { _loggingService.LogError("Exception in on-demand agent creation logic"); throw ex; } } public string Username { get; set; } public string Password { get; set; } }
All clients of OnDemandAgentService are now obliged to provide implementations for all the dependencies of OnDemandAgentService. OnDemandAgentService in turn can be happy as it can now concentrate on its tasks without worrying about creating concrete implementations for the required dependencies.
Also, notice that all instances of the “new” keyword are gone from OnDemandAgentService except where an exception is thrown. As soon as you new up an object which is a dependency you tie your object to that concrete dependency.
Finally it’s important to note that we only have a single constructor which accepts all dependencies. Resist the temptation of creating overloaded constructors where you’ll need to provide some default implementation of a dependency like this:
public OnDemandAgentService(ILoggingService loggingService, ICloudProvider cloudProvider, IAuthorizationService authService) : this(loggingService, cloudProvider, authService, new DatabaseAuthorizationService(), new EmailService()) { }
You might just as well declare your dependencies directly in the constructor with the “new” keyword.
Inversion of control containers
I will only briefly mention IoC containers. If you don’t know what they are then it’s enough to know that they are mechanisms to automatically – automagically… – inject the object dependencies through object constructors or methods. They tend to have quite a complex implementation but it’s usually hidden. A popular IoC container in .NET is StructureMap. StructureMap needs to be initialised at the start of the application’s life cycle where you declare the concrete implementations to use for the dependencies. Here’s an example:
ObjectFactory.Initialize(x => { x.For<IUnitOfWork>().Use<EntityFrameworkUnitOfWork>(); x.For<IRoleProvider>().Use<AspNetRoleProvider>(); x.For<IConfigurationRepository>().Use<AppSettingsConfigurationRepository>(); }); ObjectFactory.AssertConfigurationIsValid();
If you’re not familiar with IoC containers this may look cryptic and it’s probably difficult to understand how this works in a real application. It’s fine, you’ll come into contact with IoC containers when working seriously with DIP.
There’s a single point I want to make here. IoC containers usually allow you to extract the declared implemented type anywhere in your application. In StructureMap you’d do it like this:
IConfigurationRepository custRepository = ObjectFactory.GetInstance<IConfigurationRepository>();
So if a class needs an IConfigurationRepository then it can simply collect it from the abstraction-implementation map you declared at the entry point of the application.
Don’t to that. Never ever.
You objects will suddenly depend on this funny ObjectFactory static class instead:
public OnDemandAgentService() { _loggingService = ObjectFactory.GetInstance<ILoggingService>(); _cloudProvider = ObjectFactory.GetInstance<ICloudProvider>(); _authService = ObjectFactory.GetInstance<IAuthorizationService>(); _punisher = ObjectFactory.GetInstance<IUnauthorizedAccessPunishmentService>(); _emailService = ObjectFactory.GetInstance<IEmailService>(); }
Even worse, the object factory may not have any concrete implementation in its internal map of abstractions. In which case your dependency will resolve to a null. You can make matters worse by adding a null check like this:
_loggingService = ObjectFactory.GetInstance<ILoggingService>(); if (_loggingService == null) _loggingService = new LoggingService();
Brilliant, you’ve just increased the degree of coupling to a new level. In addition these dependency declarations will be invisible to the caller. There’s no evidence in the public interface of OnDemandAgentService that it needs an ILoggingService and will do something with it. OnDemandAgentService in fact hides this information from the caller but that is clearly not what is meant by the OOP concept of information hiding.
We’re done with the core discussion of the SOLID concepts in this refresher series. We’ll clean up bits and pieces of our current classes and interfaces in the next post.
View the list of posts on Architecture and Patterns here.
Pingback: The Morning Brew - Chris Alcock » The Morning Brew #1861
Reblogged this on Dinesh Ram Kali..
Pingback: Les liens de la semaine – Édition #132 | French Coding
There goes the single responsibility principle. The fun fact is that the class still has the dependencies, more complicated perhaps, but just the same…
Pingback: Architecture and patterns | Michael's Excerpts