Dependency inversion principle

Dependency inversion principle (DIP)

High level modules mogen niet afhankelijk zijn van low level modules. Ze mogen enkel afhankelijk zijn van abstractie. 

PluralSight Course

PluralSight Course: DIP

Wat zijn dependencies?

Een dependency is iets dat misschien kan veranderen tijdens de levenscyclus van je code.

Stel je voor dat je muis, keyboard, monitor door de fabrikant aan je moederbord zijn gesoldeerd zijn. Met andere woorden, wanneer je een moederbord koopt heb je ook een muis, keyboard en monitor.
Stel je voor je met één van de devices vervangen. Je beschadigt misschien het moederboard.
In programmeren is het net hetzelfde. Het dependency Inversion principe is een manier om plugs aan je code toe te voegen zodat de high level modules (moederbord) onafhankelijk is van de low level modules (muis). De low level modules kunnen later ontwikkeld worden en zouden makkelijk vervangbaar moeten zijn.
Een indicatie die veranderingen te weeg brengen is het “new” keywoord.
Een praktische oplijsting van veel voorkomende dependencies:
– Third party library- Database- File system- Web Service- New keyword
Je zou er voor moeten zorgen dat constructors alle dependencies bevat die een klasse nodig heeft. Dit noemen we EXPLICIT DEPENDENCIES. In het andere geval noemen we het hidden dependencies.
Dependency injection is een techniek om  een dependency (afhankelijkheid) te injecteren in een klasse wanneer deze klasse ze nodig heeft.
Een voorbeeld:

class EventLogWriter
{
public void Write(string message)
{
//Write to event log here
}
}
class Printer
{
// Handle to EventLog writer to write to the logs
EventLogWriter writer = null;
// This function will be called when the app pool has problem
public void Notify(string message)
{
if (writer == null)
{
writer = new EventLogWriter();
}
writer.Write(message);
}
}

Op het eerste zicht is er niets mis met bovenstaande code. Maar eigenlijk schenden we DIP. De high level module ‘Printer’ is afhankelijk van de klasse EventLogWriter. Deze klasse noemen we een concrete klasse, en is dus geen abstracte klasse.

Het wordt duidelijker als we ook een email naar de IT admin willen sturen bij een bepaald probleem en niet enkel een log neerschrijven.
Als we een klasse schrijven voor het versturen van emails, moet de Printer klasse het juiste kunnen afhandelen, zonder dat wij concrete code implementeren. Dus nu is de Printer klasse afhankelijk van de EventLogWriter klasse, en hiervan willen we vanaf.

Onderstaande code laat zien hoe je verandering op een ondynamische manier injecteert:

class EventLogWriter
{
public void Write(string message)
{
//Write to event log here
}
}
class EmailLogWriter
{
public void Send(string message)
{
//Send email
}
}
class Printer
{

EventLogWriter writer = null;
EmailLogWriter email = null;

public void Notify(string message, string type)
{
if (type == "EventViewer")
{
if (writer == null)
{
writer = new EventLogWriter();
}
writer.Write(message);
}
if (type == "email")
{
if (email == null)
{
email = new EmailLogWriter();
}
email.Send(message);
}
}
}

Dus onze printer klasse moet een instantie van al onze loggers bijhouden. Volgens het dependency inversion principe moeten we ons systeem ontkoppelen zodat de higher level modules, dus de Printer module afhankelijk is van een abstracte klasse of interface.Deze abstractie zal gemapt worden (polymorf gedrag) naar een concrete klasse die de juiste actie zal ondernemen.

 interface INotification
{
void Notify(string message);
}

class EventLogWriter:INotification
{
public void Notify(string message)
{
}
}
class EmailLogWriter:INotification
{
public void Notify(string message)
{

}

}
class Printer
{

INotification writer;

public printer(INotification w){
writer = w;
}
public void Notify(string message)
{

writer.Notify(message);
}
}

    Op deze manier maken we de Printer klasse (High level module) onafhankelijk van de concrete log klassen.

Hoe kunnen we deze  verder ontkoppelen zodat we bij het toevoegen van andere log klassen (vb. SMS logger), de printer klasse niet meer aangepast hoeft te worden?
Dit kan je implementeren door Dependency injectie

Dependency Injection

Dependency Injection betekent dat we een concrete implementatie in een klasse injecteren, met als doel om de koppeling tussen klassen te verminderen. 

3 manieren voor Dependency injection:
– Constructor injection- Method injection- Property injection

Contructor injection

Constructor Injection :We geven het object van de concrete klasse mee met de constructor van de afhankelijke klasse.

class Printer
{
INotification writer;
public Printer(INotification logger)
{
writer = logger;
}

public void Notify(string message)
{
writer.Notify(message);
}
}
static void Main(string[] args)
{
EventLogWriter log = new EventLogWriter();
Printer p = new Printer(log);
p.Notify("dit is een test");
Console.ReadLine();
}

Method Injection

In constructor injection wordt de concrete klasse gedurende de volledige levenscyclus van de Printer gebruikt. Als je 
verschillende concrete klassen moet aanroepen, moet je deze in de methode zelf injecteren.


static void Main(string[] args)
{
EventLogWriter log = new EventLogWriter();
Printer p = new Printer();
p.Notify(log,"dit is een test");
Console.ReadLine();
}

Property Injection

We geven het object van de concrete klasse mee via een set property.

class Printer
{
// Handle to EventLog writer to write to the logs

public INotification writer { get; set; }
// This function will be called when the app pool has problem
public void Notify(string message)
{
writer.Notify(message);
}
}
 static void Main(string[] args)
{
EventLogWriter log = new EventLogWriter();
Printer p = new Printer();
p.writer = log;
p.Notify("dit is een test");
Console.ReadLine();
}

Een tweede voorbeeld

Bepaal de dependencies:

public class Order
{
public void Checkout(Cart cart, PaymentDetails paymentDetails, bool notifyCustomer)
{
if (paymentDetails.PaymentMethod == PaymentMethod.CreditCard)
{
ChargeCard(paymentDetails, cart);
}
ReserveInventory(cart);
if (notifyCustomer)
{
NotifyCustomer(cart);
}
}
public void NotifyCustomer(Cart cart)
{
string customerEmail = cart.CustomerEmail;
if (!String.IsNullOrEmpty(customerEmail))
{
using (var message = new MailMessage("orders@somewhere.com", customerEmail))
using (var client = new SmtpClient("localhost"))
{
message.Subject = "Your order placed on " + DateTime.Now;
message.Body = "Your order details: n " + cart;
try
{
client.Send(message);
}
catch (Exception ex)
{
Logger.Error("Problem sending notification email", ex);
throw;
}
}
}
}
public void ReserveInventory(Cart cart)
{
foreach (OrderItem item in cart.Items)
{
try
{
var inventorySystem = new InventorySystem();
inventorySystem.Reserve(item.Sku, item.Quantity);
}
catch (InsufficientInventoryException ex)
{
throw new OrderException("Insufficient inventory for item " + item.Sku, ex);
}
catch (Exception ex)
{
throw new OrderException("Problem reserving inventory", ex);
}
}
}
public void ChargeCard(PaymentDetails paymentDetails, Cart cart)
{
using (var paymentGateway = new PaymentGateway())
{
try
{
paymentGateway.Credentials = "account credentials";
paymentGateway.CardNumber = paymentDetails.CreditCardNumber;
paymentGateway.ExpiresMonth = paymentDetails.ExpiresMonth;
paymentGateway.ExpiresYear = paymentDetails.ExpiresYear;
paymentGateway.NameOnCard = paymentDetails.CardholderName;
paymentGateway.AmountToCharge = cart.TotalAmount;
paymentGateway.Charge();
}
catch (AvsMismatchException ex)
{
throw new OrderException("The card gateway rejected the card based on the address provided.", ex);
}
catch (Exception ex)
{
throw new OrderException("There was a problem with your card.", ex);
}
}
}
}

De dependencies opgelijst:

– MailMessage- SmtpClient- Inventory- PaymentGateway
Hoe moeten we dit refactoren?
1. Dependencies in interfaces stoppen2. Injecteer de implementatie van deze interface in de order klasse3. Zorg voor Single Responsible principle
Het toepassen van dependency injection zorgt typisch voor heel wat interfaces die ergens moeten geinstantierd worden. Dit doen we meestal in de default constructor of in de Main (de startup routine van je applicatie)
De MailMessage en SmtpClient zorgt voor een eventuele  verandering. Stel je wil later de klant niet via email een notificatie sturen, maar via facebook messenger, dan zal je deze code moeten aanpassen. Door dit in interface te duwen, zal je veel flexibelere code schrijven:

 public interface INotifyCustomer
{
void NotifyCustomer(Cart cart);
}
  public class NotifyCustomerService : INotifyCustomer
{
/**
* Method Notifies the customer via Email
* @param cart a cart object to mail all cart details
*/
public void NotifyCustomer(Cart cart)
{
string customerEmail = cart.CustomerEmail;
if (!String.IsNullOrEmpty(customerEmail))
{
using (var message = new MailMessage("orders@somewhere.com", customerEmail))
using (var client = new SmtpClient("localhost"))
{
message.Subject = "Your order placed on " + DateTime.Now;
message.Body = "Your order details: n " + cart;
try
{
client.Send(message);
}
catch (Exception ex)
{
Logger.Error("Problem sending notification email", ex);
throw;
}
}
}
}
}

In de order klasse zie je de flexibiliteit terugkomen:

public class Order
{
INotifyCustomer _notifier;

public Order(INotifyCustomer notification)
{
_notifier = notification;

}
public void Checkout(Cart cart, PaymentDetails paymentDetails, bool notifyCustomer)
{
if (notifyCustomer)
{
_notifier.NotifyCustomer(cart);
}
}

Op deze manier moet de Order klasse niet weten of we een email notificatie sturen, en push notification voor mobile phone, een facebook message, …