Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
November 17, 2020 01:27 pm GMT

The SOLID Object-Oriented Design Principles

The SOLID design principles are five key principles that are used to design and structure a Class in an object-oriented design. They are a set of rules that needs to be followed to write effective code thats easier to understand, collaborate, extend & maintain.

Why are they important?

Lets answer this question with a use case and well pull one principle from the five which is O - The Open-Closed Principle (Open to extension but closed to modification).

Arent we supposed to modify the class if theres new functionality or modules to be added? Well, yes but actually no. Its advised to not change/modify an already tested code thats currently in production. This might lead to some side effects that would break the functionality of the entire class which will need further refactoring (Ugh! Work!). So, we need to structure it in such a way that it should always be open to extension and closed to modification.

Thats what I like more about conventions over configurations which Ruby on Rails offers. It says Hey, lets follow a similar convention and design patterns instead of bringing our own. The one that looks good to me might not look good to others in a highly collaborative development environment we are currently having. Stop wasting time figuring things out and lets start writing some code.

Well see more in detail about each of the principles and I will be using the Ruby programming language for examples. The concepts are applicable to any object-oriented programming language.

So, who formulated this? A Background

The theory of SOLID principles was introduced by Robert C. Martin (a.k.a Uncle Bob) in his 2000 paper Design Principles and Design Patterns & The SOLID acronym was introduced later by Michael Feathers.

The five principles are as follows:

  1. The Single Responsibility Principle
  2. The Open-Closed Principle
  3. The Liskov Substitution Principle
  4. The Interface Segregation Principle
  5. The Dependency Inversion Principle

Writing a clean understandable code is not only having multiple lines of comments that explain what (sometimes, the hell) is going on. The code you write should express your intent rather than depending on comments. In most cases, extensive comments are not required if your code is expressive enough.

The main goal of any design principles is - "To create understandable, readable, and testable code that many developers can collaboratively work on."

The Single Responsibility Principle

As the name implies, the single responsibility principle denotes that a class should have only one responsibility.

Consider a SaaS product that sends out weekly analytics to your users. There are two actions that need to be done to complete the process. One is to generate the report itself and the other will be to send the report. Lets assume that we are emailing them.

Lets see a scenario where we violate and then an example that follows the single responsibility principle.

# Violation of the Single Responsibility Principle in Rubyclass WeeklyAnalyticsMailer  def initialize(activities, user)    @activities = activities    @user = user    @report = ''  end  def generate_report!    # Generate Report  end  def send_report    Mail.deliver(      from: '[email protected]',      to: @user.email,      subject: 'Yo! Your weekly analytics is here.',      body: @report    )  endendmailer = WeeklyAnalytics.new(user)mailer.generate_report!mailer.send_report
Enter fullscreen mode Exit fullscreen mode

Even though sending an analytics email looks like a single action, it involves two different sub actions. Why is the above class a violation of the single responsibility principle? Because the class has two responsibilities, one is to generate the report and the other is to email them. Having the class name as WeeklyAnalyticsMailer, it shouldnt do extra work than the intended one. This clearly violates the principle.

How to fix this? We will construct two different classes where one generates the report and the other emails to your users.

# Correct use of the Single Responsibility Principle in Rubyclass WeeklyAnalyticsMailer  def initialize(report, user)    @report = report    @user = user  end  def deliver    Mail.deliver(      from: '[email protected]',      to: @user.email,      subject: 'Yo! Your weekly analytics is here.',      body: @report    )  endendclass WeeklyAnalyticsGenerator  def initialize(activities)    @activities = activities  end  def generate    # Generate Report  endendreport = WeeklyAnalyticsGenerator.new(activities).generateWeeklyAnalyticsMailer.new(report, user).deliver
Enter fullscreen mode Exit fullscreen mode

As planned, we have two classes that have their own dedicated responsibility and it doesnt exceed one. If we want to extend the functionality of the mailer class (assume we use SendGrid to send out our email), we can simply make the necessary changes to the dedicated mailer class without touching the generator class.

The Open-Closed Principle

We looked briefly at the Open-Closed principle in the introduction. Well see more about it now.

The main goal of this principle is to create a flexible system architecture that is easier to extend the functionality of your application instead of changing or refactoring the existing source code that is in production.

Objects or entities should be open for extension but closed for modification.

Lets assume an example where we again need to send the analytics to the user in

different formats and mediums.

# Violation of the Open-Closed Principle in Rubyclass Analytics def initialize(user, activities, type, medium)   @user = user   @activities = activities   @type = type   @medium = medium end def send   deliver generate end private def deliver(report)   case @type   when :email     # Send Report via Email   else     raise NotImplementedError   end end def generate   case @type   when :csv     # Generate CSV report   when :pdf     # Generate PDF report   else     raise NotImplementedError   end endendreport = Analytics.new( user, activities, :csv, :email)report.send
Enter fullscreen mode Exit fullscreen mode

From the above example, we can send a CSV/PDF via email. If we want to add a new format, say raw and a new medium SMS, we need to modify the code which clearly violates our Open-Closed Principle. Well refactor the above code to follow the Open-Closed Principle.

# Correct use of the Open-Closed Principle in Rubyclass Analytics  def initialize(medium, type)    @medium = medium    @type = type  end  def send    @medium.deliver @type.generate  endendclass EmailMedium  def initialize(user)    @user = user    # ... Setup Email medium  end  def deliver    # Deliver Email  endendclass SmsMedium  def initialize(user)    @user = user    # ... Setup SMS medium  end  def deliver    # Deliver SMS  endendclass PdfGenerator  def initialize(activities)    @activities = activities  end  def generate    # Generate PDF Report  endendclass CsvGenerator  def initialize(activities)    @activities = activities  end  def generate    # Generate CSV Report  endendclass RawTextGenerator  def initialize(activities)    @activities = activities  end  def generate    # Generate Raw Report  endendreport = Analytics.new(  SmsMedium.new(user),  RawTextGenerator.new(activities))report.send
Enter fullscreen mode Exit fullscreen mode

We refactored the above class in such a way that the module can be easily extended without changing the existing code which now follows the Open-Close principle. Neat!

The Liskov Substitution Principle

According to Uncle Bob, Subclasses should add to a base classs behavior, not replace it..

A simple foo-bar example could be - all squares are rectangles and not vice versa. If we have a Rectangle class as our base class for our Square class. We then pass the length and width as the same values to compute the area of a square since all squares are rectangles. We will get the correct value but if we do the other way round, it will lead to an incorrect value.

To overcome this, we need to have a Shape class as our Base class, and the Rectangle and Square will extend from the Base class Shape which will satisfy the Liskov Substitution principle. Enough of the foo-bar examples. Well see a more realistic example.

In general, the Liskov Substitution Principle states that parent instances should be replaceable with one of their child instances without creating any unexpected or incorrect behaviour. Therefore, LSP ensures that abstractions are correct, and helps developers achieve more reusable code and better organize class hierarchies.

Lets see an example that violates the principle. Theres a base class called UserInvoice which has a method to retrieve all invoices.

Theres a subclass AdminInvoice which inherits the base class that has the same method invoices. The AdminInvoice will return a string when compared to the Base class method which returns an array of objects. This clearly violates the LSP since the subclass is not replaceable with the base class without any side effects as the subclass overwrites the behaviour of invoices method.

# Violation of the Liskov Substitution Principle in Rubyclass UserInvoice  def initialize(user)    @user = user  end  def invoices    @user.invoices  endendclass AdminInvoice < UserInvoice  def invoices    invoices = super    string = ''    user_invoices.each do |invoice|      string += "Date: #{invoice.date} Amount: #{invoice.amount} Remarks: #{invoice.remarks}
" end string endend
Enter fullscreen mode Exit fullscreen mode

To fix this, we need to introduce a new format method in the sub class that handles the formatting. After this, the LSP can be satisfied since the sub class is interchangeable with the base class without any side effects.

# Correct use of the Liskov Substitution Principle in Rubyclass UserInvoice  def initialize(user)    @user = user  end  def invoices    @user.invoices  endendclass AdminInvoice < UserInvoice  def invoices    super  end  def formatted_invoices    string = ''    invoices.each do |invoice|      string += "Date: #{invoice.date} Amount: #{invoice.amount} Remarks: #{invoice.remarks}
" end string endend
Enter fullscreen mode Exit fullscreen mode

The Interface Segregation Principle

The ISP says that Clients shouldnt depend on methods they dont use. Several client-specific interfaces are better than one generalized interface..

This principle mainly focuses on segregating a fat base class to different classes. Lets assume we have an ATM Machine that performs 4 actions - login, withdraw, balance, fill.

# Violation of the Interface Segregation Principle in Rubyclass ATMInterface  def login  end  def withdraw(amount)    # Cash withdraw logic  end  def balance    # Account balance logic  end  def fill(amount)    # Fill cash (Done by the ATM Custodian)  endendclass User  def initialize    @atm_machine = ATMInterface.new  end  def transact    @atm_machine.login    @atm_machine.withdraw(500)    @atm_machine.balance  endendclass Custodian  def initialize    @atm_machine = ATMInterface.new  end  def load    @atm_machine.login    @atm_machine.fill(5000)  endend
Enter fullscreen mode Exit fullscreen mode

We have two types of users User & Custodian where the user uses 3 actions (login, withdraw & balance) and the Custodian uses 2 (login & fill).

We have a single class called ATMInterface that does all the heavy lifting even though the client doesnt need them (Ex: User doesnt need to replenish while the Custodian doesnt need to withdraw/check balance). This of course violates our ISP. Lets segregate the above fat class into different subclasses.

# Correct use of the Interface Segregation Principle in Rubyclass ATMInterface  def login  endendclass ATMUserInterface < ATMInterface  def withdraw(amount)    # Cash withdraw logic  end  def balance    # Account balance logic  endendclass ATMCustodianInterface < ATMInterface  def replenish    # Fill cash (Done by the ATM Custodian)  endendclass User  def initialize    @atm_machine = ATMUserInterface.new  end  def transact    @atm_machine.login    @atm_machine.withdraw(500)    @atm_machine.balance  endendclass Custodian  def initialize    @atm_machine = ATMCustodianInterface.new  end  def load    @atm_machine.login    @atm_machine.replenish  endend
Enter fullscreen mode Exit fullscreen mode

The Dependency Inversion Principle

High-level modules shouldnt depend on low-level modules. Both modules should depend on abstractions. In addition, abstractions shouldnt depend on details. Details depend on abstractions.

Thats too much detail in 4 sentences. Lets see what it means. According to Uncle Bob, the DIP is the result of strictly following 2 other SOLID principles: Open-Closed & Liskov Substitution Principle. Hence, this will have clearly separate abstractions.

It should also be readable, extendable, and child classes should be easily replaceable by other instances of a base class without breaking the system.

# Violation of the Dependency Inversion Principle in Rubyclass Parser  def parse_xml(file)    XmlParser.new.parse(file)  end  def parse_csv(file)    CsvParser.new.parse(file)  endendclass XmlParser  def parse(file)    # parse xml  endendclass CsvParser  def parse(file)    # parse csv  endend
Enter fullscreen mode Exit fullscreen mode

The class Parser depends on classes XmlParser and CsvParser instead of abstractions, which indicates the violation of the DIP principle since the classes XmlParser and CsvParser may contain the logic that refers to other classes. Thus, we may impact all the related classes when modifying the class Parser.

#Correct use of the Dependency Inversion Principle in Rubyclass Parser  def initialize(parser: CsvParser.new)    @parser = parser  end  def parse(file)    @parser.parse(file)  endendclass XmlParser  def parse(file)    # parse xml  endendclass CsvParser  def parse(file)    # parse csv  endend
Enter fullscreen mode Exit fullscreen mode

Conclusion

Remember that theres no single equation or rule. However, following a predefined set of rules correctly will yield a great design. Writing clean code comes with experience and the principles when used smartly will yield better results which are extendable, maintainable and will make everyones life easier.

Why did I write this post?

Ive planned my career path into different segments. After my Computer Science degree, I set my course to explore, to play around with whatever tech that excites me. From JS to Rust, both Backend & Frontend. Hmm, Ive explored and can easily pick up anything that excites me, understand and work with it. Whats next? Mastering.

You can follow me/my journey on Twitter/Github respectively. Bye!


Original Link: https://dev.to/sathish/the-solid-object-oriented-design-principles-4e7o

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To