An Interest In:
Web News this Week
- March 21, 2024
- March 20, 2024
- March 19, 2024
- March 18, 2024
- March 17, 2024
- March 16, 2024
- March 15, 2024
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:
- The Single Responsibility Principle
- The Open-Closed Principle
- The Liskov Substitution Principle
- The Interface Segregation Principle
- 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
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
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
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
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
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
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
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
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
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
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
Dev To
An online community for sharing and discovering great ideas, having debates, and making friendsMore About this Source Visit Dev To