Skip to content

Instantly share code, notes, and snippets.

@Integralist
Last active April 3, 2023 08:16
Show Gist options
  • Save Integralist/7974887 to your computer and use it in GitHub Desktop.
Save Integralist/7974887 to your computer and use it in GitHub Desktop.
S.O.L.I.D principles

S.O.L.I.D principles

Single Responsibility Principle

A class or method should have no more than one responsibility. If it has more than one responsibility then use the relevant refactoring technique(s) to extract the functionality into its own class or method.

Open/Closed Principle

An object should be 'open to extension' but 'closed for modification'.

Here is a class that violates this principle...

require 'json'

class Report
  def body
    { :a => 'Anna', :b => 'Bob', :c => 'Chris' }
  end

  def print
    body.to_json
  end
end

...it violates the principle because if we want to extend the class to report the data in a different format, we can't without modifying the source class.

To fix this we can use dependency injection...

require 'json'

class Report
  def body
    { :a => 'Anna', :b => 'Bob', :c => 'Chris' }
  end

  def print(formatter: JSONFormatter.new)
    formatter.format(body)
  end
end

report = Report.new
report.print(formatter: XMLFormatter.new)

...notice we inject a specific class to handle the required formatting.

This means we can extend the class without modifying it.

Liskov Substitution Principle

This principle only applies to code that uses inheritance. The reason why is because the principle states a subtype must be substitutable/interchangeable for their base class.

The benefit of this principle is that when code is interchangeable, it becomes more reusable.

The following code violates this principle...

class Animal
  def walk
     # do some walking
  end
end

class Cat < Animal
  def run
    # do some cat style running
  end
end

...it violates the principle because the subclass implements a run method that doesn't appear in the base class.

The solution is based on the use of interfaces, but as Ruby doesn't implement interfaces or abstract classes we instead create empty methods for each part of the proposed interface.

class Animal
  def walk
     # do some walking
  end

  def run
     raise NotImplementedError
  end
end

class Cat < Animal
  def run
    # do some cat style running
  end
end

Interface Segregation Principle

If a class uses an interface, then that interface should only contain methods or properties used by its consumers. If the interface has too much functionality then any change to the interface will effect more consumers than it probably needs to (meaning more chance for errors to occur).

Take a look at the following code...

class Car
  def open; end
  def start_engine; end
  def change_engine; end
end

class Driver
  def drive
    # use `Car.open` and `Car.start_engine`
  end
end

class Mechanic
  def do_stuff
    # use `Car.change_engine`
  end
end

...this code violates the principle because the Car class has methods that are partially used by both Driver and Mechanic.

To fix this we split our interface into two interfaces...

class Car
  def open; end
  def start_engine; end
end

class CarInternals
  def change_engine; end
end

class Driver
  def drive
    # use `Car.open` and `Car.start_engine`
  end
end

class Mechanic
  def do_stuff
    # use `CarInternals.change_engine`
  end
end

Dependency Inversion Principle

Objects should depend on abstractions. If they do so then the implementation of the abstractions can be changed without safely without affecting the code consuming the abstractions.

One way to conform to this principle is to use "dependency injection", which we saw this used in the solution for OCP (Open/Closed Principle).

Dependency Injection is one part of the solution. See this example: https://gist.github.com/Integralist/5763515 and you'll notice that DIP relies on the use of Interfaces (or in Ruby's case "duck typing") to decouple the consuming code and the injected dependency (e.g. using an Interface allows any object that implements that interface to be injected into the consuming object).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment