Introdução
Se você é um desenvolvedor ou desenvolvedora, e possui um certo interesse pela área de engenharia de software, provavelmente já ouviu falar de um termo chamado SOLID.
Caso você nunca tenha ouvido falar, não se preocupe, o primeiro artigo anterior desta série possui uma introdução a respeito e os demais artigos deta série você pode encontrae em:
- Swift: Princípio da Responsabilidade Única [Artigo 1]
- Swift: Princípio Aberto-Fechado [Artigo 2]
- Swift: Príncipio de substituição de Liskov [Artigo 3]
Princípio de Segregação de Interface (ISP)
Como o nome sugere, esse princípio diz que devemos segregar as interfaces, ou seja, separá-las em várias.
A definição usada pelo Uncle Bob em seu artigo (que pode ser encontrada aqui) é que Clientes não devem ser forçados a depender de interfaces que eles não utilizam.
Esta definição é particularmente especial para mim, porque embora pareça muito fácil de entender, confesso que quando comecei na área do desenvolvimento usando POO (Programação Orientada a Objetos) eu nunca pensaria nisso. Eu diria que quando você lê ou ouve sobre segregar interfaces, faz todo o sentido, mas quando você está começando a programar, provavelmente não vai pensar nisso. E tudo bem! É normal!
Vamos falar sobre os problemas em não seguir esse princípio na próxima seção, com alguns exemplos, mas é importante dar algumas breves introduções antes disso.
ISP – Problemas Desrespeitando este Princípio
Em primeiro lugar, um termo comumente usado pelo Uncle Bob em seu artigo, que nos ajudará a tornar as coisas mais claras, é “Fat Interfaces”. Traduzindo ao pé da letra, interfaces gordas.
Elas basicamente são interfaces (ou protocolos em Swift) que não são consideradas coesas. Isso significa que as interfaces podem ter mais métodos/funções do que deveriam em termos semânticos para o cliente que as consumirá. Relacionando isso com a definição do ISP, basicamente interfaces gordas são interfaces que não seguem este princípio.
Esse tipo de interface gera alguns problemas (alguns deles já vistos no artigo de SRP), como refatoração desnecessária e necessidade de recompilar e retestar quando mudanças são necessárias.
Isso acontece porque esse tipo de interface acaba gerando acoplamentos desnecessários e, quando uma mudança em uma interface gorda é necessária, todas as classes que a implementam terão que ser compiladas e testadas novamente, e isso leva tempo. Dependendo de quão complexo e grande é o software de que estamos falando, isso pode tomar uma grande quantidade de tempo e ser doloroso.
Além disso, implementar interfaces gordas também pode dificultar a compreensão e a testabilidade do seu software. Em termos de compreensão, porque as coisas provavelmente não farão sentido semanticamente. No caso de testabilidade, teremos que criar “mocks” e “spies” maiores, que terão que implementar todos os métodos desnecessários e não utilizados exigidos por esta interface gorda.
Finalmente, quando temos um software que possui várias interfaces gordas, se não prestarmos atenção e dermos a isso a importância que devemos, elas tenderão a se tornar um padrão e aumentar, tanto a quantidade de métodos em cada interface já implementada, quanto o número de novas interfaces com este problema.
Exemplo em Swift
Considere o exemplo a seguir, onde temos duas classes demonstrativas, Documento e PDF. Elas são apenas utilizadas para representar um exemplo simples onde podemos quebrar este princípio.
A classe Document tem um nome e um conteúdo que representa o documento, enquanto a classe PDF possui um documento e cria um arquivo PDF com ele, mas não estamos implementando essa criação, pois não é o objetivo do nosso exemplo.
A seguir, descrevemos o protocolo Machine (nossa “interface gorda”), uma interface simples que algumas classes do tipo máquina irão implementar, com alguns métodos que usarão um objeto Document como único parâmetro.
Por fim, temos três classes que implementam esse protocolo: FaxMachine, NewIphone, UltraMachine.
A primeira (FaxMachine) só é capaz de implementar o método fax(document: Document), pois não faria sentido implementar os outros métodos do protocolo. Mas, uma vez que esta classe implementa o protocolo e é forçada a implementar todos os seus métodos, ela retornará nil para métodos que não fazem sentido implementar.
A segunda (NewIphone) é capaz de converter um documento em PDF ou UIImage por meio dos métodos convert(document: Document) -> PDF? e convert(document: Document) -> UIImage?. No entanto, pelo mesmo motivo, ela não implementa de fato o método fax(document: Document).
Por fim, a última (UltraMachine) é capaz de fazer todas as tarefas anteriores, e portanto implementar todos os métodos de fato.
Problemas em quebrar o ISP
Vamos falar sobre os problemas que podemos observar nesse exemplo acima.
Acoplamento
O primeiro problema que podemos observar aqui é o acoplamento gerado pelo protocolo Machine, por não ser coeso. Isso acontece porque ele tem duas responsabilidades diferentes (princípio de responsabilidade única – SRP novamente aqui!). Tem a responsabilidade de converter um documento em arquivo/imagem, e tem a responsabilidade de enviar um fax com o documento.
Por causa desse acoplamento, se precisarmos de algumas alterações no método fax(document: Document), a classe NewIphone terá que ser recompilada desnecessariamente, assim como toda estrutura que depende disso. O mesmo acontecerá para seus testes, que terão que ser re-executados.
Compreensão e Testabilidade Dificultadas
Apesar de não ilustrar o problema que mencionamos na introdução, sobre interfaces gordas serem mais difíceis de testar e entender, já que é apenas um exemplo, e foi construído de tal modo que fosse simples e pequeno para facilitar o entendimento do leitor, tenha em mente que se tivéssemos aqui não apenas três métodos no protocolo, mas dez, quinze ou até mais métodos, isso definitivamente aconteceria. Ou seja, seria dificultaria os testes e também o entendimento.
Retornos Opcionais
Outra coisa interessante a ser mencionada aqui é sobre a maneira como esse tipo de implementação geralmente acontece ao se construir uma solução. Se tentarmos pensar sobre as implementações concretas (classes) primeiro, por exemplo a FaxMachine, a NewIphone e a UltraMachine, e nas interfaces depois disso, provavelmente teremos a tendência de tentar ajustar a interface para as classes concretas. Por ajustar, quero dizer criar apenas uma interface e colocar todos os métodos lá. Por outro lado, se pensarmos nas interfaces de antemão, isso provavelmente não acontecerá, como veremos na próxima seção.
Uma vez que hipoteticamente usamos esta primeira abordagem para construir essa solução do exemplo, e tentamos ajustar todos os métodos em um protocolo, tivemos que ter retornos opcionais em alguns dos métodos, porque nem todas as implementações concretas deste protocolo teriam a capacidade de retornar o objeto necessário.
Por isso, se tivermos o seguinte código abaixo, onde apenas em tempo de execução sabemos o tipo do objeto, teríamos que lidar com a possibilidade de não ter a conversão para PDF, por exemplo, mesmo quando temos certeza de que é possível fazer isso.
Respeitando o ISP
Para respeitar o ISP naquele exemplo, poderíamos implementá-lo de forma diferente, conforme mostrado abaixo. Poderíamos definir dois protocolos, em vez de apenas um, DocumentConverter e Fax. Isso separaria as responsabilidades e transformaria esses novos protocolos em protocolos coesos.
Aqui, um breve parênteses é que poderíamos torná-lo melhor, e dividir esse protocolo em três novos protocolos, por exemplo PDFConverter, ImageConverter e Fax. Isso poderia ser feito, mas talvez não seja necessário. O ponto é que temos que encontrar um meio termo ao tentar quebrar protocolos/interfaces em novos (mesmo quando estamos apenas na fase de design/conceito de implementações). Se mergulharmos cegamente no ISP, provavelmente faremos “over-engineering” em alguns casos. E isso pode ser perigoso! ⚠️
Portanto, meu conselho é medir quantos benefícios teremos ao quebrar um protocolo ou uma interface em vários. Algumas boas perguntas para responder antes de fazer isso:
- “Essa segregação de interface vai nos trazer algum benefício imediato?”
- “Essa segregação de interface nos trará benefícios no futuro?”
Nesse caso, não nos traria nenhum benefício imediato quebrar o protocolo em três protocolos ao invés de dois.
Continuando, com esses dois novos protocolos, nossas três classes seriam um pouco diferentes, como mostrado abaixo.
A classe FaxMachine não teria que implementar métodos desnecessários, que poderiam causar comportamentos imprevisíveis, os dois métodos que retornavam nil anteriormente.
O mesmo vale para a classe NewIphone, que não teria que implementar o método fax(document: Document).
Finalmente, poderíamos usar um recurso realmente interessante em Swift para ajustar a classe UltraMachine, herança múltipla de protocolos. Para fazer isso, só temos que fazer com que ela implemente os dois protocolos.
Além disso, não teríamos o problema de retornos opcionais, como podemos ver a seguir.
Resumindo ISP
- Este princípio diz que clientes não devem ser forçados a depender de interfaces que não utilizam.
- Não seguir este princípio e implementar interfaces gordas são sinônimos. Interfaces gordas são interfaces não coesas. Podemos dizer que essas interfaces têm mais de uma responsabilidade e relacionar isto com o Princípio da Responsabilidade Única (SRP) também.
- Respeitar esse princípio é positivo porque:
- Evita interfaces gordas e, consequentemente, acoplamentos desnecessários.
- Evita recompilações e testes desnecessários quando algumas alterações são necessárias na interface gorda.
- Torna o software desenvolvido mais fácil de entender e testar.
- Pode inspirar desenvolvedores menos experientes em uma base de código, já que eles poderiam assumir naturalmente que essa é uma abordagem melhor, o que poderia evitar ter interfaces gordas como padrão dentro desse software.
- Finalmente, para evitar quebrar este princípio, a melhor recomendação é preferir segregar interfaces gordas em interfaces menores. Apenas preste atenção o quão profundo você deve ir na segregação, medindo os benefícios presentes e futuros da mesma.
Referências
- O que é SOLID
- Quem é Uncle Bob
- Robert C. Martin. Design Principles and Design Patterns, 2000
Esse é o quarto artigo da série de cinco artigos sobre SOLID e seu uso em Swift. Espero que tenham gostado e sintam-se livres para deixar feedbacks, sugerir melhorias ou até mesmo me enviar uma mensagem.
Gostou do artigo? Você também pode se interessar por:
- Swift: Princípio da Responsabilidade Única [Artigo 1]
- Swift: Princípio Aberto-Fechado [Artigo 2]
- Swift: Príncipio de substituição de Liskov [Artigo 3]
- Whitepaper: Backend Driven Development
- SwiftUI e arquiteturas: MVC
Quer receber artigos como este direto em seu e-mail? Inscreva-se aqui!