Skip to content

Instantly share code, notes, and snippets.

@rcoproc
Forked from Integralist/1. SRP .md
Last active April 17, 2020 02:12
Show Gist options
  • Save rcoproc/d38902ebd0fc690d765fea8c0cd2f346 to your computer and use it in GitHub Desktop.
Save rcoproc/d38902ebd0fc690d765fea8c0cd2f346 to your computer and use it in GitHub Desktop.
S.O.L.I.D principles in Ruby

Single responsibility principle

"Em outras palavras, todas as classes complicadas devem ser divididas em classes menores, responsáveis ​​por um comportamento específico, facilitando o entendimento e a manutenção da base de código." https://rubygarage.org/blog/solid-principles-of-ood

Probably the most well known principle, and one that should try to adhere to most of the time.

Let's say you have this code:

class AuthenticatesUser
  def authenticate(email, password)
    if matches?(email, password)
     do_some_authentication
    else
      raise NotAllowedError
    end
  end

  private
  def matches?(email, password)
    user = find_from_db(:user, email)
    user.encrypted_password == encrypt(password)
  end
end

The AuthenticatesUser class is responsible for authenticating the user as well as knowing if the email and password match the ones in the database. It has two responsibilities, and according to the principle it should only have one, let's extract one.

class AuthenticatesUser
  def authenticate(email, password)
    if MatchesPasswords.new(email, password).matches?
     do_some_authentication
    else
      raise NotAllowedError
    end
  end
end

class MatchesPasswords
  def initialize(email, password)
     @email = email
     @password = password
  end

  def matches?
     user = find_from_db(:user, @email)
    user.encrypted_password == encrypt(@password)
  end
end

I've used a refactoring technique called Extract Class and then use it on the class I already had, this is called sharing behaviour through composition.

Open/closed principle

"Simplificando, todos os módulos, classes, métodos etc. devem ser projetados de maneira modular para que você (ou outros desenvolvedores) possam alterar o comportamento do sistema sem alterar o código-fonte." https://rubygarage.org/blog/solid-principles-of-ood

class Report
  def body
     generate_reporty_stuff
  end

  def print
     body.to_json
  end
end

This code violates OCP, because if we want to change the format the report gets printed, you need to change the code of the class. Let's change it then.

class Report
  def body
     generate_reporty_stuff
  end

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

This way changing the formatter is as easy as:

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

Thus I have extended the functionality without modifying the code. In this example, I have used a technique called Dependency Injection, but many other can apply.

Liskov substitution principle

É assim que Robert Martin define o princípio do LSP: "As subclasses devem adicionar ao comportamento de uma classe base, não substituí-lo." Em uma interpretação mais informal, o princípio afirma que as instâncias pai devem ser substituíveis por uma das instâncias filho, sem criar nenhum comportamento inesperado ou incorreto. https://rubygarage.org/blog/solid-principles-of-ood

This principle applies only to inheritance, so let's see an example of inheritance that breaks it:

class Animal
  def walk
     do_some_walkin
  end
end

class Cat < Animal
  def run
    run_like_a_cat
  end
end

In order to comply with the LSP, as Bob Martin puts it:

Subtypes must be substitutable for their base types

So, they must have the same interface. Since ruby does not have abstract methods, we can do it like so:

class Animal
  def walk
     do_some_walkin
  end

  def run
    raise NotImplementedError
  end
end

class Cat < Animal
  def run
    run_like_a_cat
  end
end

Interface segregation principle

Os clientes não devem depender dos métodos que não usam. Várias interfaces específicas do cliente são melhores que uma interface generalizada. ” Simplificando, as classes principais devem ser segregadas em classes específicas menores, para que seus clientes usem apenas os métodos necessários. https://rubygarage.org/blog/solid-principles-of-ood

Simply put, this principle states that:

when a client depends upon a class that contains interfaces that the client does not use, but that other clients do use, then that client will be affected by the changes that those other clients force upon the class

This one is simpler to demonstrate, if you have a class that has two clients (objects using it):

class Car
  def open
  end

  def start_engine
  end

   def change_engine
   end
end

class Driver
  def drive
    @car.open
    @car.start_engine
  end
end

class Mechanic
  def do_stuff
    @car.change_engine
  end
end

As you can see, our Car class has an interface that's used partially by both the Driver and the Mechanic. We can improve our interface like so:

class Car
  def open
  end

  def start_engine
  end
end

class CarInternals
   def change_engine
   end
end

class Driver
  def drive
    @car.open
    @car.start_engine
  end
end

class Mechanic
  def do_stuff
    @car_internals.change_engine
  end
end

By splitting the interface into two, we can comply to the ISP.

Dependency inversion principle

O Princípio da inversão de dependência (DIP) sugere que “os módulos de alto nível não devem depender dos módulos de baixo nível. Ambos os módulos devem depender de abstrações. Além disso, as abstrações não devem depender de detalhes. Os detalhes dependem de abstrações. ” https://rubygarage.org/blog/solid-principles-of-ood

Directly from the Wikipedia page:

Abstractions should not depend upon details. Details should depend upon abstractions.

Let's go back to the first example on the OCP and change it a bit:

class Report
  def body
     generate_reporty_stuff
  end

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

class JSONFormatter
  def format(body)
     ...
  end
end

Now there is a formatter class, but I've hardcoded it on the Report class, thus creating a dependency from the Report to the JSONFormatter. Since the Report is a more abstract (high-level) concept than the JSONFormatter, we're effectively breaking the DIP.

We can solve it the exact same way with solved it on the OCP problem, with dependency injection:

class Report
  def body
     generate_reporty_stuff
  end

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

This way the Report does not depend on the JSONFormatter and can use any type of formatter that has a method called format (this is known as duck typing).

Another thing of note is that we've used, once again, dependency injection to solve a problem. This technique is a very powerful one when our goal is decoupling objects, and even though it has the same initials as the dependency inversion principle (vs dependency injection pattern), they are completely different concepts.

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