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:
- Princípios SOLID em GoLang - Single Responsability Principle (SRP)
- Princípios SOLID em GoLang - Open/Closed Principle (OCP)
- Princípios SOLID em GoLang - Liskov Substitution Principle (LSP)
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.
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:
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:
Mais duas para os respectivos tipos de telefone:
E as respectivas implementações:
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:
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.
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:
Top comments (0)