Inom objekt-orienterad programmering finns det väldigt många koncept och principer. Dependency Inversion Principle (DIP) är en av dem och används ofta när man talar om Dependency Injection och Inversion of Control containers. Men man behöver inte använda en container så som Unity, Castle Windsor, Autofac eller Ninject för att få använda sig av DIP.
Principen med dependency inversion säger helt enkelt att vi skall kasta om de beroenden som vår kod har. För att inte göra en parafras så citerar jag här på engelska direkt från en artikel om DIP hos Object Mentor (PDF):
- High level modules should not depend upon low level modules. Both should depend upon abstractions.
- Abstractions should not depend upon details. Details should depend upon abstractions.
Så som alla goda principer inom objekt-orienterad programmering är själva språket eller plattformen man kodar på totalt oviktig. Principen med dependency inversion fungerar lika bra om du sitter och kodar i Java, .NET eller något annat. Däremot kan DIP anses vara lite vagt formulerad för oss som just nu sitter på .NET-plattformen. Vad betyder det här med moduler? Det närmsta vi skulle komma är nog assemblies. Och vad är det för abstraktioner vi vill göra egentligen?
Poängen med dependency inversion principle är att vi vill ha kod som är lätt att ändra på. Vissa kanske skulle anse att dålig kod är något som t ex konkatenering av strängar. Visst varje gång vi konkatenerar strängar så slängs objekt till garbage collector och StringBuilder är kanske att föredra. Men det är inte alltid dåligt att konkatenera strängar. Bra kod är kod som man tittar på utan att ta en djup suck och funderar över var man skall börja ändra någonstans eller hur mycket som kommer gå fel när man ändrar i den. Om du känner att det kommer bli jobbigt att ändra på en bit kod, att det blir oanade följder när en bit kod ändras eller att koden är omöjlig att återanvända då är något fel.
Säg att vi har en bit kod som ser ut såhär:
public class CurrencyConverter
{
public decimal Convert(decimal amount, Currency fromCurrency, Currency toCurrency)
{
var client = new CurrencyConvertorSoapClient();
decimal rate = client.ConversionRate(fromCurrency, toCurrency);
return rate * amount;
}
}
Så vad är det som kan vara fel med den här lilla biten kod? Vad den gör är ju ganska självklart. Tar in ett belopp och konverterar från en valuta till en annan med hjälp av en webbservice. Väldigt enkelt.
När vi kodar har vi oftast två koncept eller mått för vad vi vill uppnå. Cohesion och Coupling. Med cohesion menas hur sammanhållen koden är och att den bara har ett väldefinerat ansvar. Koden ovan har flera olika ansvar, den hämtar både växlingskursen och räknar ut beloppet. Vad vi vill ha är hög sammanhålling (high cohesion). Koden har också en stark koppling (coupling) till en webbservice. Vad händer om vi vill hämta växelkursen från någon annan källa. En databas eller så kanske den kommer in till systemet från fil. Det går heller inte att enhetstesta koden. Växelkursen ändras hela tiden och att gå mot en webbservice tar oftast ganska lång tid. Vi har inte uppnått low coupling.
Att fixa detta är faktiskt väldigt enkelt. Genom att kasta om våra beroenden kan vi refaktorisera om koden så att den uppnår detta.
public interface IExchangeRateRetriever
{
decimal GetExchangeRate(Currency fromCurrency, Currency toCurrency);
}
public class WebServiceExchangeRateRetriever : IExchangeRateRetriever
{
public decimal GetExchangeRate(Currency fromCurrency, Currency toCurrency)
{
var client = new CurrencyConvertorSoapClient();
return client.ConversionRate(fromCurrency, toCurrency);
}
}
public class CurrencyConverter
{
private IExchangeRateRetriever _retriever;
public CurrencyConverter(IExchangeRateRetriever retriever)
{
_exchangeRateRetriever = retriever;
}
public decimal Convert(decimal amount, Currency fromCurrency, Currency toCurrency)
{
decimal rate = _retriever.GetExchangeRate(fromCurrency, toCurrency);
return rate * amount;
}
}
Vad vi har gjort är helt enkelt att bryta ner vår klass CurrencyConverter till dess minsta beståndsdelar. Nu har den bara hand om att räkna ut beloppet och ingenting annat. Vi har skapat ett kontrakt (interface) som vi använder oss av. Genom att CurrencyConverter nu måste ta emot ett objekt som uppfyller det här kontraktet i konstruktorn så använder vi oss av constructor injection. Vi kodar mot ett gränssnitt och inte implementationen genom att vi använder oss av dependency inversion principle. Vi har också uppnåt high cohesion och low coupling. Det går nu också att enhetstesta koden genom att t ex bara skapa en fejkad klass som uppfyller kontraktet IExchangeRateRetriever.
public class FakeExchangeRateRetriever : IExchangeRateRetriever
{
public decimal GetExchangeRate(Currency fromCurrency, Currency toCurrency)
{
return 0.1507;
}
}
Dependency injection handlar om att bryta ut saker som inte skall förhålla sig till varandra. Koden för att konvertera beloppet skall inte ha något med växlingskursen att göra. Det är nu också oerhört enkelt att ändra på koden som så skulle behövas. Det enda problemet som kvarstår nu är hur vi får in beroenden till klassen nu. Detta är något som Inversion of Control containers löser och det sparar vi till ett annat inlägg!