SOLID Principles

zeeshan1414

zeeshan1414

Posted on December 21, 2020

SOLID Principles

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!"
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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.

References


  1. S.O.L.I.D: The First 5 Principles of Object Oriented Design

  2. SOLID Principles - simple and easy explanation

  3. Example of the Liskov Substitution Principle?

  4. The Power of Interfaces in Ruby

  5. DEPENDENCY INVERSION PRINCIPLE IN RUBY

💖 💪 🙅 🚩
zeeshan1414
zeeshan1414

Posted on December 21, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

SOLID Principles
programming SOLID Principles

December 21, 2020