Saturday 21 May 2016

Dependency Inversion Principle: Part 2 - Abstraction Methods

The first activity in implementing DIP is to apply abstractions to the parts of the codes. In C# worlds, there are a couple ways to do this:
  1. Using an Interface
  2. Using an Abstract Class
  3. Using a Delegate
First, an interface is solely used to provide an abstraction, while an abstract class can also be used to provide some shared implementation details. Lastly, a delegate provides an abstraction for one particular function or method.

As a side note, it is a common practice to mark a method as virtual, so the method can be mocked when writing unit tests for the calling class. However this is not the same as applying an abstraction. Marking a method as virtual just makes it overridable, so the method can be mocked and this can be useful for testing purposes.

My preference is to use an interface for an abstraction purpose. I use an abstract class only if there is a shared implementation detail between two or more classes. Even that, I will make sure that the abstract class implements an interface for the actual abstraction. In section 1, I already give examples of applying abstractions using interfaces. In this section, I will give other examples using an abstract class and a delegate.

Using an Abstract Class

Using the example in section 1, I just need to change the interface IOperation to an abstract class, OperationBase.
public abstract class OperationBase
{
  public abstract void Send();
}

public class LowLevelModule: OperationBase
{
  public LowLevelModule()
  {
    Initiate();
  }

  private void Initiate()
  {
    //do initiation before sending
  }
  
  public void Send()
  {
    //perform sending operation
  }
}

public class HighLevelModule
{
  private readonly OperationBase _operation;

  public HighLevelModule(OperationBase operation)
  {
    _operation = operation;
  }

  public void Call()
  {
    _operation.Send();
  }
}

The above codes are equivalent to using an interface. I normally only use abstract classes if there is a shared implementation detail. For example, if HighLevelModule can use either LowLevelModule or AnotherLowLevelModule, and both classes have a shared implementation detail, then I will use an abstract class as a base class for both. The base class will implement IOperation, which is the actual abstraction.
public interface IOperation
{
  void Send();
}

public abstract class OperationBase: IOperation
{
  public OperationBase()
  {
    Initiate();
  }

  private void Initiate()
  {
    //do initiation before sending, also shared implementation in this example
  }

  public abstract void Send();
}

public class LowLevelModule: OperationBase
{
 
  public void Send()
  {
    //perform sending operation
  }
}

public class AnotherLowLevelModule: OperationBase
{
  
  public void Send()
  {
    //perform another sending operation
  }
}

public class HighLevelModule
{
  private readonly IOperation _operation;

  public HighLevelModule(IOperation operation)
  {
    _operation = operation;
  }

  public void Call()
  {
    _operation.Send();
  }
}


Using a Delegate

A single method or function can be abstracted using a delegate. Generic delegate Func<T> or Action can be used for this purpose.
public class Caller
{ 
  public void CallerMethod()
  {
    var module = new HighLevelModule(Send);
    ...
  }

  public void Send()
  {
    //this is the method injected into HighLevelModule
  }
}

public class HighLevelModule
{
  private readonly Action _sendOperation;

  public HighLevelModule(Action sendOperation)
  {
    _sendOperation = sendOperation;
  }

  public void Call()
  {
    _sendOperation();
  }
}

Alternatively, you can create your own delegate and give a meaningful name to it.

public delegate void SendOperation();

public class Caller
{ 
  public void CallerMethod()
  {
    var module = new HighLevelModule(Send);
    ...
  }

  public void Send()
  {
    //this is the method injected into HighLevelModule
  }
}

public class HighLevelModule
{
  private readonly SendOperation _sendOperation;

  public HighLevelModule(SendOperation sendOperation)
  {
    _sendOperation = sendOperation;
  }

  public void Call()
  {
    _sendOperation();
  }
}

The benefit of using generic delegates is that we don't need to create or implement a type, e.g interfaces and classes, for the dependencies. We can just use any methods or functions from the caller context or from anywhere else.

In this section, I have discussed abstraction methods using an abstract class and a delegate. Applying an abstraction using an interface has been covered in the previous section. Beside applying abstractions, the other activity in implementing DIP is to invert class dependencies. In the next section, I will discuss various dependency inversion methods.

No comments:

Post a Comment