Introdução
Este é o segundo artigo de uma série na qual descreveremos em detalhes cada um dos princípios do SOLID – um conjunto de princípios que orientam o desenvolvimento de código compreensível e fácil de manter. No primeiro, discorremos sobre o Princípio da Responsabilidade Única. Aqui falaremos sobre o princípio Aberto/Fechado (Open/Closed Principle).
Open/Closed Principle (OCP)
O princípio de Aberto/Fechado propõe que entidades (classes, funções, módulos, etc.) devem ser abertas para extensão, mas fechadas para modificação.
Aberto para extensão significa que, ao receber um novo requerimento, é possível adicionar um novo comportamento. Fechado para modificação significa que, para introduzir um novo comportamento (extensão), não é necessário modificar o código existente.
Exemplo
class MailerService
def initialize(recipient)
@recipient = recipient
end
def welcome
email = WelcomeTemplate.new(@recipient)
mailer.deliver(@recipient, email)
end
def invite
email = InviteTemplate.new(@recipient)
mailer.deliver(@recipient, email)
end
private
def mailer
Mailer.new
end
end
Nesse exemplo, a classe MailerService
é responsável por enviar um e-mail com um template específico que é definido por cada método da classe.
O problema dessa classe é que a quantidade de templates existentes na aplicação corresponde à quantidade de métodos que essa classe deve implementar. Então, se a aplicação possui 20 templates, teoricamente, essa classe deveria implementar 20 métodos.
Talvez seja melhor aplicar uma refatoração nessa classe.
Primeira Refatoração
class MailerService
def initialize(recipient, template)
@recipient = recipient
@template = template
end
def deliver
email = templates[template].new(recipient)
mailer.deliver(recipient, email)
end
private
attr_reader :recipient, :template
def mailer
Mailer.new
end
def templates
{
welcome: WelcomeTemplate,
invite: InviteTemplate,
promotion: PromotionTemplate
}
end
end
A classe espera receber um symbol com o tipo de template que ela deve utilizar, buscar no método templates
e instanciá-la. Não existe mais a necessidade de implementar um novo método para cada template. Além disso, também foi removida a duplicidade de código.
Ainda assim, essa classe continua ferindo o OCP. Toda vez que um template for criado, será necessário adicioná-lo no método templates
. Logo, é necessário alterar código já existente para adicionar um novo comportamento.
Talvez seja melhor aplicar mais uma refatoração nessa classe.
Segunda Refatoração
class MailerService
def initialize(template: WelcomeTemplate)
@template = template
end
def deliver_to(recipient)
email = template.new(recipient)
mailer.deliver(recipient, email)
end
private
attr_reader :template
def mailer
Mailer.new
end
end
class WelcomeTemplate
# código da classe
end
class InviteTemplate
# código da classe
end
PromotionTemplate
# código da classe
end
Agora, ao invés de especificar os templates diretamente na classe, ela espera que seja passada uma classe de template no construtor como parâmetro: WelcomeTemplate, InviteTemplate, PromotionTemplate. Chamamos essa solução de Dependency Injection ou Injeção de Dependência.
Dependency Injection nada mais é do que passar uma dependência como argumento para uma entidade qualquer. Isso permite que o código dependa mais de uma abstração do que de algo concreto. No exemplo de código acima, a classe Mailer pode receber um template qualquer, mas por padrão ela recebe WelcomeTemplate.
Dependency Injection é apenas uma maneira de estender uma classe sem alterar seu comportamento. Existem muitas outras soluções como Decorator Pattern, Strategy Pattern, Composição e Herança.
Entre essas soluções, é importante atentar-se a Herança. Apesar da herança permitir resolver o OCP, se não houver o devido cuidado, ela pode aumentar o acoplamento do código, dificultando a manutenção, extensão e permitindo criar código que viole o Single Responsibility Principle.
Considerações Finais
OCP foca em facilitar a manutenção das entidades. A ideia é que nossas entidades comportem-se como um plugin do software, adicionando algo novo sem que isso interfira no comportamento do que já existe.
Precisamos evitar que ao alterar uma entidade, não seja necessário fazer alterações em diversos outros trechos da nossa base de código (chamamos isso de shotgun surgery).
Uma das maneiras de adicionar comportamento mantendo o OCP, seria utilizar herança. Poderíamos ter uma classe User
à qual queremos adicionar uma funcionalidade de usuário administrador. Para isso, criamos uma segunda classe Admin
que herda de User
somente o necessário e em Admin
adicionamos a nova funcionalidade.
O problema é que a herança pode fazer com que aumente o acoplamento entre as classes, se dependermos mais da forma como a classe é implementada, do que da maneira como ela é representada no sistema (sua abstração).
O ideal é que a entidade dependa de uma abstração que forneça uma interface para interagirmos, sem nos importarmos como ela funciona.
Se você estiver interessado em ler mais sobre os princípios de S.O.L.I.D., recomendamos os demais artigos dessa série:
- Single Responsibility Principle (Responsabilidade Única)
- 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