SOL[I]D – Interface Segregation Principle (ISP)

SOLID - Interface Segregation Principle - ISP

Estou aqui, leitores! Quase 3 meses de ausência, hein? Que isso não se repita, rsrs!
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.

 

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:

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, não.


Como assim?

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 é?

 

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:

type
  IRelatorio = interface
    procedure ConsultarDados;
    procedure AgruparPorData;
  end;

 

Há duas classes que implementam essa Interface, referente aos relatórios de movimentação de estoque…

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:

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:

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:

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:

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:

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:

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:

{ 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.

 

Como podemos eliminar essa violação?
De uma forma bem simples, pessoal: 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:

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.

 

Compreendi, André, mas talvez pode existir uma classe que, de alguma forma, será um relatório e um comprovante ao mesmo tempo…
Sim, embora não faça muito sentido, isso pode ocorrer, e a solução também é simples! Basta declarar uma implementação múltipla de Interfaces:

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!


 

4 comentários

  1. Excelente explicação meu amigo André! Muito boa essa série! Vê se não some hein kkkk
    Forte abraço!

  2. 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.

    1. 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!

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Preencha o campo abaixo * Time limit is exhausted. Please reload CAPTCHA.