Aspects of Single Responsibility Principle



By           



One of the most promising principles of software development is Single Responsibility Principle (SRP). This article gives a general idea of SRP and how software developers develop more evolved codes maintaining inner software quality.  


Definition of SRP


SRP is defined as a functional unit on a given level of abstraction should only be responsible for a single aspect of a system’s requirements. An aspect of requirements is a trait or property of requirements, which can change independently of other aspects. There are many other definitions for SRP.
At same abstraction level, responsibilities should not overlap. Two methods of the same should focus on different aspects. However, two methods in the same class will both have to be concerned with the same higher level responsibility. Classes, components, modules, applications should focus on different aspects.

Typical Aspects


Technologies and resources are the most common typical aspects in software. Three environment interaction aspects:
user needs to interact with the program (technology)
program needs to access the data (resource)
program needs to “be configured” with some  data source and the page length.


Each interaction of software development requires the use of at least one platform API.  In addition to these there is an aspect for all non-interaction code. Formatting the data for display is also an aspect.


Aspects can be refined into finer grained sub-aspects and there is mapping between non-functional requirements and aspects.


Subtle Aspects


Functional and Non-Functional    
The following code gives an idea of mixing a functional aspect with non-functional ones:
interface IModem
{
  public void Dial(String number);
  public void Hangup();
  public void Send(char c);
  public char Recv();
}
Communication via a modem has two sub-aspects. Handling the connection and exchanging data can change at a different rate. Termination of a connection could change separately from how data is sent/received. The code for this is:
IModem m = new IsdnModem();
m.Dial(…);
m.Send(…);
m.Hangup();
By making modem a true resource, the code is changed as:
using(IModem m = new IsdnModem())
{
  m.Dial(…);
  m.Send(…);
}

Developer using an IModem implementation should free the resource by C# using statement. For this software developers  need to change the interface:
interface IModem : IDisposable
{
  public void Dial(String number);
  public void Send(char c);
  public char Recv();
}
This change must not affect the code. Connection handling must be refactored into its own interface in this way:
interface IModemConnection : IDisposable
{
  public void Dial(String number);
}

interface IModemDataExchange
{
  public void Send(char c);
  public char Recv();
}
Here, connection is a non-functional aspect and sending/receiving data is the functional aspect. Next consideration is if the same class implements both interfaces. For this, the interface would have to be tweaked a bit more:
interface IModemConnection : IDisposable
{
  public IModemDataExchange Dial(String number);
}

interface IModemDataExchange
{
  public void Send(char c);
  public char Recv();
}
Modem connection execution becomes an important source for a data exchange completion. Data exchange cannot take place before a connection has been established with Dial().
Another case of blending functional and non-functional aspects is:
void StoreCustomer(Customer c)
{
  trace.Write("Storing customer…");
  using(db.Connect(…))
  {
    var tx = db.OpenTransaction();
    try
    {
      db.ExecuteSql(…); // Store name and address
      db.ExecuteSql(…); // Store contact data
      tx.Commit();
    }
    catch(Exception ex)
{
      tx.Rollback();
      trace.Write("Failed to store customer");
      log.Log("Storing customer failed; exception: {0}", ex);
      throw new ApplicationException(…);
    }
  }
}

The responsibility of this code is to store a customer’s data, which requires just two calls to db.ExecuteSql().
Non-functional aspects that spread all over the code base can be overcome using the continuation:
void StoreCustomer(Customer c)
{
  dbServices.WrapInTransaction(
    "Store customer",
    db => {
      db.ExecuteSql(…);
      db.ExecuteSql(…);
    });
}

Request and Response
SRP problems cannot be solved without knowing how persistent they are. An example of SRP violation is:
var y = f(x);

Requesting or sending data is different from responding or receiving data. The result depends on how data is sent and received. Combining request preparation and response processing tends to violate SRP.
Here are two suggestions using C# to detangle request and response:
void Caller() {
  …
  f(x, ProcessResult);
}

This can be continued by decoupling request preparation from result processing. Further detangling would require hiding result-processing altogether from the caller. The caller would just fire an event:
void Caller()
{
  …
  OnF(x);
}

Action<TypeOfX> OnF;
The code for binding request handling and result processing:
OnF = x => f(x, ProcessResult);
Functions are a persistent form of coupling. Therefore they should be used with care. Simple function calls must not be refrained from. We must recognize how they subtly combine these two aspects.

Functionality and Topology
Function call hierarchies as well as object dependency hierarchies violate the SRP. An example is:
class Client : IClient {
  IService s;
  …
}

interface IService {
  S k(T t);
  void l();
}

interface IClient {
  void f();
}
Here, client and service seem to have individual responsibilities.  The problem with this code is functional as well as topological.


Functional- If IService or the interface implementation changes, then Client needs to change too.
Topological- If the topology of the service landscape changes, e.g. further dependencies might be introduced and references need to be changed.
Applying the Interface Segregation Principle might help to lessen the problem. Redefining the client as follows has some advantages:
interface IClient {
  void f();
  Func<T, S> k {get;set;};
  Action l {get;set;};
}
This removes the aspect of topological dependency and keeps the specification of a client self-contained.
Syntax and Semantics
The strangest blends of aspects very hard to notice are:
void a() {
  f();
  if (g())
    h();
  k();
}
The syntactical aspect (the shape of the control flow, a static view of the code) and the semantic aspect (the dynamic view of the code) are combined here. To avoid this mixture of aspects, again continuations can help:
void a() {
  f();
  g(h);
  k();
}
The decision if h() has to be called is completely pushed into g(). It not only contains the condition but also the control statement, e.g.
void g(Action doIfConditionHolds) {
  if (…)
    doIfConditionHolds();
}
The lowest level of a call hierarchy should be free of expressions and control statements to strictly separate the syntactic and the semantic aspects.


Conclusion


SRP is easily understandable and universally applicable. Mixing of obvious aspects of functionality and data, leads to hard to maintain code. So developers must be more cautious while developing a code.



 

Presentation