Dependency Injection is Loose Coupling by Mark Seemann
It seems to me that I've lately encountered a particular mindset towards Dependency Injection (DI). People seem to think that it's only really good for replacing one data access implementation with another. Once you get to that point, you know that the following argument isn't far behind:
“That's all well and good, but we know for certain that we will never exchange [insert name of RDBMS here] with anything else in this application.”
Apart from the hubris of making such a bold statement about the future of any software endeavor, such a statement reveals the narrow view on DI that its only purpose is for replacing data access components - and perhaps for unit testing.
Those are relevant reasons for using DI, but they are only some of the reasons. Let's briefly revisit why we employ DI.
We use DI to enable loose coupling.
DI is only a means to an end. Even if you never intend to replace your database and even if you never want to write a single unit test, DI still offers benefits in form of a more maintainable code base. The loose coupling gives you better separation of concerns because it allows you to apply the Open/Closed Principle.
Example coming right up:
Imagine that we need to implement a PrécisViewModel class with a TopSellers property that returns an IEnumerable<string>. To implement this class, we have a data access component. Let's use the ubiquitous Repository pattern and define IProductRepository to see where that leads us:
public interface IProductRepository { IEnumerable<Product> SelectTopSellers(); }
We can now implement PrécisViewModel like this:
public class PrécisViewModel { private readonly IProductRepository repository; public PrécisViewModel(IProductRepository repository) { if (repository == null) { throw new ArgumentNullException("repository"); } this.repository = repository; } public IEnumerable<string> TopSellers { get { var topSellers = this.repository.SelectTopSellers(); return from p in topSellers select p.Name; } } }
Nothing fancy is going on here. It's just straight Constructor Injection at work.
Obviously, we can now implement and use a SQL Server-based repository:
var repository = new SqlProductRepository(); var vm = new PrécisViewModel(repository);
So what does all this loose coupling buy us? It doesn't seem to help us a lot.
The real benefit is not yet apparent, but it should become more obvious when we start adding requirements. Let's start with some caching. It turns out that the SelectTopSellers implementation is slow, so we would like to add some caching somewhere.
Where should we add this caching functionality? Without loose coupling, we would more or less be constrained to adding it to either PrécisViewModel or SqlProductRepository, but both have issues:
- First of all we would be violating the Single Responsibility Principle (SRP) in both cases.
- If we implement caching in PrécisViewModel, other consumers of the SelectTopSellers would not benefit from it.
- If we implement caching in SqlProductRepository, it wouldn't be available for any other IProductRepository implementations.
Since the premise for this post is that we will never use any other database than SQL Server, implementing caching directly in SqlProductRepository sounds like the correct choice, but we would still be violating the SRP, and thus making our code more difficult to maintain.
A better solution is to introduce a caching Decorator like this one:
public class CachingProductRepository : IProductRepository { private readonly ICache cache; private readonly IProductRepository repository; public CachingProductRepository( IProductRepository repository, ICache cache) { if (repository == null) { throw new ArgumentNullException("repository"); } if (cache == null) { throw new ArgumentNullException("cache"); } this.cache = cache; this.repository = repository; } #region IProductRepository Members public IEnumerable<Product> SelectTopSellers() { return this.cache .Retrieve<IEnumerable<Product>>("topSellers", this.repository.SelectTopSellers); } #endregion }
For completeness sake is here the definition of ICache:
public interface ICache { T Retrieve<T>(string key, Func<T> readThrough); }
The point is that CachingProductRepository extends any IProductRepository we provide to it (including SqlProductRepository) without modifying it. Thus, we have satisfied both the OCP and the SRP.
Just to drive home the point, let us assume that we also wish to record execution times for various methods for purposes of SLA compliance. We can do this by introducing yet another Decorator:
public class PerformanceMeasuringProductRepository : IProductRepository { private readonly IProductRepository repository; private readonly IStopwatch stopwatch; public PerformanceMeasuringProductRepository( IProductRepository repository, IStopwatch stopwatch) { if (repository == null) { throw new ArgumentNullException("repository"); } if (stopwatch == null) { throw new ArgumentNullException("stopwatch"); } this.repository = repository; this.stopwatch = stopwatch; } #region IProductRepository Members public IEnumerable<Product> SelectTopSellers() { var timer = this.stopwatch .StartMeasuring("SelectTopSellers"); var topSellers = this.repository.SelectTopSellers(); timer.StopMeasuring(); return topSellers; } #endregion }
Once again, we modified neither SqlProductRepository nor CachingProductRepository to introduce this new feature. We can implement security and auditing features by following the same principle.
To me, this is what loose coupling (and DI) is all about. That we can also replace data access components and unit test using dynamic mocks are very fortunate side effects, but the loose coupling is valuable in itself because it enables us to write more maintainable code.
We don't even need a DI Container to wire up all these repositories (although it sure would could be helpful). Here's how we can do it with Pure DI:
IProductRepository repository = new PerformanceMeasuringProductRepository( new CachingProductRepository( new SqlProductRepository(), new Cache() ), new RealStopwatch() ); var vm = new PrécisViewModel(repository);
The next time someone on your team claims that you don't need DI because the choice of RDBMS is fixed, you can tell them that it's irrelevant. The choice is between DI and Spaghetti Code.
Comments
Btw, i never figured out if there is anything why service locator isn't anti pattern. :)
I'm not sure I understand your comment regarding Service Locator. It is an anti-pattern :)
No, seriously, I never expected the entire world to just accept my word as gospel, and there are many people who disagree on this point. Did you have something specific in mind?
For me the interesting thing about the talk was that apparently this "separation of concerns" thing had been an important enough discovery for him that it warranted the use of half the presentation time to explain, with the other half being spent talking about the dangers of multi-threading :-)
A little off-topic, but how'd you implement Cache to force evaluation if it gets passed a Func<IEnumerable<Something>>? Else it will just cache the query.
However, we could always specialize the implementation of the cache so that if T was IEnumerable, we'd invoke ToList() on it before caching the result.
Thanks Mark. Any books from you scheduled for this year or the next?
What if IProductRepository has 15 methods but only one method should be cached?
Or what if I don't need always the cache? So I have a ProductService that needs a IProductRepository. For 5 cases the ProducrtService would need the CachingProductRepository and for the rest the standard ProductRepository?
However, if you have this scenario, could it be that the interface violates the Interface Segregation Principle?