- Single Responsibility Principle
- Open/Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion 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.
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.
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
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
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).