Following on from a previous blog about Inversion of Control, I intend to continue by discussing the patterns identified in that article as being IoC patterns, or rather implementation patterns which enable the application of the IoC concept. Specifically in this post we will be looking at the Dependency Injection pattern, which will be followed up by a post on the Service Locator anti-pattern.
What is the Dependency Inversion Principle?
The dependency inversion principle refers to a specific form of decoupling software modules. When following this principle, the conventional dependency relationships established from high-level, policy-setting modules to low-level, dependency modules are inverted (i.e. reversed), thus rendering high-level modules independent of the low-level module implementation details.
I feel that Dependency Inversion was adequately discussed in my previous blog (which can be found here) and I will therefore delve no deeper into this principle now, but rather I will turn our attention to a means of accomplishing said inversion of dependencies.
What is Dependency Injection
Dependency injection (DI) is a software design pattern that implements inversion of control for resolving dependencies. The salient points about this statement are
- A dependency is an object that can be used (a service).
- An injection is the passing of a dependency to a dependent object (a client) that would use it.
The client delegates the responsibility of providing its dependencies to external code. The client does not call the injector code, but rather it is the injecting code that both constructs the services and calls the client to inject those services. This means that the client does not need to know about the injecting object. The client does not need to know how the services are constructed, it does not need to know even which actual services it is using. The client only needs to know about the interfaces of the services, because these define the usage of those services.
This pattern separates the responsibilities of use and construction, and as a biproduct we are also enabled with a greater deal of control over the testing of the client, of the services, and of the injector. Through the use of this pattern we can cleanly test individual units in isolation, through perhaps the use of a Mocking Framework, for example.
A tightly Coupled Example: hopefully very similar to something we might find in the real world
For the purpose of this discussion, we will imagine a fairly simple scenario within which we have a boiler (responsible for bringing water to a given temperature), and a thermostatic controller which talks to the boiler telling it what temperature the water needs to be adjusted to.
To reduce the distraction of how a boiler actually works we will present the caveat that our boiler is very clever and it can raise and lower the temperature of the water -don't ask how, just accept that it can.
An initial implementation of this relationship might resemble the following (ignoring the argument of whether the thermostat belongs to the boiler or vice versa)
1 public class Boiler 2 { 3 public int Temperature { get; private set; } 4 public bool AdjustTemperatureBy(int temperatureChange) { } 5 } 6 7 public class Thermostat 8 { 9 public bool SetTemperatureTo(int temperature) 10 { 11 var b = new Boiler(); 12 var current = b.Temperature; 13 return b.AdjustTemperatureBy(temperature - current); 14 } 15 }
In summary:
- A thermostat is instructed to set a temperature
- The Thermostat creates a Boiler instance
- The Thermostat determines the current temperature in the boiler
- The Boiler's temperature is adjusted accordingly
Because the Thermostat is creates a new Boiler instance (line 11), it needs to know how a Boiler is constructed; and because we can imagine the same kind of "problem" should the roles be reversed and the Thermostat belongs to the Boiler (i.e. the Boiler would need to know how to construct a Thermostat), from this point on I will cease to make mention of the possibility of an alternate relationship between the two classes and instead just consider the pattern in relation to the model we are working with.
What happens if the constructor for the Boiler class requires various data, for example water pressure, which are simply not available to the Thermostat?
In such a scenario, should we then pollute our Thermostat class with these data? It would seem quite illogical because there is no reason for a thermostat to know anything about water pressure. Clearly then, the Thermostat cannot be responsible for constructing the Boiler. Also, when we consider that the Thermostat will have to work with different types of boiler, it is plain that this implementation will not suffice at all. Because the Thermostat creates the Boiler instance ...
- The Thermostat is tightly coupled to Boiler
- The Thermostat code would need changing to accommodate different types of boiler
Let's look at how we might adjust the implementation so that the two classes are no longer tightly coupled.
Break the ties which bind ...
First we might introduce an interface for boilers, so that their function can be clearly defined without knowledge of any specific implementaiton.
public interface IBoiler { int Temperature { get; } bool AdjustTemperatureBy(int temperatureChange); }
This interface tells us that every boiler will provide us with a means of ascertianing the current water temperature, and a means of adjusting that temperature by a desired value. We are also aware of combi-boilers available on the market, and because not all boilers are of the combi- variety, we will provide another interface to describe said combi-boiler. A combi-boiler provides all funcationality of a boiler, with some added extras.
public interface ICombiBoiler : IBoiler { bool AdjustValve(double adjustment); }
Along with the interfaces, we have also identified the need for some representation of Pressure which will be used in the instantiation of our boilers.
public struct Pressure { }
... and finaly we have some concrete classes: a couple of boiler implementations of which we are aware. The first is a Combi- variety, and the second is a regular, run of the mill, Boiler.
public class Honeywell : ICombiBoiler { public Honeywell(Pressure waterPressure) { /* ... */ } public int Temperature { get; private set; } public bool AdjustTemperatureBy(int temperatureChange) { /* ... */ } public bool AdjustValve(double adjustment) { /* ... */ } } public class ImmersionHeater : IBoiler { public int Temperature { get; private set; } public bool AdjustTemperatureBy(int temperatureChange) { /* ... */ } }
Interfaces extracted! ... has it helped?
So far we have just introduced a bunch of stuff which we were quite happily progressing without. We've got two new interfaces, a struct and a couple of boilers. All we needed were the two boilers! What gives?
In order to answer "what gives?", let us look at how we might have previously implemented a HoneywellThermostat
1 public class HoneywellThermostatA 2 { 3 public bool SetTemperatureTo(int temperature) 4 { 5 var b = new Honeywell(????); 6 var current = b.Temperature; 7 return b.AdjustTemperatureBy(temperature - current); 8 } 9 }
This first attempt, quite strictly follows the paradigm from the initial Thermostat implementation, and as you can see, at line 5 we encounter the very issue identified several paragraphs earlier. From whence does the Pressure object come which we need in order to instantiate the boiler? We cannot pass it in as an argument to the SetTemperatureTo() because, as can be seen from our two concrete boiler classes, not every boiler requires this Pressure object. The Pressure object has no bearing on the setting of temperature and therefore it makes no sense to be a parameter.
It is true however, that the thermostat is dependent upon Honeywell, and Honeywell is dependent upon Pressure, so what are our options?
We could perhaps alleviate this problem by having the boiler instantiated during object initialization; this way the Honeywell will be readily available for use from within the SetTemperatureTo() method when the time comes.
1 public class HoneywellThermostatB 2 { 3 Honeywell _boiler; 4 public HoneywellThermostatB() 5 { 6 _boiler b = new Honeywell(????); 7 } 8 }
But as we can see from line 6, all we have achieved is to move the problem out from the SetTemperatureTo() method and into the constructor. Perhaps, as in HoneywellThermostatC (below), we could declare Pressure as a constructor parameter?
1 public class HoneywellThermostatC 2 { 3 Honeywell _boiler; 4 public HoneywellThermostatC(Pressure waterPressure) 5 { 6 _boiler b = new Honeywell(waterPressure); 7 } 8 }
But again, we find ourselves in pretty much the same situation as when we started; we have a thermostatic controller which, although it has no connection nor even any concern with water pressure, needs to know about water pressure (line 4). This erroneous need is driven solely by the boiler needing to know about water pressure (line 6).
This is a clear violation of The Law of Demeter - an object must not know about the internal workings of a service upon which it has a dependency. Even if this were not true, we would still have an object which maintained a reference to another object for which it has no use. How can that not sound wrong? The next logical step in resolving this issue leads us directly into Dependency Injection.
This is Dependency Injection
The reader should, without further guidance, be able to devise HoneywellThermostatD such that a Honeywell object is the constructor parameter rather than a Pressure object. The reader should also notice that because the boiler is passed in to the constructor, there is no longer any need for separate HoneywellThermostat and ImmersionThermostat implementations, and instead we can declare a single thermostat class which is able to work with any boiler implementing the IBoiler interface, i.e. change the constructor parameter to type IBoiler.
Next I am going to offer three solutions, each of which would be preferred under different usage conditions (requirements, or Stories).
1 public class ThermostatA 2 { 3 private readonly IBoiler _boiler; 4 public ThermostatA(IBoiler boiler) { _boiler = boiler; } 5 public bool SetTemperatureTo(int temperature) 6 { 7 var adjustment = temperature - _boiler.Temperature; 8 return _boiler.AdjustTemperatureBy(adjustment); 9 } 10}
This is pretty much the implementation to which we were previously led, and we can imagine this class under the following conditions
- A building contains 1…n boilers
- For each boiler, there is a corresponding thermostatic controller
The class below models these conditions. The building controller at some point invokes the AttachThermostats() method which itself binds a new ThermostatA instance to each IBoiler for which the controller is responsible. SetTemperature() can be invoked at any point in the future
public class BuildingControlA { private IEnumerable<ThermostatA> _thermostats; private void AttachThermostats(IEnumerable<IBoiler> boilers) { _thermostats = boilers .Select(b => new ThermostatA(b)).ToArray(); } public void SetTemperature(int temperature) { for each(thermostat in _thermostats) { t.SetTemperatureTo(temperature); } } }
The second implementation could be used in a situation where
- A building contains 1…n boilers
- There is no requirement for a separate thermostatic unit for each boiler
With this implementation, the thermostat is notified of both the temperature to set and the boiler concerned, in the call to SetTemperatureTo(). This scenario is modelled in ThermostatB and its associated BuildingControl.
public class ThermostatB { public bool SetTemperatureTo(IBoiler boiler, int temperature) { var adjustment = temperature - boiler.Temperature; return boiler.AdjustTemperatureBy(adjustment); } }
public class BuildingControlB { private ThermostatB _thermostat; private IEnumerable<IBoiler> _boilers; public void SetTemperature(int temperature) { foreach(boiler in _boilers) { _thermostat.SetTemperatureTo(boiler, temperature); } } }
The SetTemperatureTo() method is invoked on the ThermostatB instance, once for each IBoiler for which the controller is responsible.
One could imagine for this implementation that along with the IBoiler argument, the temperature argument could also be different for each call made to SetTemperatureTo(), so that all IBoilers need not be set to the same temperature (perhaps one boiler is for a swimming pool, whilst another is for tap water, and another still is for a separate heating system). In this case, the SetTemperature() method of BuildingControlB would be more elaborate in its inner workings and would also require a parameter representing a series of temperature values.
The third implementation might come into being if there were multiple steps involved in performing a temperature change, such as might be the case for our Honeywell, but not for our ImmersionHeater -remember that Honeywell implements ICombiBoiler.
Now is probably a good time to query the inclusion of the ICombiBoilerinterface as a distinct interface from IBoiler. This is of course our adherence to the I of the SOLID principles, which states that
many client-specific interfaces are better than one general- purpose interface
Where there are multiple operations to be performed, ThermostatC should cache the IBoiler between calls, and to this end we introduce a Fluent-like interface.
public class ThermostatC { private IBoiler _boiler; public int CurrentTemperature { get; private set; } public ThermostatC For(IBoiler boiler) { _boiler = boiler; return this; } public bool SetTemperatureTo(int temp, double valve) { var adjustment = temp - _boiler.Temperature; return _boiler.AdjustTemperatureBy(adjustment) && ((!(_boiler is ICombiBoiler)) || ((ICombiBoiler) _boiler) .AdjustValve(valve)); } public bool ReadTemperature() { CurrentTemperature = _boiler.Temperature; return true; } }
BuildingControlC (below) models the usage of ThermostatC. For some reason, when setting the temperature for a CombiBoiler, it is imperative that the thermostat reads the temperature back from the boiler -perhaps the CombiBoilers are renowned for not having a reliable temperature change mechanism.
public class BuildingControlC { private double _successRate; private IEnumerable<IBoiler> _boilers; private ThermostatC _thermostat; public void SetTemperature(int temperature, double adjustment) { foreach(boiler in _boilers) { var result = _thermostat.For(boiler) .SetTemperatureTo(temperature, adjustment); if(result && (boiler is ICombiBoiler)) _thermostat.ReadTemperature(); } } }
Summing it all up
Although each solution above is quite different, and will come into being under different circumstances, they all have one thing in common; they all implement the Dependency Injection pattern.
Except for the following line in BuildingControlA there is not a single object being "new-ed up".
_thermostats = boilers
.Select(b => new ThermostatA(b)).ToArray();
Further, since the focus here is on the boiler and thermostat implementations rather than the example consumers (the BuildingCotrol classes) I would request that this instance be considered as to not exist (maybe this is a tall order and I might have to do some further explaining, but I would rather leave that to a later post which will discuss details of Dependency Injection as opposed to what Dependency Injection is).
ThermostatA clearly demonstrates Dependency Injection. The class has a dependency on the IBoiler interface, but the realisation, instantiation, and initialization are all taken care of somewhere else and by something else. However, let us not concern ourselves at this time with that somewhere or that something, and instead consider that the lifetime of IBoiler is managed externally to ThermostatA.
ThermostatB might look to have the same "issue" as passing a Pressure object to the SetTemperatureTo() method, but this implementation is completely different: In this instance we are not passing a seemingly unrelated argument, and the method does not use that parameter to initialize a boiler instance of some kind, instead the lifetime of the IBoiler is managed externally to ThermostatB. Incidentally, the IBoiler can be passed to the method because it (the IBoiler) is the object being acted upon; we are saying to the thermostat, "for this boiler, set the temperature to n", and more to the point, the boiler instance should be passed to the thermostat because ThermostatB can work with any boiler which implements the IBoiler interface.
Much like ThermostatA, Dependency Injection is clearly implemented in ThermostatC; Prior to invoking the SetTemperatureTo() method, we explicitly hand to the thermostat the IBoiler instance that it should be concerned with. The creation and lifetime of the IBoiler which ThermostatC is controlling are completely external to the thermostat instance.
For each of the thermostat implementations (A through C), the corresponding building controller injects the dependencies. Dependency Injection really is that simple . Which kind of raises the question of why was this blog so long?!
There are however different aspects to Dependency Injection, for example there are a variety of tools available such as Spring, NInject, Castle Windsor, Unity, etc. (erroneously known as IOC Containers) to help in constructing an application utilizing the Dependency Injection Pattern. We have mostly considered method injection in this article, and briefly we touched upon, without further discussing, Constructor injection (ThermostatA implementation, line 4).
There is in fact quite a lot to discuss when considering the Dependency Injection Pattern, but this blog has run for quite long enough so I will finish here and carry on in another post.
On the Horizon:
- Different forms of DI
- DI Containers
- Convention, configuration, and code
- Service Locator anti-pattern
No comments:
Post a Comment