Entendendo SOLID de uma vez por todas | Parte 04 – (ISP)



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 struct BasicPrinter ao criar a func que é um método Print()

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

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