one should “depend upon abstractions, [not] concretions.”
Dependencies are practically anything that your class controls trough another class. For example if you need to manipulate a file, you will do this through a class which is exposed by the framework you are using. Here are a few common dependencies: Database, Network, File System, System information, Time, Printing, Mailing, Timers, External Hardware, External Services, etc.
Building upon the examples we discussed so far in this series, let’s add the following requirement. Our client wants to vary the percentage of discount given based on the quantity. Since he wants to modify this later for each warehouse he requested us to read this data from a CSV file.
In other words, our class needs to process some information. That information is currently stored in a file called “C:\abc.csv”. My first instinct is to open a files stream, read the information and then process it. Remembering what we learned so far we correctly identify that, fetching the data is a different responsibility then processing it, and we learned we should have only one. It is clear we need to separate these two, so we come up with something that looks like this.
class QuantityModifierConfigEntry
{
public int Quantity { get; set; }
public double Discount { get; set; }
}
class QuantityModifierConfigFileReader
{
public IEnumerable<QuantityModifierConfigEntry> GetConfig()
{
var configuration = new List<QuantityModifierConfigEntry>();
var reader = new StreamReader("C:\abc.csv");
while (!reader.EndOfStream)
{
var line = reader.ReadLine();
var info = line.Split(';');
configuration.Add(new QuantityModifierConfigEntry()
{
Quantity = int.Parse(info[0]),
Discount = int.Parse(info[1])
});
}
return configuration;
}
}
class QuantityBasedModifier : IPaymentCalcModifier
{
public QuantityModifierConfigFileReader _configReader;
public QuantityBasedModifier
(QuantityModifierConfigFileReader reader)
{
_configReader = reader;
}
public double Calculate(Order order, double currentPrice)
{
var configs = _configReader
.GetConfig()
.OrderByDescending(x => x.Quantity); ;
foreach (var item in order.Items)
{
var d = configs
.First(x => item.Quantity >= x.Quantity)
.Discount;
currentPrice -= d * item.TotalPrice;
}
return currentPrice;
}
}
This code already looks pretty nice. You can find some improvements here and there, but overall no major issue. The important thing to notice here is that, we limited the QuantityBasedModifier to the QuantityModifierConfigFileReader. If you examine the code a bit closer you will see that QuantityBasedModifier can work with any class that has a function GetConfig with the correct signature. This is exactly what this principle stands for. Instead of requiring a concrete implementation we can request an abstraction.
interface IQuantityModifierConfigReader
{
IEnumerable<QuantityModifierConfigEntry> GetConfig();
}
class QuantityBasedModifier : IPaymentCalcModifier
{
public IQuantityModifierConfigReader _configReader;
public QuantityBasedModifier
(IQuantityModifierConfigReader reader)
{
_configReader = reader;
}
public double Calculate(Order order, double currentPrice)
{
var configs = _configReader
.GetConfig()
.OrderByDescending(x => x.Quantity); ;
foreach (var item in order.Items)
{
var d = configs
.First(x => item.Quantity >= x.Quantity)
.Discount;
currentPrice -= d * item.TotalPrice;
}
return currentPrice;
}
}
Now this is a lot better. We can work with any implementation of the IQuantityModifierConfigReader. I know the solution seems similar to the OCP solution but the reasoning behind it is different. There we wanted to make our Payment calculator extensible. Here we just want to be nonrestrictive about who should provide the data to us.
One great added benefit if your respect this principle is testability. When you write unit tests for a class you aim to place that class into various situations and examine the way it behaves. With our first solution we would have been relying on an actual file being in the correct place, resulting in limited testability. The second approach however a lot more flexible. You can give an implementation for every test case, or better yet generate one with a mocking framework, which returns hardcoded data (no files required).
But where does the inversion part come in? Well let’s look at the following diagram.
In a real life application the file reader would be in a different module than the calculation modifier. In the first version our Payment module depended upon the configuration module. In our second example it is clear that our Configuration module depends on the Payment module. So in essence the dependencies switched direction and both of our classes now depend on an abstraction. Taking this a step further we could extract our interface on a separate module on which our current modules would depend on.
This principle clearly helps you reach a better design not only on the class level but also on the module level, so give it a try. See you soon!