DEV Community

Wali Queiroz
Wali Queiroz

Posted on

1

Princípios SOLID em GoLang - Interface Segregation Principle (ISP)

E aí, galerinha!

Hoje vamos falar sobre o quarto princípio SOLID: o Interface Segregation Principle. Para ver os artigos onde detalho os 3 primeiros, acesse:

  1. Princípios SOLID em GoLang - Single Responsability Principle (SRP)
  2. Princípios SOLID em GoLang - Open/Closed Principle (OCP)
  3. Princípios SOLID em GoLang - Liskov Substitution Principle (LSP)

GoLang - Interface Segregation Principle

O Interface Segregation Principle ou Princípio da Segregação de Interfaces é bem simples de ser compreendido. Ele postula que "nenhum cliente deve ser forçado a depender de métodos que não utiliza". "Cliente", nesse contexto, não são os usuários finais do software, mas os módulos que dependem de uma interface dentro do sistema.

Em outras palavras, o princípio prega que nossas interfaces devem ser concisas, de forma que não precisemos fazer com que as classes ou structs implementem métodos apenas para "respeitar o contrato".

Semelhante ao Princípio da Responsabilidade Única, o objetivo do ISP é reduzir os efeitos colaterais e a frequência das alterações necessárias, dividindo o software em várias partes independentes.

Violando o ISP

Ninguém quer escrever código ruim, mas manter princípios de design nem sempre é fácil ou intuitivo. Com o crescimento do sistema em usuários e funcionalidades, cada mudança se torna um desafio. Às vezes, a solução rápida é adicionar um novo método a uma interface existente, mesmo que não tenha relação direta. Isso pode resolver problemas quando o requisito é o prazo (o que a gente sabe que acontece ⛓️💼💔), mas polui a interface e gera contratos confusos com métodos de diferentes responsabilidades.

Vamos dar uma olhada em um exemplo onde esse tipo de equívoco poderia acontecer.

Começaremos definindo uma interface chamada Phone que representa um telefone celular com funcionalidades básicas, como discar num teclado físico e fazer e receber chamadas.

package main
import "fmt"
type Phone interface {
MakeCall(number string)
ReceiveCall(caller string)
DialPhysicalKeypad(key string)
}
type SimplePhone struct {
// campos e métodos específicos para telefones simples
}
func (sp *SimplePhone) MakeCall(number string) {
fmt.Println("Calling", number)
}
func (sp *SimplePhone) ReceiveCall(caller string) {
fmt.Println("Receiving call from", caller)
}
func (sp *SimplePhone) DialPhysicalKeypad(key string) {
fmt.Println("Dialing", key, "on physical keypad")
}
func performPhoneActions(phone Phone) {
phone.DialPhysicalKeypad("*")
phone.MakeCall("123456789")
phone.ReceiveCall("John")
}
func main() {
fmt.Println("Scenario 1: Simple Phone")
simplePhone := &SimplePhone{}
performPhoneActions(simplePhone)
}

Nesta etapa, temos uma interface clara e uma implementação para telefones simples. Tudo funciona bem até aqui.

Vamos supor que agora o sistema precise acomodar funcionalidades de smartphones. Uma abordagem inicial para lidar com isso poderia ser incluir todos os métodos de smartphones na interface do telefone básico, como demonstrado abaixo:

package main
import "fmt"
type Phone interface {
MakeCall(number string)
ReceiveCall(caller string)
DialPhysicalKeypad(key string)
TakePhoto()
SendEmail(recipient, subject, body string)
}
type SimplePhone struct {
// campos e métodos específicos para telefones simples
}
func (sp *SimplePhone) MakeCall(number string) {
fmt.Println("Calling", number)
}
func (sp *SimplePhone) ReceiveCall(caller string) {
fmt.Println("Receiving call from", caller)
}
func (sp *SimplePhone) DialPhysicalKeypad(key string) {
fmt.Println("Dialing", key, "on physical keypad")
}
func (sp *SimplePhone) TakePhoto() {
panic("Simple phones can't take photos")
}
func (sp *SimplePhone) SendEmail(recipient, subject, body string) {
panic("Simple phones can't send emails")
}
type AdvancedPhone struct {
// campos e métodos específicos para smartphones
}
func (ap *AdvancedPhone) MakeCall(number string) {
fmt.Println("Calling", number)
}
func (ap *AdvancedPhone) ReceiveCall(caller string) {
fmt.Println("Receiving call from", caller)
}
func (ap *AdvancedPhone) DialPhysicalKeypad(key string) {
panic("Smartphones don't have physical keypads")
}
func (ap *AdvancedPhone) TakePhoto() {
fmt.Println("Taking a photo")
}
func (ap *AdvancedPhone) SendEmail(recipient, subject, body string) {
fmt.Printf("Sending email to %s with subject: %s\n%s\n", recipient, subject, body)
}
func performPhoneActions(phone Phone) {
phone.DialPhysicalKeypad("*")
phone.MakeCall("123456789")
phone.ReceiveCall("John")
phone.TakePhoto()
phone.SendEmail("example@mail.com", "Hello", "How are you?")
}
func main() {
simplePhone := &SimplePhone{}
performPhoneActions(simplePhone)
advancedPhone := &AdvancedPhone{}
performPhoneActions(advancedPhone)
}

Como a interface Phone mudou e mais métodos foram adicionados, todos os seus clientes precisam ser atualizados. O problema é que implementá-los é indesejado e pode levar a muitos efeitos colaterais. Neste ponto, estamos forçando a struct SimplePhone a implementar métodos como TakePhoto() e SendEmail(), mesmo que eles sejam irrelevantes para esse tipo de telefone. O mesmo ocorre com a struct AdvancedPhone, que implementa o método DialPhysicalKeypad() mesmo que smartphones geralmente não o tenham teclado físico. Nesse cenário, as implementações têm que lidar com as funcionalidades não suportadas lançando panics (em outras linguagens a gente vê bastante o uso de exceções nessas situações).

Para evitar a interrupção abrupta de um caso de uso, o código que utiliza essas implementações (como a função performPhoneActions) teria que ser modificado para verificar a capacidade do telefone antes de chamar cada método. Isso funciona? Funciona! Mas não é nada escalável e aumenta bastante a chance de introduzirmos um bug mexendo em código que já existia sem necessidade. Além disso, como já vimos, essa abordagem quebra pelo menos mais dois princípios SOLID, o OCP e o LSP.

Aplicando o Interface Segregation Principle

Na seção anterior poluímos intencionalmente a interface Phone e violamos o ISP. Vejamos como corrigi-lo.

Observando o código com problemas, podemos ver que os métodos MakeCall e ReceiveCall são necessários em ambas as implementações. Por outro lado, DialPhysicalKeypad só é necessário em telefones básicos, e TakePhoto e SendEmail são apenas para Smartphones. Com isso resolvido, vamos dividir as interfaces e aplicar o ISP.

Assim, agora temos uma interface comum:

type Phone interface {
MakeCall(number string)
ReceiveCall(caller string)
}

Mais duas para os respectivos tipos de telefone:

type BasicPhone interface {
Phone
DialPhysicalKeypad(key string)
}
type Smartphone interface {
Phone
TakePhoto()
SendEmail(recipient, subject, body string)
}

E as respectivas implementações:

package main
import "fmt"
type SimplePhone struct {
// campos e métodos específicos para telefones simples
}
func (sp *SimplePhone) MakeCall(number string) {
fmt.Println("Calling", number)
}
func (sp *SimplePhone) ReceiveCall(caller string) {
fmt.Println("Receiving call from", caller)
}
func (sp *SimplePhone) DialPhysicalKeypad(key string) {
fmt.Println("Dialing", key, "on physical keypad")
}
type AdvancedPhone struct {
// campos e métodos específicos para smartphones
}
func (ap *AdvancedPhone) MakeCall(number string) {
fmt.Println("Calling", number)
}
func (ap *AdvancedPhone) ReceiveCall(caller string) {
fmt.Println("Receiving call from", caller)
}
func (ap *AdvancedPhone) TakePhoto() {
fmt.Println("Taking a photo")
}
func (ap *AdvancedPhone) SendEmail(recipient, subject, body string) {
fmt.Printf("Sending email to %s with subject: %s\n%s\n", recipient, subject, body)
}
func performBasicPhoneActions(phone BasicPhone) {
phone.DialPhysicalKeypad("*")
phone.MakeCall("123456789")
phone.ReceiveCall("John")
}
func performSmartphoneActions(phone Smartphone) {
phone.MakeCall("123456789")
phone.ReceiveCall("John")
phone.TakePhoto()
phone.SendEmail("example@mail.com", "Hello", "How are you?")
}
func main() {
fmt.Println("Scenario 1: Simple Phone")
simplePhone := &SimplePhone{}
performBasicPhoneActions(simplePhone)
fmt.Println("Scenario 2: Advanced Phone")
advancedPhone := &AdvancedPhone{}
performSmartphoneActions(advancedPhone)
}

Com essa abordagem, conseguimos corrigir muitos problemas do código anterior. Agora, diferentes tipos de telefones têm contratos que fazem sentido para eles. Não precisamos mais nos preocupar com métodos desnecessários ou pânicos. E se quisermos adicionar um novo tipo de telefone ou funcionalidade, podemos fazer isso sem estragar o que já estava funcionando.

Onde aplicar no mundo real?

Imagine que você está desenvolvendo uma aplicação de comércio eletrônico que precisa lidar com diferentes gateways de pagamento para processar transações. Cada gateway tem suas próprias capacidades e limitações. Como você pode garantir que sua aplicação seja flexível e adaptável a essas diferenças?

Inicialmente, você pode criar uma interface única que engloba todas as operações possíveis:

type PaymentGateway interface {
// Authorize autoriza uma transação com um cartão de crédito
Authorize(card CreditCard, amount float64) (Authorization, error)
// Capture captura uma transação previamente autorizada
Capture(auth Authorization, amount float64) (Capture, error)
// Refund reembolsa uma transação previamente capturada
Refund(capt Capture, amount float64) (Refund, error)
// Cancel cancela uma transação previamente autorizada
Cancel(auth Authorization) error
}

No entanto, a disponibilidade de cada recurso pode variar de uma solução para outra. Alguns gateways de pagamento podem ser mais limitados em termos de funcionalidades e recursos.

Para aplicar o ISP nesse cenário, é sensato separar as operações em interfaces mais específicas, de acordo com a disponibilidade de recursos em cada gateway. Isso permite que você modele de forma precisa o comportamento esperado para cada serviço de pagamentos.

// Authorizer é uma interface que define a operação de autorização
type Authorizer interface {
Authorize(card CreditCard, amount float64) (Authorization, error)
}
// Capturer é uma interface que define a operação de captura
type Capturer interface {
Capture(auth Authorization, amount float64) (Capture, error)
}
// Refunder é uma interface que define a operação de reembolso
type Refunder interface {
Refund(capt Capture, amount float64) (Refund, error)
}
// Canceller é uma interface que define a operação de cancelamento
type Canceller interface {
Cancel(auth Authorization) error
}

Formas práticas de seguir o ISP

Antes que vocês saiam por aí quebrando toda e qualquer interface em várias de um método só, lembrem-se: não é assim que a banda toca. A depender do seu contexto, pode ser que faça sentido ter uma interface grande. No mundo das interfaces, ao contrário do que podemos ser levados a pensar quando estamos começando a estudar boas práticas, não é a fragmentação que importa, é a coesão. Em vez de se perguntar se suas classes/structs estão lidando com interfaces "inchadas", questionem: "Esses métodos fazem sentido juntos?". Se a resposta for um enigmático "não", refatore o quanto antes!

That's all, folks!

Refrências:

AWS Security LIVE!

Join us for AWS Security LIVE!

Discover the future of cloud security. Tune in live for trends, tips, and solutions from AWS and AWS Partners.

Learn More

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay