Desvendando o SOLID: Interface Segregation Principle

Vitor Ferraz Varela
6 min readMay 23, 2021

--

Introdução

No último artigo falamos um pouco sobre o princípio de substituição de Liskov e como podemos utilizar a composição para evitar alguns problemas que encontramos ao usar herança de forma errada.

Ao longo dessa série, abordamos muito sobre a coesão e como é fundamental para manutenção e reuso do nosso código. Falamos muito sobre a coesão a nível de classe e pensando nisso, hoje iremos abordar o princípio da segregação de interface (ISP — Interface segregation principle) para discutir a coesão a nível de interface.

Interfaces coesas e magras

Quando abordamos o tema da coesão anteriormente, vimos que isso está fortemente vinculado com as responsabilidades da nossa classe. Quanto mais coesa a classe mais claro é a responsabilidade dela, esse mesmo pressuposto se aplica para as interfaces.

O princípio da segregação de Interface (ISP) afirma:

Nenhum cliente deve ser forçados a depender de métodos que não utiliza.

Existe um termo usado no artigo do Uncle Bob abordando esse assunto, que são as famosas interfaces gordas (fat interfaces), que são interfaces que possuem muitas responsabilidades, e portanto se tornam pouco coesas.

Os problemas que temos com interfaces que possuem muitas responsabilidades, são os mesmos que abordamos no primeiro artigo dessa série, o princípio de responsabilidade única (SRP), caso não tenha lido vale a pena dar uma olhada no artigo.

Acho interessante recapitular alguns desses problemas. Sabemos que quanto menos coeso nosso código maior a dificuldade de dar manutenção principalmente devido ao acoplamento que o código pode ter, além disso menor será o reuso desse código na nossa aplicação.

Portanto, interfaces coesas são aquelas que possuem também apenas uma única responsabilidade. Quando coesas, essas interfaces possibilitam um maior reuso, tendem a ser mais estáveis e quando mais estáveis menor será o risco de termos efeitos colaterais quando estamos refatorando nosso código ou implementando novas funcionalidades.

Um pouco de história

Um fato que acho interessante compartilhar é como o Uncle Bob criou o princípio da segregação de interfaces e que tipo de problema ele estava tendo. Entender primeiro o problema, acaba dando um contexto melhor sobre a linha de raciocínio usada para resolvê-lo.

Quando prestava consultoria para a Xerox, a empresa tinha criado um novo sistema de impressora que podia executar uma variedade de tarefas, tais como grampeamento e envio de fax.

Com o tempo, esse código acabou se tornando mais complexo e dar manutenção se tornou uma tarefa cada vez mais complicada. Fazer modificações, estava sendo mais e mais difícil, de modo que a menor mudança impactava diretamente no tempo do ciclo de desenvolvimento da aplicação.

O principal problema estava na modelagem desse sistema, mais especificamente com a coesão, onde as funcionalidades ficavam centralizadas em uma única classe que era usada por várias outras classes no sistema.

Sempre que a funcionalidade de grampeamento ou envio de fax era necessária, essa classe com todas responsabilidade era necessária.

Desta forma isso acabou resultando numa classe cheia de métodos específicos que eram usados em contextos muito diferentes.

A funcionalidade de fax por exemplo tinha contexto e acesso a todos os métodos da funcionalidade de grampear sem nenhuma necessidade, resultando em um alto acoplamento, baixo encapsulamento e diversos problemas no desenvolvimento, como introduzir novas funcionalidades ou dar manutenção porque a classe se tornou pouco estável.

A solução sugerida por Martin, foi criar uma camada de interface entre essa classe e as outras que usavam ela, usando o princípio de inversão de dependências, que veremos mais à frente. Desta forma ao invés de ter uma classe grande que era usada nas classe de Grampos ou de Impressão, foi criado uma interface para cada uma dessas tarefas, desta forma mantendo o contexto de cada uma contendo somente o que era necessário.

Depois dessa história podemos entender com mais clareza a definição do princípio:

Nenhum cliente deve ser forçado a depender de métodos que não utiliza.

Exemplo prático

Uma forma de tornar toda essa teoria algo mais palpável, utilizando um exemplo mais prático. Vamos entender de que forma podemos usar esse princípio no nosso código.

Digamos que tenhamos um protocolo de `cache` no nosso aplicativo e nele podemos salvar o `Access Token`.

Agora temos uma nova funcionalidade que irá poder recuperar o identificador do usuário, UserID.

À primeira vista, basta a gente adicionar mais um método no nosso protocolo que estaria tudo certo.

Agora vamos abordar alguns requisitos dessa funcionalidade e ver a fundo os detalhes de implementação deste protocolo.

Primeiro vamos aos requisitos, serão usados duas formas diferentes para salvar os dados, o token de usuário será usando o `Keychain` enquanto que o `UserId` será salvo no `UserDefaults`.

Desta forma, sabemos que iremos ter duas classes que irão implementar o protocolo de `Cache`, iremos criar a `UserCache`, para o contexto do `UserId` e a `AccessTokenCache` para o contexto do `Access Token`.

Quebrando o ISP

Aqui temos um exemplo do ISP sendo quebrado. Podemos notar que para implementar o protocolo de Cache, as classes concretas precisam implementar métodos fora do seu contexto e domínio, quebrando assim a responsabilidade única que deveriam ter para ser mais coesas e desta forma causando problemas de acoplamento onde caso tenhamos mudanças em algum dos métodos de uma classe poderá impactar causando efeitos colaterais na outra classe.

Além disso temos o cenários de métodos que retornam objetos opcionais, como cada uma das nossas implementações concretas têm formas diferentes de salvar os dados, a classe que utiliza o UserDefaults não precisa ter contexto da implementação usando o Keychain e desta forma o método retorna nulo. Esse é um indício que nosso protocolo poderia ser dividido para ganhar mais coesão.

Por fim, outro problema que encontramos com interfaces pouco coesas é no quesito de testabilidade. No exemplo que temos aqui é um protocolo com três métodos, mas imagine que ao invés de três ele tivesse 10 métodos,criar classes de mocks ou spies acaba sendo um tarefa muito custosa pois temos que implementar métodos que não fazem sentido para o contexto do teste.

Uma possível solução para os problemas que vimos anteriormente é justamente a separação de responsabilidades, assim podemos quebrar o protocolo de Cache em protocolos menores e fazer uso da composição de protocolos para deixar dentro do mesmo contexto coisas que façam sentido serem compartilhadas.

Como podemos ver no código acima o protocolo de `Cache` acabou ficando muito mais simples do que era antes e contendo somente o necessário, enquanto que os outros dois protocolos que criamos cuidam respectivamente dos seus devidos contextos.

Agora nas nossas implementações concretas, basta realizarmos as alterações para que elas implementem esses novos protocolos.

Nosso código agora se tornou mais coeso pois cada responsabilidade está dentro do seu devido contexto e além disso podemos remover as implementações vazias ou que retornavam valores opcionais.

Vale lembrar um cuidado que devemos ter quando aplicar o princípio de segregação de interfaces que é balancear até que nível é o ideal quebrar nosso protocolo em protocolos menores. Meu conselho é sempre seguir a regra do primeiro faça funcionar, depois refatore e depois reflita. Sempre existe espaço para melhoria mas devemos ter cuidado em não fazer `over-engineering` do nosso código, por isso faça uma análise dos benefícios que o nível da granulação pode trazer.

Conclusão

Em resumo, o princípio da segregação de interface expande nossa discussão sobre coesão, para além das classes e agora abordamos em nível de protocolos. Desta forma clientes não devem ser forçados a depender de interfaces que não utilizam. E por fim, quais os problemas que temos com interfaces gordas que além de possuir mais responsabilidade do que deveriam, também um acoplamento do seu código e dificulta a criação de testes.

O ideal é sempre ponderar o nível de granulação que estamos tendo nos nossos protocolos e desta forma podemos ter um código menos acoplado, em outros cenários talvez a composição dos protocolos pode ser uma solução mais elegante e coesa para o problema.

Muito obrigado por acompanhar até aqui e me siga no Medium para não perder os próximos artigos.

Você pode me encontrar nas redes sociais:

Linkedin | Instagram | Twitter

--

--