This content originally appeared on DEV Community and was authored by Rafael Honório
Hey pessoal, continuando a série sobre SOLID, hoje vamos falar sobre o Interface Segregation Principle (ISP). Esse é, na minha opinião, o conceito mais tranquilo dos cinco princípios. O próprio nome já entrega bastante sobre o que vamos conversar, e ainda vou aproveitar para trazer alguns insights interessantes sobre interfaces.
Resumo
O conceito de interface surgiu no final da década de 80, após as linguagens Simula e Smalltalk já terem introduzido boa parte dos fundamentos que a gente entende hoje como orientação a objetos.
Foi o Bertrand Meyer quem apresentou formalmente o conceito de interface em seu livro Object-Oriented Software Construction, publicado em 1988. Nele, Meyer definiu a ideia de contratos entre componentes de software na linguagem Eiffel, e desde então essas técnicas e conceitos passaram a ser aplicados amplamente em outras linguagens como Java, C#, Ruby, entre outras.
A intenção de criar um contrato (interface) parte da ideia de que determinado módulo do software possui características comuns que podem ser reaproveitadas em vários contextos diferentes. Exemplos clássicos disso seriam:
- Interação com banco de dados
- Interação com cache
- Interação com message broker
- Interação com subtipos (herança)
Todas essas situações tem em comum dois pontos em comum;
1 – Existe uma base de comportamento que vai se repetir em todas, ou quase todas as vezes.
2 – Cada implementação vai ser usada em vários pontos do seu código.
A definição de Interface Segregation Principle (IPS) é:
Nenhum cliente deve ser forçado a depender de métodos que não utiliza.
Ou seja, a tendência é que interface defina comportamentos de uma determinada área do seu software com intuíto de criar mais coesão e desacoplamento entre os módulos.
Vamos aos exemplos
type Printer interface {
Print()
PrintColorFull()
Fax()
Scan()
}
type BasicPrinter struct{}
func (BasicPrinter) Print() { fmt.Println("Printing...") }
nesse caso não existe implementação dos outros métodos, isso viola o ISP.
O correto seria ter interfaces específicas para esse caso
type Printable interface { Print() }
type PrintableColor interface { PrintColorFull() }
type Scannable interface { Scan() }
type Faxable interface { Fax() }
type BasicPrinter struct{}
func (BasicPrinter) Print() { fmt.Println("Printing...") }
Agora não infringe o ISP, talvez fique um pouco complicado de entender para quem não familiaridade com Golang mas o que aconteceu nesse código foi:
- Definição de todas interfaces
- Definição de uma struct chamada
BasicPrinter
- Implementação da interface
Printable
na structBasicPrinter
ao criar afunc
que é um métodoPrint()
Obs: Esses vínculos acontecem de forma implícita em Go.
A seguir, apresento outro exemplo em Elixir que reproduz a mesma ideia. A diferença é que a linguagem permite retornar tipos genéricos, o que implica implementar esses comportamentos em outros módulos sem necessariamente devolver o mesmo tipo de dado.
Em Elixir, a implementação de uma “interface” recebe o nome de behavior ou callback.
defmodule MultiFunctionDevice do
@callback print() :: any()
@callback fax() :: any()
@callback scan() :: any()
end
defmodule SimplePrinter do
@behaviour MultiFunctionDevice
def print, do: IO.puts("Printing")
def fax, do: :not_implemented
def scan, do: :not_implemented
end
A mesma ideia se aplica aqui. Mesmo que a função seja implementada, o retorno é :not_implemented
. Devolver :not_implemented
ou criar funções vazias é um baita sinal de que a interface foi mal pensada e está quebrando o princípio, mesmo respeitando a assinatura das funções da interface.
defmodule PrinterOnly do
@callback print() :: any()
end
A abordagem é a mesma, dividir a interface outras específicas.
Conclusão
Não existe mistério quando falamos de interfaces. O ISP defende a divisão de grandes interfaces em contextos menores e mais coesos, algo que vale em várias situações como:
- Herança entre objetos
- Interações com contextos externos (cache, message broker, banco de dados)
- Erro handling estruturado
Com isso a gente ganha:
- Redução no acoplamento
- Código mais coeso
- Código mais flexível a mudanças
Código de Referência
- Exemplos em Elixir: solid_elixir_examples
- Exemplos em Go: solid-go-examples
Espero que tenha ficado claro os conceitos, se caso ficou alguma dúvida, fique a vontade para deixar um comentário.
This content originally appeared on DEV Community and was authored by Rafael Honório