Design principles and testing – part 3
Let’s return to our weather data from part 1. In it’s current state it looks something like this
So the weather service is using YrNoWeatherRepository to load some xml and then a mapper to map from that xml to properties on the WeatherData class. Let’s ignore the mapper for a second and talk about the repository and it’s relation to the service.
If we look at the code for the service it’s fairly obvious that it has a dependency to repository in a way that you can’t create a WeatherService class without also creating an instance of the repository class.
So what’s the problem with this?
Imagine that we want to change our repository implementation to some other weather service. Since our weather service in it’s current state not only care about the ability to fetch weather data but also finding which class and how it handles that, we have to edit our service to change the implementation.
And our new friend Dependency Inversion Principle (DIP) would not like that, not one bit. Like this motivational picture so elegantly states, designed applications this way is like soldering a lamp directly to the electrical wiring in the wall. I think most would agree that it would make more sense if you (the user of the electrical wiring) was responsible for plugging something in instead.
DIP states that “High-level modules should not depend on low-level modules. Both should depend on abstractions.” Another way to put it is that you should program to an interface and not an implementation. (Note here that “interface” in that sentence talks about the concept of interface, not the c# type interface (eg an abstract class can also be an interface).)
So let’s try to break away our high-level (WeatherService) object dependency to the lower level (YrNoWeatherRepository) one.
Refactoring to better comply to DIP
We’ll start with extracting an interface from YrNoWeatherRepository which it will implement itself.
1: public interface IWeatherRepository
2: {
3: XElement Load();
4: }
1: public class YrNoWeatherRepository : IWeatherRepository
2: {
3: public XElement Load()
4: {
5: return XElement.Load("http://www.yr.no/place/Sweden/Stockholm/Stockholm/forecast.xml");
6: }
7: }
The point here is that our service should only program to that interface. So how do we make that happen?
1: public WeatherData GetWeatherData()
2: {
3: IWeatherRepository repository = new YrNoWeatherRepository();
4: return WeatherDataMapper.FromYrNoXml(repository.Load());
5: }
This doesn’t give us much, sure we program against the interface rather than a concrete implementation, but it’s still as dependent on the concrete class as it was when we begun.
Dependency injection (DI) and Inversion of Control (IoC)
What we want to achieve is that the service class shouldn’t be responsible for resolving it’s dependencies but rather being told about them. This is what Dependency Injection is all about. You might also hear people talk about the Hollywood principle (“don’t call us, we’ll call you”). So in essence what this means is that we should tell the service which concrete implementation to use rather then letting it decide for itself. In a way this is the complete opposite to how you normally design your application (or at least it was when it was introduced in 1988) which is why it’s called Inversion of Control (ioC). You could say that DI is a way to achieve IoC.
Injecting dependencies
The first (and I think most widely used) approach is to inject the dependencies in the objects constructor, called Constructor injection.
1: private readonly IWeatherRepository weatherRepository;
2:
3: public WeatherService(IWeatherRepository weatherRepository)
4: {
5: this.weatherRepository = weatherRepository;
6: }
7:
8: public WeatherData GetWeatherData()
9: {
10: return WeatherDataMapper.FromYrNoXml(weatherRepository.Load());
11: }
Notice that the GetWeatherData method only talks to the repository field which is of the type IWeatherRepository.
If we try to build this we’ll get errors from all the places that currently uses the WeatherSerivce since we by doing this removed the default constructor which all current code uses. To solve this there are (at least) three approaches of which we’ll speak of two now and one later on.
1. Poor mans dependency injection
The quickest way is to simply add a default constructor that passes in the concrete class we want to use
1: public WeatherService() : this(new YrNoWeatherRepository())
2: {
3: }
4:
5: public WeatherService(IWeatherRepository weatherRepository)
6: {
7: this.weatherRepository = weatherRepository;
8: }
As you notice this leads to our class being as dependent to the concrete class as it was when we begun. This is called poor mans dependency injection for this reason and some even consider it somewhat of an anti-pattern. But it can be a real life-save (or at least the only viable option) when dealing with a legacy app that you’re rewriting to (for instance) make more testable.
2. Using a factory
The other option is to remove the default constructor and change every occurrence to inject the concrete class like such
1: new WeatherService(new YrNoWeatherRepository())
If we have a lot of these in our code base it can be somewhat of a pain to update them (even with ReSharper). Plus if we change which implementation to use we have to once again edit all the occurrences. One thing we could to is to hide the initiation behind a factory (yes I remember that I said no design patterns…)
1: public class ServicerFactory
2: {
3: public static WeatherService GetWeatherService()
4: {
5: return new WeatherService(new YrNoWeatherRepository());
6: }
7: }
And when we want a service we simply call our ServiceFactory
1: var weatherData = ServiceFactory.GetWeatherService().GetWeatherData();
So what about the third option? And when is this “testing” starting?
The third option will be discussed in next post where we’ll also talk more about what the benefits of doing this are, what an IoC-container can do for us and then (finally) start testing our application. Cliffhanger alert!
If you feel this series doesn’t really make sense, have questions or ideas about what it also should mention or have any other feedback please leave a comment or drop me a mail at stefan.forsberg at cloudnine . se.
They sure do make sense :) Nice writing and presented in an easy to understand fashion. I think a lot of people will enjoy these posts, I know I do :)
Keep up the good work!
/ Bjørn Isaksen
Thanks for your kind words! =)
/ Stefan Forsberg