Monday, July 11, 2011

The Single Responsibility Principle

In my previous post I introduced the S.O.L.I.D. Object Oriented Design Principles and the problems of the design that they solve. I would like to start with the first of them which is the Single Responsibility principle as it is quite simple to understand, but at the same time, it is the most difficult to implement.

It is very common, specially in legacy code, that we can find classes whose interface does "too much," or even worse, we define the "god class" where we dump a big part of the implementation of an application. A major problem is that when we need to update or extend functionality, specially when working in a team, it is the same single class that is modified over and over.

Responsibility: A reason for change.

The Single Responsibility principle targets cohesion. There should never be more than ONE reason for a class to change.

Each responsibility is an axis of change. If a class has more than one responsibility, then they become coupled. Changes to one responsibility may impair or inhibit the class' ability to meet the others.

In the following UML class diagrams we can picture this principle:

A class AB implements two responsibilities: A and B.

By separating responsibility B from A, it is clear for the class C what instance is the information expert to provide each one of the responsibilities.


Code sample:
Despite the following code is in C# it can be easily ported to Java or other OOP language.


Wrong implementation of this principle: 

   1:  interface IModem
   2:  {
   3:      void Dial(String number);
   4:      void Hangup();
   5:      void Send(char c);
   6:      char Receive();
   7:  }
   8:   
   9:  class Modem : IModem
  10:  {
  11:      Random random = new Random((int)DateTime.Now.Ticks);
  12:   
  13:      public void Dial(String number)
  14:      {
  15:          Console.WriteLine(
  16:              string.Format("Modem is dialing number {0}", number));
  17:      }
  18:   
  19:      public void Hangup()
  20:      {
  21:          Console.WriteLine("Call finished");
  22:      }
  23:   
  24:      public void Send(char c)
  25:      {
  26:          Console.WriteLine(string.Format("Sending {0}...", c));
  27:      }
  28:   
  29:      public char Receive()
  30:      {
  31:          return (char)(random.Next() % 256);
  32:      }
  33:  }
We can observe that the interface IModem is implementing both roles at the same time; the role as a channel which is in charge of sending and receiving data over the wire, and the role as connection that deals with dialing and closing the communication.

Good implementation of this principle:

   1:  interface IDataChannel
   2:  {
   3:      void Send(char c);
   4:      char Receive();
   5:  }
   6:   
   7:  class DataChannel : IDataChannel
   8:  {
   9:      Random random = new Random((int)DateTime.Now.Ticks);
  10:   
  11:      public void Send(char c)
  12:      {
  13:          Console.WriteLine(string.Format("Sending {0}...", c));
  14:      }
  15:   
  16:      public char Receive()
  17:      {
  18:          return (char)(random.Next() % 256);
  19:      }
  20:  }
The responsibility of sending and receiving data over the wire is defined by the IDataChannel interface and is implemented by the class DataChannel. By giving a single responsibility to this class, we can easily maintain any change in its implementation as this provides low coupling, also it works organically with other classes that requires a data channel as the interface improves cohesion between classes.

   1:  interface IConnection
   2:  {
   3:      void Dial(string number);
   4:      void Hangup();
   5:  }
   6:   
   7:  class Connection : IConnection
   8:  {
   9:      public void Dial(string number)
  10:      {
  11:          Console.WriteLine(
  12:              string.Format("Dialing number {0}", number));
  13:      }
  14:   
  15:      public void Hangup()
  16:      {
  17:          Console.WriteLine("Call finished");
  18:      }
  19:  }
The interface IConnection defines the operation of dialing and hanging up a connection and the class Connection implements the details. The responsibility of opening and closing a connection has been separated.

Now, we can implement the class Modem and delegate to the encapsulated members, the implementation of the responsibilities of data channel and connection.
   1:  class Modem : IDataChannel, IConnection
   2:  {
   3:      IDataChannel dataChannel;
   4:      IConnection connection;
   5:   
   6:      public Modem()
   7:      {
   8:          this.dataChannel = new DataChannel();
   9:          this.connection = new Connection();
  10:      }
  11:   
  12:      public void Send(char c)
  13:      {
  14:          this.dataChannel.Send(c);
  15:      }
  16:   
  17:      public char Receive()
  18:      {
  19:          return this.dataChannel.Receive();
  20:      }
  21:   
  22:      public void Dial(string number)
  23:      {
  24:          this.connection.Dial(number);
  25:      }
  26:   
  27:      public void Hangup()
  28:      {
  29:          this.connection.Hangup();
  30:      }
  31:  }
If we need to modify any detail in the implementation of either how to establish the connection or how to send data over the channel, we just have a single class as entry point to update such requirement without touching any other class, keeping still a high cohesion among them.

Conclusions:
We have seen the benefits of giving a single responsibility to a class, a low index in the complexity of code supportability and maintainability is always desired. By separating a responsibility we can extend such functionality without affecting the class client that uses it, this is a topic that I will cover in my next post about the Open-Closed principle.

[RobertMartin96] SRP: The Single Responsibility Principle, Robert Martin, 1996

2 comments:

  1. Why is Modem implementing IDataChannel and IConnection? These should be passed into the constructor. Also, this way, any concrete implementations of IDataChannel and IConnection can be used by Modem.

    ReplyDelete