Desacoplar MVP e VCL com Interfaces

(english)

Ao contrário de alguns astros da programação orientada a objetos, eu não era muito fã de interface. Os primeiros usos foram sub-utilizados, como mero auxiliar de gerenciamento de memória (interface em Object Pascal tem contagem de referência controlada pelo compilador, ao contrário de classes).

Com o avanço no desenvolvimento do PressObjects, tornou-se cada vez mais comum a necessidade de desacoplar implementações a um ponto aonde certas classes passaram a ser totalmente abstratas. Algo assim (bem resumido):

TIDEIntf = class(TObject)
public
  procedure ReadText(...); virtual; abstract;
  procedure SaveText(...); virtual; abstract;
end;

E então, uma implementação para Delphi, outra para Lazarus e outra ainda para MSEide:

TDelphiIDEIntf = class(TIDEIntf)
public
  procedure ReadText(...); override;
  procedure SaveText(...); override;
end;

Outras duas declarações idênticas para Lazarus e MSEide. A diferença fica por conta da implementação de cada método, que utiliza rotinas próprias de cada uma destas IDEs. A vantagem é ter uma única rotina que lê e altera dados do editor da IDE (um Expert) através de um meio comum (a classe TIDEIntf) e utilizar este mesmo expert em três IDEs diferentes. E para aumentar o número de IDEs, basta implementar uma nova classe que sobrecarregue estes métodos virtuais.

Este é um exemplo bem simples em que uma interface pode ser utilizada no lugar da classe abstrata. Existe uma desvantagem no modelo proposto, e que é justamente a proposta das interfaces, mas que no entanto não está tão evidente porque o modelo não converte-se em limitação para o programador (ao menos por enquanto): as classes que implementam as funcionalidades para a IDE devem obrigatoriamente ser descendentes de TIDEIntf.

Esta desvantagem tornou-se mais evidente durante a implementação do desacoplamento ocorrido entre a View de um framework MVP e a biblioteca gráfica VCL/LCL.

Uma View de um framework MVP é o que faz a ponte entre o núcleo deste framework e um controle visual. Existe uma View para controles ComboBox, ao ser criada ela direciona alguns eventos como OnExit, OnDropDown, entre outros, e transforma-os em eventos que o framework conhece. Mesma coisa para a View que controla um Edit. Eventos como OnEnter e OnChange são redirecionados.

Além de controlar os eventos, a View possui métodos para acessar o controle e evitar que o framework faça referência direta a ele (evitando acoplamento). Exemplo, a View do controle ComboBox possui um método DropDown, que faz com que o controle abra uma lista. O Edit e o ComboBox possuem uma propriedade AsText, que lê e grava o conteúdo da propriedade Text do controle.

Tendo entendido como a View funciona, passemos para a próxima parte. Eu enxerguei a necessidade de desacoplar a View e classes da VCL. Por quê? Porque existem opções tão boas e até melhores do que a VCL para ser usado pelo MVP. Alguns exemplos são MSEgui, Kol/Mck, e para FPC ainda existe fpGUI (a LCL não conta, ela tem a mesma interface da VCL e suas Views utilizam a mesma implementação).

Tudo bem. Minha primeira idéia foi criar classes abstratas, que nos exemplos acima, seria uma para a View do ComboBox e outra para a View do Edit. Elas, de forma bem simplificada, ficariam assim:

TEditView = class(TObject)
public
  procedure Text(...); virtual; abstract;
end;

TComboBoxView = class(TObject)
public
  procedure DropDown; virtual; abstract;
  procedure Text(...); virtual; abstract;
end;

E as classes concretas, assim:

TVCLEditView = class(TEditView)
public
  procedure Text(...); override;
end;

TVCLComboBoxView = class(TComboBoxView)
public
  procedure DropDown; override;
  procedure Text(...); override;
end;

Diferentes units implementam diferentes conjuntos de classes. Uma unit para VCL/LCL, outra para MSEgui, outra para fpGUI e assim por diante. Para incluir uma nova biblioteca gráfica, basta implementar uma unit que sobrecarregue os métodos de todas estas classes.

Então temos métodos virtuais na classe abstrata, e em outra unit existe a classe concreta que sobrecarrega estes métodos e faz o acesso direto ao componente visual. O núcleo do framework acessa a classe abstrata, e a classe concreta é quem realmente acessa o controle visual porque apenas ela está acoplada à biblioteca gráfica. Um registro de classe feito pelo usuário dirá que ele vai trabalhar com VCL, MSE, ou seja lá qual for.

Agora o problema: note que alguns componentes compartilham características, no exemplo acima tanto ComboBox quanto Edit possuem um Text que fazem a mesma coisa.

Para remover o acoplamento entre View e VCL, e permitir aproveitamento de código e comportamento através de herança, eu decidi criar um conjunto de interfaces que descrevem o que as classes concretas devem implementar. A implementação ficou desta forma:

ICustomView = interface(IInterface)
[...]
  procedure Text(...);
end;

IEditView = interface(ICustomView)
[...]
end;

IComboBoxView = interface(ICustomView)
[...]
  procedure DropDown;
end;

E as classes que implementam a interface, reaproveitando tanto código quanto possível, com acoplamento zero:

TVCLCustomView = class(TMyIntfObject, ICustomView)
public
  procedure Text(...);
end;

TVCLEditView = class(TVCLCustomView, IEditView)
end;

TVCLComboBoxView = class(TVCLCustomView, IComboBoxView)
public
  procedure DropDown;
end;

Note que apenas neste modelo é possível reaproveitar uma única procedure Text nas duas classes. No modelo que utiliza apenas classes, cada descendente deveria implementar seu próprio Text porque elas não poderiam herdar de uma classe em comum.

5 thoughts on “Desacoplar MVP e VCL com Interfaces”

  1. Os usos de reference counting das interfaces era pra compatibilidade com o modelo COM da microsoft, interfaces como conceito precedem COM. Acredito que seja equivocada sua afirmação neste ponto, interfaces sempre tiveram o proposito de abstrair a “interface”(parte publica) de uma classe, simulando também uma pseudo herança múltipla com a vantagem de não carregar a implementação promovendo desacoplamento como vc diz no final do artigo.

  2. Exatamente. Talvez tenha faltado mais esclarecimento da minha parte. Meu objetivo foi colocar que eu fazia uso de um recurso do compilador, que é o decremento da contagem de referência para cada variável que saia de escopo. Nada mais.

  3. Eu me lembro da época em que vc olhou para o Infra e disse, pq usar interfaces 🙂
    Nossos projetos estão cada vez mais parecidos hein joão?

  4. Daí Marcos!

    Poizé, algumas semelhanças estão aparecendo mas eu ainda estou relutante para usar interfaces em absolutamente tudo. Há frameworks que precisam ficar mais desacoplados mas eu ainda uso muita referência através de classe, especialmente entre aquelas que cooperam entre si.

    Para resolver o problema de AV ao misturar classe e interface, criei uma classe com contagem de referência em que a chamada ao .Free irá decrementar esse contador sem destruir a instância. O mesmo quando uma interface sai de contexto. Estou misturando ponteiros para classe e interface com sucesso, e qualquer hora falo um pouco mais sobre isto.

    Em http://blog.joaomorais.com.br/2008/09/06/objetos-contagem-ref.html é possível ver a implementação da parte ‘classe’ da idéia. Em cima disto, basta implementar _addref e _release.

Comments are closed.