Estou aqui, leitores!
Conforme esperado, o artigo de hoje aborda a letra “I” do SOLID, que corresponde ao Interface Segregation Principle, ou ISP. Assim com o LSP, este princÃpio também está relacionado com o conceito de abstração da orientação a objetos. Observaremos, no artigo, que abstrações genéricas podem prejudicar classes que as implementam, forçando implementações desnecessárias.
Introdução
Há quem diga que, dos quatro pilares da orientação a objetos, a Abstração seria o “menos importante”. É um equÃvoco. A arquitetura de um projeto é elaborada sob os alicerces da Abstração, já que o processo de “converter” as regras de negócio em classes no sistema é essencial para o sucesso na fase de implementação. O resultado deste procedimento é uma arquitetura flexÃvel e fracamente acoplada em virtude de Interfaces e classes bem delimitadas.
O Interface Segregation Principle (ISP) está intimamente ligado à Abstração e orienta a identificação e definição adequada das Interfaces. O objetivo é evitar que a arquitetura seja comprometida à medida que novos módulos, componentes ou classes são adicionados ao projeto. Veja, portanto, que estamos nos referindo à sustentabilidade da arquitetura.
Na teoria, o ISP traz a seguinte declaração:
“Clients should not be forced to depend upon interfaces that they do not use”
(Clientes não devem ser forçados a depender de Interfaces que eles não utilizam)
Os “clientes”, nessa frase, são representados por classes. Dito isso, a declaração acima significa que as classes da arquitetura não devem implementar Interfaces que não fazem parte do seu contexto. Caso isso ocorra, haverá codificações desnecessárias, métodos sem ação e/ou classes com mais de uma responsabilidade. Veremos tudo isso durante este artigo.
Eu diria que a violação do ISP geralmente ocorre quando existem Interfaces muito genéricas na arquitetura ou generalizadas demais. Tome, como exemplo, a declaração da Interface abaixo:
1 2 3 |
IDocumento = interface procedure Salvar; end; |
Um “documento”, por si só, é um termo relativamente genérico e pode ter várias interpretações: “É um documento de texto? Um documento fiscal? Um documento pessoal? Pode ser impresso? É exportável par algum formato? Possui assinatura digital? Pode ser editado?”.
O problema de uma Interface com essa caracterÃstica é o receio de utilizá-la. Por exemplo, uma nova classe chamada TDocumentoViagem
 deve implementar a Interface IDocumento
?
Bom, pelo nome, sim. Pela abstração, talvez não.
Embora um documento de viagem seja um documento, a Interface pode declarar métodos que não fazem parte da sua abstração, como EnviarParaContabilidade
. Não faz sentido, não é?
Cenário de exemplo
Codificaremos, a seguir, um pequeno exemplo para demonstrar essa violação. Em seguida, faremos uma refatoração para eliminá-la.
Considere a Interface abaixo, que declara apenas dois métodos para trabalhar com relatórios:
1 2 3 4 5 |
type IRelatorio = interface procedure ConsultarDados; procedure AgruparPorData; end; |
Há duas classes que implementam essa Interface, referente aos relatórios de movimentação de estoque…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
type TRelatorioMovimentacaoEstoque = class(TInterfacedObject, IRelatorio) procedure ConsultarDados; procedure AgruparPorData; end; { TRelatorioMovimentacaoEstoque } procedure TRelatorioMovimentacaoEstoque.ConsultarDados; begin FDQuery.Open('SELECT * FROM MovimentacaoEstoque'); end; procedure TRelatorioMovimentacaoEstoque.AgruparPorData; begin // Comando fictÃcio para demonstrar a funcionalidade Report.AddGrouping('DataMovimentacao'); end; |
… e contas a receber:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
type TRelatorioContasReceber = class(TInterfacedObject, IRelatorio) procedure ConsultarDados; procedure AgruparPorData; end; { TRelatorioContasReceber } procedure TRelatorioContasReceber.ConsultarDados; begin FDQuery.Open('SELECT * FROM ContasReceber'); end; procedure TRelatorioContasReceber.AgruparPorData; begin // Comando fictÃcio para demonstrar a funcionalidade Report.AddGrouping('DataConta'); end; |
Ambas as classes consultam dados nas tabelas (método ConsultarDados
) e disponibilizam uma opção para agrupamento por data (método AgruparPorData
). Até o momento, tudo certo.
Por solicitação do cliente, devemos iniciar a codificação de um novo relatório de comprovante de venda.
Bom, já que é um relatório, essa nova classe deverá implementar a Interface IRelatorio
para disponibilizar os métodos necessários e também para manter um padrão na arquitetura:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
type TComprovantePedidoVenda = class(TInterfacedObject, IRelatorio) procedure ConsultarDados; procedure AgruparPorData; end; { TComprovantePedidoVenda } procedure TComprovantePedidoVenda.ConsultarDados; begin FDQuery.Open('SELECT * FROM Venda WHERE CodVenda = 123'); end; procedure TComprovantePedidoVenda.AgruparPorData; begin // ??? end; |
Ops… temos um problema. Este relatório não pode ser agrupado por data, pois consiste em apenas uma venda especÃfica, registrada em uma única data. Porém, como é um método da Interface, a classe é obrigada a implementá-lo. Neste caso, portanto, adicionaremos um Exit
para indicar que o método não deve ter ação alguma:
1 2 3 4 |
procedure TComprovantePedidoVenda.AgruparPorData; begin Exit; end; |
Pois bem, leitores, ao escrever este Exit
, violamos o PrincÃpio da Segregação da Interface. A classe TComprovantePedidoVenda
foi “forçada” a implementar um método que não condiz com o seu contexto, ferindo a declaração do ISP. Embora o método não tenha ação, a sua implementação deve existir. Podemos afirmar, portanto, que houve uma falha na definição da Interface, ou melhor, na abstração.
No final das contas, este método se torna desnecessário e provavelmente confundirá alguns programadores em codificações futuras. Por exemplo, a chamada abaixo ao método AgruparPorData
é válida, não gera erros, mas não serve para absolutamente nada:
1 2 3 4 5 6 7 8 |
var Comprovante: IRelatorio; begin Comprovante := TComprovantePedidoVenda.Create; { ... } Comprovante.AgruparPorData; { ... } end; |
Este não é o único problema. O mesmo pode acontecer do modo inverso. Imagine que, por conta das regras da classe de comprovante de venda, um novo método foi adicionado à Interface:
1 2 3 4 5 |
type IRelatorio = interface { ... } procedure EnviarParaCliente; end; |
Apesar de ser uma alteração simples, o projeto deixa de compilar. As classes referentes aos relatórios de movimentação de estoque e contas a receber começam a receber o erro abaixo, criticando que o método EnviarParaCliente
também deve ser implementado:
1 |
Missing implementation of interface method IRelatorio.EnviarParaCliente |
Porém, estes relatórios não são enviados a clientes, pois são administrativos e de uso interno na empresa.
Solução? Novas instruções Exit
:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
{ TRelatorioMovimentacaoEstoque } procedure TRelatorioMovimentacaoEstoque.EnviarParaCliente; begin Exit; end; { TRelatorioContasReceber } procedure TRelatorioContasReceber.EnviarParaCliente; begin Exit; end; |
Agora ficou bem ruim, não? Para complicar, a tendência é surgir cada vez mais “Exits” nas classes conforme novos relatórios de diferentes contextos forem desenvolvidos.
Aplicando o ISP
De uma forma bem simples, pessoal: a solução é corrigir a abstração ao separar os conceitos. No nosso caso, a Interface IRelatorio
deverá ser “quebrada” em Interfaces menores e mais restritas. Para isso, os métodos exclusivos do comprovante de venda serão movidos para uma nova Interface:
1 2 3 4 5 6 7 8 9 |
type IRelatorio = interface procedure ConsultarDados; procedure AgruparPorData; end; IComprovante = interface procedure EnviarParaCliente; end; |
Com essa alteração, as Interfaces deixam de ser genéricas e passam a ser mais especÃficas, com um propósito delimitado. Ao codificar novas classes, há uma certeza maior de qual Interface deverá ser utilizada para a implementação.
Além disso, claro, o ISP também evita os efeitos “magnéticos” causados pelas alterações em Interfaces. Com a aplicação do ISP, garantimos que somente as classes que fazem parte do mesmo contexto serão atualizadas.
Agora, uma reflexão: e se existir uma classe que, de alguma forma, seja um relatório e um comprovante ao mesmo tempo?
Bom, embora não faça muito sentido, isso pode ocorrer, mas a solução também é simples. Basta declarar uma implementação múltipla de Interfaces:
1 2 3 4 5 6 |
type TRelatorioComprovante = class(TInterfacedObject, IRelatorio, IComprovante) procedure ConsultarDados; procedure AgruparPorData; procedure EnviarParaCliente; end; |
Fechou, pessoal?
Espero que o Interface Segregation Principle tenha ficado claro para vocês!
Qualquer dúvida, por favor, entrem em contato!
Abraços e até logo!
Excelente explicação meu amigo André! Muito boa essa série! Vê se não some hein kkkk
Forte abraço!
Olá, Elton! Muito obrigado, meu caro!
Pode deixar que não vou sumir mais, rsrs!
O próximo artigo já está em elaboração. Grande abraço!
Olá meu amigo, tudo bem? Perdoe o inconveniente, porém tenho lido muito seus artigos, e cada vez o meu encanto se torna maior, pois a simplicidade e objetividade de suas palavras faz tudo parecer muito mais simples do que de fato é, se é que não é simples? Então, sem mais delongas, como diria o Mourão, quero saber quando você pretende publicar o último artigo da série (SOLI[D]) , pois talvez seja o único que ainda não consegui compreender muito bem, o que é óbvio, você ainda não explicou. Brincadeiras à parte, gostaria de expressar minha sincera gratidão por seu trabalho, dedicação e empenho. Um grande abraço na expectativa de ler logo esse artigo.
Olá, Bhawan, como vai?
Agradeço fortemente pelo seu comentário! Fico muito feliz ao saber que os artigos do blog são bases de conhecimento para programadores! 🙂
O próximo – e último – artigo da série SOLID já está em elaboração! O último princÃpio é muito importante e traz uma referência com os outros 4.
Aguarde!
Grande abraço!
Ótimo Artigo André,
Venho acompanhando seu conteúdo a tempo já. Está de parabéns com a forma de didática.
Obrigado, Luiz!
Em breve eu volto com novos artigos. Continue acompanhando!
Abraço!