Introdução
Este é o quarto artigo de uma série na qual descreveremos cada um dos princípios do SOLID – conjunto de princípios que orientam o desenvolvimento de código compreensível e fácil de manter. No artigo anterior, discorremos sobre o Princípio de Substituição de Liskov. Aqui falaremos sobre o princípio de Segregação de Interface (Interface Segregation Principle).
Interface Segregation Principle (ISP)
O Princípio de Segregação de Interface diz que interfaces específicas são melhores do que uma única interface de propósito geral.
Em programação orientada a objetos, quando falamos de interface, estamos falando do conjunto de métodos que um objeto expõe, ou seja, das maneiras como nós podemos interagir com esse objeto. Toda mensagem (ou chamada de método) que um objeto recebe constitui uma interface.
A interface funciona como um contrato: nós definimos o comportamento da interface na forma de diferentes métodos que ela possui. Cada classe que deseja compartilhar o comportamento dessa interface precisa implementar os métodos dela. Quando a classe utiliza uma interface, ela assina esse contrato dizendo que irá implementar todos os métodos dessa interface.
As interfaces são comumente utilizadas por linguagens de programação estaticamente tipadas (como Java e C#) para adicionar novos comportamentos às classes, isso é feito utilizando a palavra reservada interface
.
Linguagens como Ruby ou Python, definem sua interface de forma implícita, através dos métodos que a classe implementa. Comumente nos referimos a essa propriedade como Duck Typing. A ideia por trás do Duck Typing é que o objeto é representado pelos métodos que ele possui e pelo que esses métodos fazem.
Outra forma de implementar interfaces em Ruby é utilizando herança. Nós podemos fazer com que a classe base lance exceções para cada método, de forma que a classe herdeira é obrigada a implementar esses métodos. O mesmo pode ser obtido com mixins. Dessa maneira, ao herdar uma classe ou incluir um módulo, a classe herdeira precisa implementar o método.
O ISP é violado quando uma classe é obrigada a implementar métodos que não utiliza. Por esse motivo, o ISP diz que as interfaces devem ser específicas (ou pequenas) para que as classes possam implementar somente os comportamentos necessários. Caso contrário, erros serão lançados.
Exemplo
# Simple mailer
class Mailer; end
# Mailer that can send attachments
class AttachmentMailer; end
class MailerService
def initialize(recipient)
@recipient = recipient
end
def deliver_to(message, send_attachment)
if send_attachment
attachment_mailer.enqueue(recipient, message, message.attachments)
else
mailer.deliver(recipient, message)
end
end
private
attr_reader :recipient
def mailer
Mailer.new
end
def attachment_mailer
AttachmentMailer.new
end
end
Nesse exemplo temos uma classe Mailer
, para e-mails simples, AttachmentMailer
, para lidar com e-mails que possuem anexos e MailerService
, que é responsável por enviar um e-mail simples ou com anexo.
MailerService
recebe um destinatário (recipient
) no construtor. Ela também possui um método deliver_to
que recebe uma mensagem e um boolean (send_attachment
), que indica se deve ou não enviar um e-mail com anexo.
A assinatura desse método nos força a especificar na chamada do método um boolean (send_attachment
) que indica se queremos enviar um e-mail com anexo ou não. Além disso, os mailers utilizam assinatura de métodos (nome e quantidade de parâmetros) distintos para o mesmo fim (enqueue
e deliver
).
Vejamos agora como podemos melhorar esse código.
Refatoração
class BaseMailer
def deliver(recipient, message)
raise NotImplementedError
end
end
class Mailer < BaseMailer
def deliver(recipient, message)
# code
end
end
class AttachmentMailer < BaseMailer
def deliver(recipient, message)
attachments = message.attachments
deliver_to(recipient, message, attachments)
end
private
def deliver_to(recipient, message, attachments)
# sends email
end
end
class MailerService
def initialize(recipient)
@recipient = recipient
end
def deliver_to(message)
mailer.deliver(recipient, message)
end
def deliver_with_attachment(message)
attachment_mailer.deliver(recipient, email)
end
private
attr_reader :recipient
def mailer
Mailer.new
end
def attachment_mailer
AttachmentMailer.new
end
end
Primeiramente, criamos um BaseMailer
para garantir que Mailer
e AttachmentMailer
utilizem a mesma assinatura de método. Enquanto o código de Mailer
permanece inalterado, o de AttachmentMailer
sofreu pequenos ajustes, mas sem alterar o comportamento prévio.
Na classe MailerService
, no lugar de passar um boolean, agora podemos dizer qual método queremos utilizar: o deliver_to
para o Mailer
e deliver_with_attachment
para AttachmentMailer
. Essa separação permite que tenhamos métodos bem definidos e sem a necessidade de parâmetros extras que aumentam a complexidade do método.
Para esse exemplo, usar herança para forçar a implementação de um método funcionou, mas a medida que um projeto evolui, talvez seja melhor rever se essa herança está aumentando o acoplamento das classes.
Essa refatoração deixou os métodos duplicados e dependentes das classes Mailer
e AttachmentMailer
. No próximo artigo veremos como o Princípio de Inversão de Dependência pode resolver esse problema.
Considerações finais
Em resumo o ISP propõe que, ao definirmos uma interface, ela tenha poucos métodos e que sejam específicos dos escopos que estamos trabalhando.
Em Ruby temos basicamente duas maneiras de implementar interfaces: por meio de herança ou de mixins. Uma alternativa seria simplesmente implementar um método e torcer para que as outras classes também o implementem.
Para forçar a implementação podemos definir os métodos de forma que lancem exceções nas classes bases (herança) ou nos módulos (mixins). Se não utilizarmos as opções anteriores, podemos adicionar testes compartilhados (shared_examples
do rspec) e adicioná-los aos testes unitários das classes que queremos que implementem a interface. Dessa forma conseguimos garantir que todos os métodos serão implementados.
Ao separarmos em pequenas interfaces, conseguimos reaproveitar parte do código e utilizar só o que é realmente necessário em cada caso. Isso torna nossas classes mais coesas, pois temos comportamentos e responsabilidades bem definidas.
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)
- Open/Closed Principle (Aberto e Fechado)
- Liskov Substitution Principle (Substituição de Liskov)
- 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 ### Livros e Artigos
- The Principles of OOD - Uncle Bob
- Back to Basics: SOLID
- SOLID Principles made easy
- SOLID - Single Responsibility Principle by example
- SOLID Principles #4 - Interface Segregation Principle
- Livro - Ruby Science, por Thoughtbot
- Livro - Practical Object-Oriented Design in Ruby, por Sandi Metz