Definition
Defined by Robert C. Martin, it’s the first of the five design principles SOLID.
what he said may seem very simple to us
A class should have only one reason to change
However, implementing this simplicity can be complicated.
And because of the confusion around the word "reason", he also stated
This principle is about people
What he means by people is the people who use our system or in other words the actors.
Actors become the source of change for the family of features that serve them.
As their needs change, that specific family must also change to meet their needs.
Uncle bob gave a funny example in The Single Responsibility Principle The Clean Code Blog
Imagine you took your car to a mechanic in order to fix a broken electric window. He calls you the next day saying it’s all fixed. When you pick up your car, you find the window works fine; but the car won’t start. It’s not likely you will return to that mechanic because he’s clearly an idiot.
That’s how customers and managers feel when we break things they care about that they did not ask us to change.
SRP is probably not what do you think it is. If you think that "A class should do only one thing and do it well", this is not the correct explanation of the principle (it's partially correct) and in the same time it's the most commun.
Yeah, this is the common misunderstanding of the principle.
Uncle Bob provides a more clear explanation in his book
"Clean Architecture: A Craftsman's Guide to Software Structure and Design" to further explain its intention in this principle.
A module should be responsible to one, and only one, actor
So, let's go on and see how we can apply SRP in practice.
Code refactoring example
Taking the following example, we have a product class where we find a set of methods:
- A method to save the product in a txt file
- A method that generates a report file.
- Two other methods to manage or validate the data according to the management rules before changing the value of the field.
public class Product
{
public string Label { get; set; }
public string Description { get; private set; }
public double Price { get; private set; }
private readonly StringBuilder _priceActions = new();
public Product(string description, string label, double price)
{
Label = label;
ChangeDescription(description);
ChangePrice(price);
}
public void ChangePrice(double price)
{
if (price == 0)
{
Console.WriteLine("price can't be zero!");
throw new ArgumentNullException(nameof(price));
} if (price < Price)
_priceActions.Append($"New price applied to the product : ' {Label} '" +
$". \n Old price {Price} " +
$"\n New Price is {price}");
Price = price;
}
public void ChangeDescription(string desc)
{
if (string.IsNullOrWhiteSpace(desc))
{
Console.WriteLine("description can't be null");
throw new ArgumentNullException(nameof(desc));
}
Description = desc;
}
public void Save()
{
var fs = new FileStream("products.txt", FileMode.Append, FileAccess.Write);
var writer = new StreamWriter(fs);
writer.WriteLine($"Label:{Label},Description:{Description},Price:{Price}");
writer.Close();
fs.Close();
}
public void GenerateReportFile()
{
var fs = new FileStream("report.txt", FileMode.Create, FileAccess.Write);
var writer = new StreamWriter(fs);
writer.WriteLine("****this is a report****");
writer.WriteLine("product basic infos");
writer.WriteLine("-----------------------------------");
writer.WriteLine($"{Label} - {Price}");
writer.WriteLine($"Description : {Description}");
writer.WriteLine("-----------------------------------");
writer.WriteLine("price actions");
writer.WriteLine($"{_priceActions}");
writer.Close();
fs.Close();
}
}
If we think of the actors involved in the use of the product object, who could they be?
We can easily from these methods identify the following actors and reasons:
- An editor who manages the basic information of the product.
- Top management people for product reporting reasons.
- The persistence way or the storage system.
Doing this kind of analysis, we can easily identify that the methods that we must have outside of the product class are the following:
- Reporting method and related code stuff.
- Storage method.
- Validation rules and related messages.
So, let's refactor our code to follow SRP.
Let's start with the storage.
In this step, we will extract the method save into a new class which will be only responsible for storage stuff.
So, the related code in this class will change only if the storage manner change.
public interface IProductRepository
{
void Save(Product product);
}
public class ProductRepository : IProductRepository
{
public void Save(Product product)
{
var fs = new FileStream("products.txt", FileMode.Append, FileAccess.Write);
var writer = new StreamWriter(fs);
writer.WriteLine($"Label:{product.Label},Description:{product.Description},Price:{product.Price}");
writer.Close();
fs.Close();
}
}
We will continue our refactoring by extracting all validation rules and business validation messages into classes that will be the single entry point for every change in our business rules.
We will refactor the code in this way:
- First, we will extract the validation rules into a class that will be responsible only for product validations.
- Second, all the messages will be in a single class which will represents the single entry point for our business validation messages.
- In the final step, we will add a custom exception class for throwing all product business exception if something went wrong.
public class ProductBusinessException : Exception
{
public ProductBusinessException(string message) : base(message)
{
}
}
public static class BusinessExceptionMessages
{
public const string PriceIsnullErrorMessage = "price can't be zero!";
public const string DescriptionIsnullErrorMessage = "description can't be null";
}
public class ProductValidator
{
public static void ValidatePrice(double price)
{
if (price == 0)
throw new ProductBusinessException(BusinessExceptionMessages.PriceIsnullErrorMessage);
}
public static void ValidateDescription(string description)
{
if (string.IsNullOrWhiteSpace(description))
throw new ProductBusinessException(BusinessExceptionMessages.DescriptionIsnullErrorMessage);
}
}
In the final step, we will refactor all the code related to the reporting stuff.
So, we will extract the method GenerateReportFile into a new class.
and we will add a new method to our product class in order to expose all the price changes related to the product.
//New method in our product class
public string GetPriceChanges()
{
return _priceActions.ToString();
}
public interface IProductTextReporting
{
void GenerateReportFile(Product product);
}
public class ProductTextReporting : IProductTextReporting
{
public void GenerateReportFile(Product product)
{
var fs = new FileStream("report.txt", FileMode.Create, FileAccess.Write);
var writer = new StreamWriter(fs);
writer.WriteLine("****this is a report****");
writer.WriteLine("product basic infos");
writer.WriteLine("-----------------------------------");
writer.WriteLine($"{product.Label} - {product.Price}");
writer.WriteLine($"Description : {product.Description}");
writer.WriteLine("-----------------------------------");
writer.WriteLine("price actions");
writer.WriteLine($"{product.GetPriceChanges()}");
writer.Close();
fs.Close();
}
}
After the refactoring, our product class looks like the code below.
public class Product
{
public string Label { get; set; }
public string Description { get; private set; }
public double Price { get; private set; }
private readonly StringBuilder _priceActions = new();
public Product(string description, string label, double price)
{
Label = label;
ChangeDescription(description);
ChangePrice(price);
}
public void ChangePrice(double price)
{
ProductValidator.ValidatePrice(price);
if (price < Price)
_priceActions.Append($"New price applied to the product : ' {Label} '" +
$". \n Old price {Price} " +
$"\n New Price is {price}");
Price = price;
}
public void ChangeDescription(string desc)
{
ProductValidator.ValidateDescription(desc);
Description = desc;
}
public string GetPriceChanges()
{
return _priceActions.ToString();
}
}
There it is ! we have refactored our code in a way that each class will have only one reason to change.
Final Thoughts
To conclude, once you find that a class or module starts to change for different reasons, take the time and effort to refactor your code to follow SRP, but needless refactoring can bring us down to classes or modules that are difficult to understand.
A second advice, if you are working in a high coupled software, module or class, be careful when you are refactoring your code, take you time in order to uncouple it before, because any fast refactoring step can lead you to a disaster.
That's it !
Ask me, comment below, share it, rate it, whatever you want.
See you soon.
References
Source code : Link to github with the whole code from this article
The Clean Code Blog : The Single Responsibility Principle
Clean Architecture: A Craftsman's Guide to Software Structure and Design
Top comments (0)