Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for SRP - Is the most misunderstood principle ?
Khalid BOURZAYQ
Khalid BOURZAYQ

Posted on

     

SRP - Is the most misunderstood principle ?

Definition

Defined by Robert C. Martin, it’s the first of the five design principlesSOLID.

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 inThe Single Responsibility PrincipleThe 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:

  1. A method to save the product in a txt file
  2. A method that generates a report file.
  3. Two other methods to manage or validate the data according to the management rules before changing the value of the field.
publicclassProduct{publicstringLabel{get;set;}publicstringDescription{get;privateset;}publicdoublePrice{get;privateset;}privatereadonlyStringBuilder_priceActions=new();publicProduct(stringdescription,stringlabel,doubleprice){Label=label;ChangeDescription(description);ChangePrice(price);}publicvoidChangePrice(doubleprice){if(price==0){Console.WriteLine("price can't be zero!");thrownewArgumentNullException(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;}publicvoidChangeDescription(stringdesc){if(string.IsNullOrWhiteSpace(desc)){Console.WriteLine("description can't be null");thrownewArgumentNullException(nameof(desc));}Description=desc;}publicvoidSave(){varfs=newFileStream("products.txt",FileMode.Append,FileAccess.Write);varwriter=newStreamWriter(fs);writer.WriteLine($"Label:{Label},Description:{Description},Price:{Price}");writer.Close();fs.Close();}publicvoidGenerateReportFile(){varfs=newFileStream("report.txt",FileMode.Create,FileAccess.Write);varwriter=newStreamWriter(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();}}
Enter fullscreen modeExit fullscreen mode

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.

publicinterfaceIProductRepository{voidSave(Productproduct);}publicclassProductRepository:IProductRepository{publicvoidSave(Productproduct){varfs=newFileStream("products.txt",FileMode.Append,FileAccess.Write);varwriter=newStreamWriter(fs);writer.WriteLine($"Label:{product.Label},Description:{product.Description},Price:{product.Price}");writer.Close();fs.Close();}}
Enter fullscreen modeExit fullscreen mode

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.
publicclassProductBusinessException:Exception{publicProductBusinessException(stringmessage):base(message){}}publicstaticclassBusinessExceptionMessages{publicconststringPriceIsnullErrorMessage="price can't be zero!";publicconststringDescriptionIsnullErrorMessage="description can't be null";}publicclassProductValidator{publicstaticvoidValidatePrice(doubleprice){if(price==0)thrownewProductBusinessException(BusinessExceptionMessages.PriceIsnullErrorMessage);}publicstaticvoidValidateDescription(stringdescription){if(string.IsNullOrWhiteSpace(description))thrownewProductBusinessException(BusinessExceptionMessages.DescriptionIsnullErrorMessage);}}
Enter fullscreen modeExit fullscreen mode

In the final step, we will refactor all the code related to the reporting stuff.

So, we will extract the methodGenerateReportFile 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 classpublicstringGetPriceChanges(){return_priceActions.ToString();}
Enter fullscreen modeExit fullscreen mode
publicinterfaceIProductTextReporting{voidGenerateReportFile(Productproduct);}publicclassProductTextReporting:IProductTextReporting{publicvoidGenerateReportFile(Productproduct){varfs=newFileStream("report.txt",FileMode.Create,FileAccess.Write);varwriter=newStreamWriter(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();}}
Enter fullscreen modeExit fullscreen mode

After the refactoring, our product class looks like the code below.

publicclassProduct{publicstringLabel{get;set;}publicstringDescription{get;privateset;}publicdoublePrice{get;privateset;}privatereadonlyStringBuilder_priceActions=new();publicProduct(stringdescription,stringlabel,doubleprice){Label=label;ChangeDescription(description);ChangePrice(price);}publicvoidChangePrice(doubleprice){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;}publicvoidChangeDescription(stringdesc){ProductValidator.ValidateDescription(desc);Description=desc;}publicstringGetPriceChanges(){return_priceActions.ToString();}}
Enter fullscreen modeExit fullscreen mode

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)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

.Net Tech leader | Senior Software engineer | Software craftsman
  • Joined

More fromKhalid BOURZAYQ

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp