Desmistificando as Interfaces

Parte I

Muitos já ouviram falar, outros sabem que existe mas nunca utilizaram... o fato é que poucos desenvolvedores fazem uso efetivo de interfaces em seus projetos. Este recurso, que foi introduzido no Delphi 3 para dar suporte à arquitetura COM, pode melhorar substancialmente o design de uma aplicação orientada a objetos. Além disso, apesar de não parecer, é muito simples e fácil de ser implementado.

Mas o que são interfaces?

Pense na primeira imagem que vem à sua mente quando falamos esta palavra: interface. Certamente, existe uma grande possibilidade de você ter pensado em uma janela/formulário para a interação com os usuários de seu sistema. Contudo, podemos ir mais a fundo e também imaginar os objetos de nossa aplicação interagindo uns com os outros. Estes, por sua vez, precisam se conhecer para então saber que mensagens poderão trocar. É aí que entram as interfaces.

Podemos visualizar os objetos de nossa aplicação como instâncias de uma classe na memória e dividi-los em duas partes: sua visualização externa e a própria implementação de seus métodos.

Note que até o momento estamos focando nossas atenções apenas nesse contexto. Entretanto, interfaces podem ser encontradas em diversas outras situações. Além das já citadas interações com usuário e entre objetos, programas inteiros também podem se comunicar e peças de hardware precisam se encaixar perfeitamente. Com isso, fica fácil imaginar diversas situações para o uso de interfaces.

Vamos então conceitualizá-las como um contrato entre duas partes. Se determinado objeto declara que implementa uma interface, ele a deve seguir à risca, implementando tudo o que for estipulado por este “acordo”. Da mesma maneira, os outros objetos poderão ter certeza de que as mensagens enviadas a ele serão correspondidas. E o mais importante: saberão quais operações poderão ser solicitadas.

Tente fazer uma relação desses objetos com um programa e sua interface gráfica. A intenção do desenho de um formulário é a de mostrar quais operações podem ser executadas sobre tal aplicação. Expondo-as de maneira clara para quem estiver utilizando o sistema.

E quais as vantagens do uso desse conceito em meus objetos?

O Delphi, por natureza, é uma linguagem fortemente tipada. Isso significa que, quando um método é chamado, já sabemos previamente quais os seus parâmetros e seu tipo de retorno. No caso de objetos, a declaração desses métodos é feita na sua classe, em uma seção denomida, coincidentemente, de interface.

Se declararmos uma variável em nosso código cujo tipo é uma classe, estaremos certos de que poderemos chamar qualquer método disponibilizado pelo objeto que a ela for associado.

 

UmCiclista: TCiclista;

 

(...)

 

UmCiclista := TCiclista.Create;

UmCiclista.Pedala;

 

Como podemos ver, a variável UmCiclista recebe uma instância da classe TCiclista e chama seu procedimento Pedala. No entanto, o que poderia ser feito se precisássemos associar à UmCiclista, um objeto de uma classe TTriatleta? Bom, se você fosse um programador C++ responderia rápido: fácil, projete TTriatleta para herdar de 3 classes: TCorredor, TCiclista e TNadador. Tudo bem, uma solução adequada para este caso. Como UmCiclista só aceita objetos que implementem TCiclista, o TTriatleta seria aceito sem problemas, pois herda de TCiclista ou, em outras palavras, todo triatleta também é um ciclista. Entretando, caro leitor, não possuímos o conceito de Herança Múltipla na nossa Delphi Language. Uma classe pode herdar somente de uma outra. Mas então como resolveríamos esse caso? Veremos mais a frente como utilizar as interfaces para suprir essa necessidade.

 

Ok, chega de teoria! Que tal partimos logo para o código?

Uma interface é constituída apenas por declarações de métodos. Sua diferença em relação a uma classe comum é que nela não haverá nenhuma implementação.

 

ICiclista = interface

  procedure SetBicicleta(Bicicleta: TBicicleta);

  function GetBicicleta: TBicicleta;

  procedure Pedala;

end;

 

Ou seja, declarar uma classe que implementa ICiclista significa dizer que todos os métodos dessa interface estarão presentes. Note ainda, que a declaração dos métodos na interface é feita da mesma forma que em uma classe, com a exceção da visibilidade (public, protected, private...). E, da mesma maneira que utilizamos a letra T para denotarmos tipos, usaremos a letra I para iniciarmos interfaces, apenas por convenção.

Vejamos agora a declaração de uma classe que implementa ICiclista.

 

TCiclista = class(TInterfacedObject, ICiclista)

private

  (...)

public

  //Métodos de ICiclista

  procedure SetBicicleta(Bicicleta: TBicicleta);

  function GetBicicleta: TBicicleta;

  procedure Pedala;

end;

 

Como podemos ver, a classe TCiclista poderia ter ainda diversos outros métodos próprios. Todavia, os declarados em ICiclista obrigatoriamente devem ser implementados (ou declarados como abstratos para posterior implementação). Agora, as operações de ICiclista estarão presentes em TCiclista e podem ser chamadas como outras quaisquer.

Neste ponto, já podemos apontar uma grande utilidade para esse conceito. Imagine a implementação de um grande sistema. Quando dois programadores ou pares iniciam a codificação de módulos separados e interligados, eles podem estabelecer previamente um “contrato” de comunicação. Deste modo, quem programa o módulo A, saberá que o módulo B implementa a interface X, portanto, terá certeza das mensagens que poderão ser enviadas. E mais importante do que isso, terá código compilável e receberá ajuda do tipo code insight ao manipular os fontes.

Como sabemos, toda classe em Object Pascal acabará herdando de uma outra conhecida como TObject. Deste modo, os métodos desta classe estarão disponíveis a qualquer outra que for herdada. Não surpreendentemente, o mesmo acontece com as interfaces. Toda e qualquer interface no Delphi, mesmo que não seja declarado explicitamente, herdará de IInterface. Mais uma vez, a diferença na herança entre classes e interfaces está na falta de implementação. Se uma interface X herda de Y, um objeto que implemente X também deverá implementar os métodos de Y. Ou seja, se seguirmos essa idéia até o topo da hierarquia, todos os objetos que implementem interfaces, deverão definir implementações para os métodos de IInterface.

Se você for do tipo que repara em tudo, deve ter notado que a nossa classe TCiclista não herda diretamente de TObject. Deve também ter notado que não estamos implementando nenhum método de IInterface (mesmo que ainda não saibamos quais eles sejam). A declaração de uma classe tem a seguinte forma: TClasse = class(TClasseMãe, IInterfacesImplementadas). Vamos agora dar uma boa olhada nas implementações de IInterface e TInterfacedObject.

 

IInterface = interface

  ['{00000000-0000-0000-C000-000000000046}']

  function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;

  function _AddRef: Integer; stdcall;

  function _Release: Integer; stdcall;

end; 

 

TInterfacedObject = class(TObject, IInterface)

protected

  FRefCount: Integer;

  function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;

  function _AddRef: Integer; stdcall;

  function _Release: Integer; stdcall;

public

  procedure AfterConstruction; override;

  procedure BeforeDestruction; override;

  class function NewInstance: TObject; override;

  property RefCount: Integer read FRefCount;

end;

 

Perceba que TInterfacedObject já faz o trabalho de declarar e codificar os métodos de IInterface. Sendo assim, tais métodos já foram implementados para qualquer classe que a herde, o que nos livra do trabalho da implementação a cada nova declaração. É altamente aconselhável que sempre usemos essa classe para implementar interfaces.

Mas o que há de tão especial em IInterface? Observamos um método para consulta e dois para a contagem de referências. Um tópico muito importante é que essa contagem inibe a necessidade de executarmos um destrutor para estes objetos. Ou seja, nunca precisaremos (e nem deveremos) chamar o método Free de um objeto que utiliza uma interface. Isso funciona da seguinte maneira: a cada nova referência ao objeto, o método _AddRef é chamado, o que incrementa o valor de RefCount de um TInterfacedObject. Da mesma forma, a cada perda de referência, o método _Release é executado e a contagem é decrementada. Quando o contador chegar a 0, o objeto é liberado da memória.

 

UmCiclista := TCiclista.Create; //Referências ao objeto criado: 1.

AqueleCiclista := UmCiclista; //Referências ao mesmo objeto: 2.

AqueleCiclista.Pedala;

UmCiclista := nil; //Perdeu a referência, total: 1.

AqueleCiclista := nil; //Nenhuma referência ao objeto. Retirado da memória.

E o problema do Triatleta?

Até o momento, a única utilidade prática das interfaces está na garantia de que determinados métodos estão presentes nas classes. Vamos agora partir para um exemplo mais concreto. Além de ICiclista, vamos declarar:

 

ICorredor = interface

  procedure SetTenis(Tenis: TTenis);

  function GetTenis: TTenis;

  procedure Corre;

end;

 

INadador = interface

  procedure SetTouca(Touca: TTouca);

  function GetTouca: TTouca;

  procedure Nada;

end;

 

E finalmente, vamos especificar TTriatleta:

 

TTriatleta = class(TInterfacedObject, ICorredor, ICiclista, INadador)

private

  (...)

public

  procedure Para;

  procedure Descansa;

 

  //Métodos de ICorredor

  procedure SetTenis(Tenis: TTenis);

  function GetTenis: TTenis;

  procedure Corre;

 

  //Métodos de ICiclista

  procedure SetBicicleta(Bicicleta: TBicicleta);

  function GetBicicleta: TBicicleta;

  procedure Pedala;

 

  //Métodos de INadador

  procedure SetTouca(Touca: TTouca);

  function GetTouca: TTouca;

  procedure Nada;

end;

 

Implemente os métodos e tente compilar. Você não receberá nenhuma mensagem de erro, nem sequer um hint. Veja que conseguimos driblar a falta da herança múltipla apenas com o uso de interfaces. Este é um dos principais pontos de uso do conceito abordado, que está presente em quase todas as linguagens que nos disponibilizam a programação orientada a objetos. Diferentemente da herança múltipla, que é muito difícil de ser implementada e controlada.

Existe uma outra maneira de declararmos uma variável:

 

UmCiclista: ICiclista;

 

E é nesse ponto que as interfaces fazem uma grande diferença. Em outra parte de nosso código poderíamos ter:

 

UmCiclista := TCiclista.Create;

UmCiclista.Pedala;

 

Ou então:

 

UmCiclista := TTriatleta.Create;

UmCiclista.Pedala;

 

Isso acontece porque a variável UmCiclista foi declarada como sendo do tipo ICiclista, ou seja, essa pode receber objetos de qualquer classe, desde que implementem ICiclista. Em uma situação real, poderíamos ter instâncias que herdem de classes completamente diferentes sendo armazenados em uma mesma variável. Por exemplo, existem casos em que não é conceitualmente correto mexer na hierarquia apenas para que objetos possam ser associados uns aos outros. Nestes casos, as interfaces cairiam como uma luva, pois serviriam como um encaixe de classes.

Entretanto, como na herança comum, se temos uma variável declarada de um tipo TAtleta e fazemos com que ela receba um objeto de uma classe TCiclista (que descende de TAtleta), os únicos métodos disponíveis serão os de TAtleta, e não os de TCiclista. Por isso, o código a seguir não seria compilado:

 

UmCiclista := TTriatleta.Create;

UmCiclista.Nada;

 

Apesar de a classe TTriatleta possuir um método Nada (oriundo de INadador), a variável UmCiclista foi declarada como sendo do tipo ICiclista, o qual não possui tal operação. O mesmo código não seria compilado se tentássemos chamar o método Descansa.

Conclusão

Um projeto de sistema orientado a objetos pode apresentar muitos pontos complexos a serem tratados. Como vimos, as interfaces nos permitem muita flexibilidade tanto na hora do design como da implementação.

Nos próximos artigos, abordaremos mais a fundo: identificação, type casts, resolução de duplicidades e delegações. Porém, não é preciso esperar para começar a usufruir de todas as vantagens do uso de interfaces desde já!