Introduction
Of all the SOLID design principles, this one may be the easiest, in theory, to adhere to. It also exposes some subtleties that the inexperienced eye may miss allowing rot to take a foothold in our code.
The Single-Responsibility Principle (SRP) is “officially” defined as the functional relatedness or cohesion of elements in a class. To my eyes, this is a rather broad definition. Uncle Bob refines this a little stating that “a class should have only one reason to change”. How do we classify or identify a “reason to change”?
When we move away from a data-centric view of design and begin to recognize our code from a more anthropomorphic and metaphor driven perspective, we begin to see emergent behaviors. Analysis of behavior gives us a keener sense of motivation for changing a class or method. If we can find more than one force exerting pressure to change our code, then it must be refactored to separate out those responsibilities or reasons for change. This can be at a class or method level.
Examples
Here is a very simple example that violates SRP:
public void SRPViolation(Contract contract)
{
if (contract.ExecutionDate == null ||
(!contract.ExpirationDate == null &&
contract.ExecutionDate > contract.ExpirationDate))
{
//contract is not valid
}
else if (contract.ExecutionDate <= DateTime.Now &&
(contract.ExpirationDate > DateTime.Now ||
contract.ExpirationDate == null))
{
//contract is current so do stuff
}
}
There are several problems with the above method. First, it does not effectively communicate. Second, it has two reasons to change. The first if statement defines some validation rules. The second defines a “current” contract. These are business rules and are one of the most volatile aspects of our code. Let’s start refactoring and improve the quality of the code.
We will start by introducing an IValidator interface and move the validation logic into a concrete ContractValidator class:
public interface IValidator<T>
{
void Validate(T item);
bool IsValid { get; }
}
public class ContractValidator : IValidator<Contract>
{
private bool isValid;
public void Validate(Contract item)
{
isValid = true;
if (item.ExecutionDate == null ||
(item.ExpirationDate != null &&
item.ExecutionDate > item.ExpirationDate))
{
isValid = false;
}
}
public bool IsValid
{
get { return isValid; }
}
}
Next we will move our “current” contract logic to the contract class, as this logic is unlikely to change (I know the word “unlikely” is suspect, but if the requirements change we can always refactor):
public class Contract
{
public DateTime ExecutionDate { get; set; }
public DateTime? ExpirationDate { get; set; }
public bool IsCurrent
{
get
{
return
this.ExecutionDate == null ||
(this.ExpirationDate != null &&
this.ExecutionDate >
this.ExpirationDate);
}
}
}
Now our updated method looks like this:
public void SRPRefactored(Contract contract,
IValidator<Contract> validator)
{
validator.Validate(contract);
if (!validator.IsValid)
{
//contract is not valid
}
else if (contract.IsCurrent)
{
//do stuff
}
}
This code is starting to shape up, but there is still room for improvement.
Believe it or not, if…then…else can be a violation of SRP because we are creating branching logic that can introduce another reason for change to our code. Let’s refactor this one more time:
public void SRPRefactored(Contract contract,
IValidator<Contract> validator)
{
Validate(contract, validator);
if (contract.IsCurrent)
{
//do stuff
}
}
private void Validate(Contract contract,
IValidator<Contract> validator)
{
validator.Validate(contract);
if (!validator.IsValid)
{
throw new InvalidOperationException();
}
}
Conclusion
The Single-Responsibility Principle is relatively easy to grasp, and conceptually the simplest to begin putting to use right away to improve the quality of your code.
I hope this article has given you some new insights into the Single-Responsibility Principle, and inspired you to put these ideas to use in your own code.