Introdução
Em qualquer projeto desenvolvido em times, será necessário em algum momento resolver divergências sobre como solucionar um problema. Por mais que todos concordem com a mesma solução, cada pessoa poderá escrevê-la à sua maneira. Para que o time possa continuamente entregar valor com qualidade, é necessário estabelecer um conjunto de regras e orientações de como o trabalho deve ser executado.
Na programação orientada a objetos, existem padrões que servem de guia para que as pessoas escrevam um código mais compreensível, flexível e fácil de manter. Esse conjunto de 5 princípios foi introduzido por Robert C. Martin, o “Uncle Bob”, em um paper denominado “Design Principles and Design Patterns”. Posteriormente, Michael Feathers cunhou o acrônimo SOLID:
- Single Responsibility Principle (Responsabilidade Única)
- Open/Closed Principle (Aberto e Fechado)
- Liskov Substitution Principle (Substituição de Liskov)
- Interface Segregation Principle (Segregação de Interface)
- Dependency Inversion Principle (Inversão de Dependência).
Devs que procuram seguir os princípios do SOLID produzem códigos mais fáceis de serem refatorados e com menos "code smells".
Este é o primeiro de uma série de artigos nos quais descreveremos em detalhes cada um dos princípios do SOLID. Vamos mostrar exemplos de cada um dos princípios utilizando a linguagem de programação Ruby e, para facilitar a compreensão, usaremos somente Plain Old Ruby Object, ou seja, não estaremos trabalhando dentro de nenhum framework específico.
Single Responsibility Principle (SRP)
O Princípio da Responsabilidade Única está definido como:
“Uma classe deve ter somente uma razão para mudar”
Isso quer dizer que uma classe deve ter somente uma responsabilidade, ou seja, deve ser responsável por executar apenas uma tarefa.
Durante o processo de criação de uma aplicação os requerimentos mudam e consequentemente a responsabilidade de pelo menos uma classe pode mudar.
Quanto mais responsabilidades uma classe tiver, mais vezes ela terá que ser alterada e mais acopladas tornam-se as partes da aplicação.
Exemplo
require 'net/smtp'
class ProcessUser
def initialize(params)
@params = params
end
def call
# Process user params
user_params = {
name: "#{params[:user_name]} #{params[:user_surname]}",
email: params[:user_email]
}
# Create User
user = User.create(user_params)
# Create a message and sends
from = "First Last <person@example.com>"
to = "#{user.name} <#{user.email}>"
subject = "Welcome User"
message = <<MESSAGE_END
From: #{from}
To: #{to}
Subject: #{subject}
Mime-Version: 1.0
Content-Type: text/html
Content-Disposition: inline
<b>Welcome email</b><br/>
We welcome you to our new platform.
MESSAGE_END
Net::SMTP.start('localhost', 25) do |smtp|
smtp.send_message message, from, to
end
end
end
No exemplo acima, a classe ProcessUser
possui o método call
, que é responsável por formatar os parâmetros do usuário, criar o usuário, configurar o e-mail e enviá-lo, ou seja, essa classe possui muitas responsabilidades.
Por possuir muitas responsabilidades, ela possui muitos motivos para ser
alterada. Por exemplo, se decidirmos usar uma gem de envio de e-mail ao invés do Net::SMTP
, se o corpo do e-mail mudar, se alterarmos o modo como acessamos os valores dos parâmetros ou se adicionarmos outros atributos ao user_params
, precisaremos alterar a classe.
Agora vejamos como isso ficaria quando tentamos extrair cada responsabilidade para uma classe isolada.
Refatoração
class ProcessUser
def initialize(params)
@params = params
end
def call
user = UserFactory.create(params)
MailerService.new(user).welcome
end
end
class UserFactory
def self.create(params)
parameters = UserParamsFormatter.format(params)
User.create(parameters)
end
end
class UserParamsFormatter
def self.format(params)
{
name: "#{params[:user_name]} #{params[:user_surname]}",
email: params[user_email]
}
end
end
class MailerService
def initialize(recipient)
@recipient = recipient
end
def welcome
email = WelcomeTemplate.new(@recipient)
mailer.deliver(@recipient, email)
end
def mailer
Mailer.new
end
end
class WelcomeTemplate
# template formatted with recipients values (name/email)
end
class Mailer
# responsible for sending the e-mail (Net:SMTP)
end
Agora com as responsabilidades separadas em classes, podemos notar que cada classe tem somente uma razão para mudar.
A classe Mailer
pode utilizar uma gem ao invés do Net::SMTP
e somente ela vai precisar ser alterada; WelcomeTemplate
pode alterar as informações do e-mail; UserParamsFormatter
só precisa se preocupar em como acessar os parâmetros e quais atributos devem ser adicionados ao hash.
Considerações finais
Conseguimos quebrar uma classe que ao poucos poderia virar um God Object (classe que faz tudo e sabe tudo) para algo mais fácil de administrar, porém, acabamos criando seis classes no processo.
Dependendo do projeto isso não seria um problema, mas, se considerarmos essa classe como parte inicial de um projeto, talvez tenhamos ido longe demais.
Inicialmente todo fluxo de como a aplicação funcionava era bastante simples e realizado por apenas uma classe. Mas agora precisamos ler até 6 classes para entender o que está acontecendo.
Esse é um dos problemas quando começamos a aplicar o SOLID: nos focamos muito no que cada princípio diz e, às vezes, podemos aplicar uma abstração errada ou um código que pode vir a ser um problema no futuro (um code smell).
No caso do Single Responsibility Principle, é comum nos preocuparmos somente se a classe tem apenas uma função, mas não verificamos se ao criar as novas classes, a compreensão do código ficou comprometida. Isso acontece quando criamos classes que possuem funções muito simples (lazy class). Por exemplo: no código anterior, criamos a classe UserParamsFormatter
que é responsável por preparar os dados de entrada para a classe User
. Essa classe tem apenas a função de retornar um hash simples com os atributos do usuário.
Talvez, nesse momento, fosse melhor mover o conteúdo da classe UserParamsFormatter
para a classe UserFactory
. No futuro, se a complexidade do código aumentar para lidar com os dados antes da criação do objeto, podemos avaliar a necessidade de quebrar as responsabilidades da UserFactory
.
Existem casos em que pode ser melhor esperar o amadurecimento da aplicação para depois verificar se vale a pena ou não separar as responsabilidades das classes.
Se você estiver interessado em ler mais sobre os princípios de S.O.L.I.D., recomendamos os demais artigos dessa série:
- Open/Closed Principle (Aberto e Fechado)
- Liskov Substitution Principle (Substituição de Liskov)
- Interface Segregation Principle (Segregação de Interface)
- Dependency Inversion Principle (Inversão de Dependência)
Referências
Videos
- GORUCO 2009 - SOLID Object-Oriented Design by Sandi Metz
- S.O.L.I.D. Principles of Object-Oriented Design - A Tutorial on Object-Oriented Design