SOLID PRINCIPLES
SOLID Principles is a coding standard that all developers should have a clear
concept for developing software in a proper way to avoid a bad design.
It was promoted by Robert C Martin and is used across the object-oriented design spectrum.
When applied properly it makes your code more extendable, logical and easier to read.
Solid stands for
- S - Single responsibility principle
- O - Open-closed principle
- L - Liskov substitution principle
- I - Interface segregation principle
- D - Dependency Inversion principle
1. Single Responsibility Principle
A class should have one and only one reason to change, meaning that a class should have only one job.
For example, say we have some shapes and we wanted to sum all the areas of the shapes.
Let's take a look at the following code:
class Circle
attr_reader :radius
def initialize(radius)
@radius = radius
end
end
class Square
attr_reader :length
def initialize(length)
@length = length
end
end
First we create our shapes classes and have the constructors setup the required parameter. Next, we move on by creating the AreaCalculator class and then write up our logic to sum up the areas of all provided shapes.
class AreaCalculator
def AreaCalculator(shapes)
@shapes = shapes
end
def sum
# logic to sum the areas
end
# Disobey SRP, implement the SumCalculatorOutputter
def output
sum
end
end
To use the AreaCalculator class, we simply instantiate the class and pass in an array of shapes, and display the output at the bottom of the page.
shapes = [Circle.new(2), Square.new(4), Square.new(5)]
areas = AreaCalculator.new(shapes)
puts areas.output
The problem with the output method is that the AreaCalculator handles the logic to output the data. Therefore, what if the user wanted to output the data as json or something else?
All of that logic would be handled by the AreaCalculator class, this is what SRP frowns against; the AreaCalculator class should only sum the areas of provided shapes, it should not care whether the user wants json or HTML.
So, to fix this you can create an SumCalculatorOutputter class and use this to handle whatever logic you need to handle how the sum areas of all provided shapes are displayed.
class SumCalculatorOutputter
def initialize(areas)
@areas = areas
end
def JSON; end
def XML; end
def HTML; end
end
output = SumCalculatorOutputter.new(areas)
puts output.JSON
puts output.XML
puts output.HTML
Now, whatever logic you need to output the data to the user is now handled by the SumCalculatorOutputter class.
2. Open-closed Principle
Objects or entities should be open for extension, but closed for modification.
This simply means that a class should be easily extendable without modifying the class itself. Let’s take a look at the AreaCalculator class, especially it’s sum method.
def sum
area = []
@shapes.each do |shape| # disobey the Open-closed principle
if shape.instance_of? Square
area << shape.length**2
elsif shape.instance_of? Circle
area << Math::PI * shape.radius**2
end
end
array_sum(area) # arr.reduce(0) { |a,b| a+b }
end
If we wanted the sum method to be able to sum the areas of more shapes, we would have to add more if/else blocks and that goes against the Open-closed principle.
A way we can make this sum method better is to remove the logic to calculate the area of each shape out of the sum method and attach it to the shape’s class.
class Square
attr_reader :length
def initialize(length)
@length = length
end
def area
length**2
end
end
The same thing should be done for the Circle class, an area method should be added. Now, to calculate the sum of any shape provided should be as simple as:
def sum
area = []
@shapes.each do |shape|
area << shape.area
end
array_sum(area) # arr.reduce(0) { |a,b| a+b }
end
Now we can create another shape class pass it in calculating the sum without breaking our code.
3. Liskov Substitution Principle
Every derived/child classes must be substitutable for their base/parent class.
Basically, it takes care that while coding using interfaces in our code, we not only have a contract of input that the interface receives but also the output returned by different Classes implementing that interface; they should be of the same type.
Let's take an example of a Rectangle and Sqaure. In mathematics, a Square is a Rectangle. Indeed it is a specialization of a rectangle. The "is a" makes you want to model this with inheritance. However if in code you made Square derive from Rectangle, then a Square should be usable anywhere you expect a Rectangle. This makes for some strange behavior.
Code for Rectangle
class Rectangle
attr_reader :length, :breadth
def set_length(len)
@length = len
end
def set_breadth(width)
@breadth = width
end
def get_area
@length * @breadth
end
end
Code for Square
class Square < Rectangle
def set_breadth(width)
super
set_length(width)
end
def set_length(length)
super
set_breadth(length)
end
end
set_length and set_breadth methods on Rectangle class makes perfect sense. However if your Rectangle reference pointed to a Square, then set_breadth and set_length doesn't make sense because setting one would change the other to match it.
In this case Square fails the Liskov Substitution Test with Rectangle.
4. Interface Segregation Principle
A Client should not be forced to implement an interface that it doesn't use.
This rule means that we should break our interfaces in many smaller ones, so they better satisfy the exact needs of our clients.
Let's see an example
module WorkerInterface
def work
raise "Not implemented"
end
def sleep
raise "Not implemented"
end
end
class HumanWorker
include WorkerInterface
def work
puts "works"
end
def sleep
puts "sleep"
end
end
class RobotWorker
include WorkerInterface
def work
puts "works"
end
def sleep
# No need of this method.
end
end
In the above code, RobotWorker doesn't need sleep method, but the class has to implement the sleep method else an error will be raised. This breaks the Interface Segregation Principle.
Here's how we can fix this:
module WorkAbleInterface
def work
raise "Not implemented"
end
end
module SleepAbleInterface
def sleep
raise "Not implemented"
end
end
class HumanWorker
include WorkAbleInterface
include SleepAbleInterface
def work
puts "works"
end
def sleep
puts "sleep"
end
end
class RobotWorker
include WorkAbleInterface
def work
puts "works"
end
end
By seperating the WorkAble and SleepAble interfaces we have removed the compulsion to implement the sleep method.
5. Dependency Inversion Principle
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Abstractions should not depend on details. Details should depend on abstractions.
To simply put, Depend on Abstractions not on concretions.
This principle allows for decoupling, let's see an example.
class Newsperson
def broadcast(news)
Newspaper.new.print(news)
end
end
class Newspaper
def print(news)
puts news
end
end
laura = Newsperson.new
laura.broadcast("Some Breaking News!") # => "Some Breaking News!"
This is a functioning broadcast method, but right now it’s tied to the Newspaper object. What if we change the name of the Newspaper class? What if we add more broadcast platforms? Both of these things would cause us to have to change our Newsperson object as well. Here we have a dependency on the type of broadcasting our newsperson which is a High level module and it depends on low level module(Newspaper).
Now let's do it using Dependency Inversion Principle.
class Newsperson
def broadcast(news, platform = Newspaper)
platform.new.broadcast(news)
end
end
class Newspaper
def broadcast(news)
puts "do_something_with news"
end
end
class Twitter
def broadcast(news)
puts "tweets news"
end
end
class Television
def broadcast(news)
puts "live_coverage news"
end
end
laura = Newsperson.new
laura.broadcast("Breaking news!") #do_something_with "Breaking news!"
laura.broadcast("Breaking news!", Twitter) #tweets "Breaking news!"
laura.broadcast("Breaking news!", Television)
As you can see, we can now pass any news broadcasting platform through the broadcast method whether that’s Twitter, TV, or Newspaper. We can change any of these classes and we won’t break the other classes. The higher level Newsperson class does not depend on the lower level classes and vice versa.
Top comments (1)
thanks for sharing