Este é um post disponível para assinantes MVPou para quem possui Créditos DevMedia. Clique aqui para saber mais!
POO - Dominando o uso de Interfaces - Revista Clube Delphi 134
O artigo trata sobre interfaces, que são contratos estabelecidos para que classes os sigam. No artigo será visto como criar, usar, trocar e delegar interfaces. Também serão vistos erros comuns a se evitar e as novidades do Delphi XE para o tratamento de interfaces.
Você não gostou da qualidade deste conteúdo?
(opcional) Você gostaria de comentar o que não lhe agradou?
[Artigo disponível no Leitor Digital DevMedia. Clique aqui para acessá-lo]
> Clique aqui para ler todos os artigos da ClubeDelphi 134
Interface é uma palavra com múltiplos significados, tanto na física, na comunicação e na ciência da computação. No caso da ciência da computação o termo foi “emprestado” mais de uma vez, pois pode significar o tipo ou padrão de conexão entre dois componentes, o conjunto de elementos gráficos para comunicar um estado ou aceitar comandos de um usuário (GUI) e um contrato ou padrão entre dois objetos ou componentes.
No caso do hardware, por exemplo, USB é uma interface. É possível conectar qualquer tipo de dispositivo na porta USB de um computador contanto que o dispositivo siga certos padrões, ou seja, implemente a interface USB. O mesmo ocorre com PCI, PCI-express e outras tantas portas de conexão do computador.
Não é necessário ir muito longe: o TCP/IP fornece uma linguagem comum, acima dos protocolos físicos como ethernet, para que qualquer dispositivo em uma rede possa se comunicar em uma linguagem comum. O HTTP é um protocolo implementado sobre TCP/IP que recebe e envia dados através de requisições GET e POST e é a base de todas os sistemas online, redes sociais, webservices e computação em nuvem.
Qualquer objeto tem uma interface, mesmo que seja implícita, pois tem um conjunto de métodos e atributos públicos, que são visíveis e podem ser usados ou conectados com o meio exterior. A classe da Listagem 1, com seus métodos e atributos públicos e privados, tem uma interface implícita, que é o conjunto de seus membros públicos.
Nota: Todos os exemplos desse artigo foram feitos em um formulário com vários TButton, um para cada teste, e alguns métodos ou eventos. O código distribuído junto com esta edição tem mais exemplos além dos mostrados no artigo. O objetivo é que o leitor explore esse código modificando ou comentando partes para observar o que acontece.
Listagem 1. Classe de exemplo
TForm1 = class(TForm)
private
{ Private declarations }
public
{ Public declarations }
end;
TUmaClasse = class
private
FUmaData: TDateTime;
FUmNumero: integer;
FUmNome: string;
FUmaFracao: Double;
FUmBooleano: boolean;
procedure UmMetodoPrivado;
property Data: TDateTime read FUmaData write FUmaData;
public
procedure SetNome(const value: string);
function GetNome: string;
procedure DigaAlo;
property
end;
Temos duas classes, um formulário e a classe TUmaClasse. Em TUmaClasse apenas SetNome, GetNome, DigaAlo e Nome são públicos, por isso eles definem uma interface implícita.
Em programação orientada a objetos quando se cria uma interface cria-se um contrato que deve ser respeitado por quem a implementa. A classe que implementa uma interface deve obrigatoriamente ter os métodos dela implementados.
Há uma certa similaridade com classes abstratas, mas a similaridade para por aí. É correto afirmar que uma classe totalmente abstrata não tem nenhum “comportamento” pois todo ele será criado nas classes derivadas, mas ainda assim uma classe derivada pode implementar alguns métodos e deixar outros ainda abstratos, para uma implementação futura. Adicionalmente uma classe abstrata ainda pode ter atributos públicos, ou propriedades, que podem ser herdados pelos seus descendentes. Uma classe totalmente abstrata não pode ser instanciada, mas uma classe que tem alguns métodos abstratos pode.
Além disso quando se começa uma família de classes a partir de uma classe abstrata todos os seus descendentes seguem uma herança linear, vertical. Usando interfaces esse cenário muda muito.
Para interfaces valem todas as regras de herança e polimorfismo, exceto que uma interface pode “herdar” de uma, duas ou mais interfaces. Quando uma interface herda de outra o termo técnico apropriado é “implementa” e não herda. O mesmo ocorre quando uma classe implementa uma interface.
Uma classe não pode ser descendente de uma interface, mas se for descendente de uma classe que implementa uma interface, ela e todos os seus descendentes passam automaticamente pela herança a implementar essa interface também.
"
Este é um post disponível para assinantes MVPou para quem possui Créditos DevMedia. Clique aqui para saber mais!
Analista de Sistemas Sr. na Editora Revista dos Tribunais. Trabalha com Delphi desde a versão 3. Formado em Processamento de Dados pela FATEC-SP
8 COMENTÁRIOS
Fui tentar fazer exatamente como é mostrado nas listagens apresentadas para exemplos e dá erro quando tento compilar o código da 'listagem 5' da página 49, na seguinte parte:
-> if (aviao is TAviao) then
begin
ShowMessage('Avião é um TAviao');
(aviao as TAviao).DeslocarSe;
end;
O Delphi apresenta a seguinte mensagem de erro:
Operator not applicable to this operand type
Se alguém puder me ajudar me explicando o porquê disso, agradeço.
Qual é a versão do seu Delphi? Algumas particularidades dos operadores "is" e "as" no sentido interface --> objeto passaram a funcionar apenas no Delphi XE.
Além disso a interface deve obrigatoriamente ter um GUID para que os operadores is e as funcionem com ela.
O que você pode fazer é um typecast forçado, tipo:
var
o: TObject;
o := TObject(minhainterface);
//operações com O agora podem usar is e as
mas cuidado: a linha o := TObject(minhainterface) cria uma nova referencia a uma instância já existente que não será contada pelo contador de referencias da interface. Se algum método ou trecho de código atribuir nil à minhainterface zerando o contador de referências a instância será destruida antes da hora e "o" terá uma referência inválida. Faça testes para verificar leaks ou corrupção de memória com o Fast Memory Manager 4 e não use o método Free em "o".
"Na Listagem 5 a linha (UmObjetoQualquer as ICarro).DeslocarSe cria uma referência a uma interface Icarro durante a conversão que não existirá mais depois da conversão, depois da execução do método, o que fará com que o objeto UmObjetoQualquer seja destruído."
Fiz o teste aqui e o objeto ainda continua existindo. Não seria o correto?
Pode acontecer de o objeto continuar acessível na memória, ou pelo menos alguns métodos que não usem dados internos do objeto, mas esse comportamento não é o esperado e é causado simplesmente por "sujeira" na memória.
Fiz um teste aqui e também funcionou, mesmo assim eu não recomendo. Diferenças na versão do Delphi ou na plataforma podem ocasionar erros.
Na época em que escrevi esse artigo eu usava Delphi XE em um computador de 64 bits, mas não me lembro a versão do Windows.
Hoje fiz o teste tanto no Delphi XE como no Delphi XE 4 usando Windows 7, mas num computador de 32 bits.
O porquê de eu esperar que essa construção cause um erro eu explico:
Dado que interfaces contam referências, então são "Reference Counted" e dado que você cria, converte e usa um objeto dentro de um mesmo método (como o evento click de um botão).
Se você criar o seu objeto já atribuindo ele à uma variável do tipo interface, assim:
var i: ICarro;
begin
i:= TCarro.Create; //1 referencia a um TCarro
(i as ICarro).DeslocarSe; //2 referencias ao TCarro //não precisa disso, apenas mostro como funciona normalmente
i.OutroMetodo;
end; //final do método, 0 referencias ao mesmo TCarro, então o objeto "hospedado" em I é destruido com free.
É esperado que um objeto apresente erro nesse tipo de conversão principalmente quando ele possui outro objeto dentro. Considere um objeto que tenha um StringList como um de seus campos.
var i: TCarro;
begin
i:= TCarro.Create; //1 referencia a um TCarro, no entanto I é um objeto, portanto não conta referências
(i as ICarro).DeslocarSe; //1 referencias ao TCarro hospedado em I convertido para ICarro
i.OutroMetodo; //antes de chegar aqui já temos 0 referências (pois não existe mais a referência temporária que estava sendo usada na conversão para ICarro), portanto Free é acionado e o objeto em I é destruído "por engano". Aqui deve ocorrer um erro ao se executar OutroMetodo, pois o próprio objeto está destruído .
end;
Veja esse exemplo que eu criei no Delphi Xe 4: (em www.vitorrubio.com.br/downloads/TesteInterfaces.zip)
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
Button2: TButton;
Button3: TButton;
Button4: TButton;
Button5: TButton;
Button6: TButton;
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
procedure Button3Click(Sender: TObject);
procedure Button4Click(Sender: TObject);
procedure Button5Click(Sender: TObject);
procedure Button6Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
IVeiculo = interface (iinterface)
['{1D2F0F1D-60CC-47BE-AD22-4DA59D87C7CA}']
procedure DeslocarSe;
procedure Parar;
end;
IVeiculoTerrestre = interface (IVeiculo)
['{CD718BB9-FE3A-4C19-8CE2-01EBB1843B96}']
procedure DeslocarSe;
procedure Parar;
procedure Rodar;
end;
ICarro = interface (IVeiculoTerrestre)
['{3B167780-8EA0-4F59-9DBF-A9F68F54F595}']
procedure DeslocarSe;
procedure Parar;
procedure Rodar;
end;
//TCarro = class(TInterfacedObject, ICarro) //nessa forma você pode atribuir a criação de um TCarro a uma variável ICarro, IVeiculoTerrestre e IVeiculo, mas não pode fazer conversões do tipo AS entre eles
TCarro = class(TInterfacedObject, ICarro, IVeiculoTerrestre, IVeiculo) //esta forma permite qualquer tipo de atribuição e conversão.
private
FPlaca: TStringList;
function GetPlaca: string;
procedure SetPlaca(value: string);
public
constructor Create;
destructor Destroy; override;
procedure DeslocarSe;
procedure Parar;
procedure Rodar;
property Placa: string read GetPlaca write SetPlaca;
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
{ TCarro }
constructor TCarro.Create;
begin
FPlaca := TStringList.Create;
end;
procedure TCarro.DeslocarSe;
begin
ShowMessage(Placa + ' Deslocando-se. ' + self.RefCount.ToString() + ' referências');
end;
destructor TCarro.Destroy;
begin
FPlaca.Free;
inherited;
end;
function TCarro.GetPlaca: string;
begin
Result := FPlaca.Text;
end;
procedure TCarro.Parar;
begin
ShowMessage(Placa + ' Parando.' + self.RefCount.ToString() + ' referências');
end;
procedure TCarro.Rodar;
begin
DeslocarSe;
end;
procedure TCarro.SetPlaca(value: string);
begin
FPlaca.Text := value;
end;
procedure TForm1.Button1Click(Sender: TObject);
var C: TCarro;
begin
C := TCarro.Create; //tenho um objeto, mas C não conta referencias
C.Placa := 'ABC1234';
(C as ICarro).Rodar; //converte e roda na mesma linha, mas zera o contador de referencias na proxima linha
C.Parar; //isso dá erro
C.Rodar;
C.Parar;
end;
procedure TForm1.Button2Click(Sender: TObject);
var C: TCarro;
I: IVeiculoTerrestre;
begin
C := TCarro.Create; //tenho um objeto, mas C não conta referencias
C.Placa := 'ABC1234';
I := (C as ICarro); //Conversão joga para I o controle da contagem de referencias
I.Rodar; //uso normal
I.Parar; //uso normal
I.Rodar; //uso normal
I.Parar; //uso normal
C.Rodar; //Funciona normal, mas não se deve misturar objetos comuns com interfaces, por isso para de usar C uma vez convertido para I
C.Parar; //Funciona normal, mas não se deve misturar objetos comuns com interfaces, por isso para de usar C uma vez convertido para I
end;
procedure TForm1.Button3Click(Sender: TObject);
var C: TCarro;
I: IVeiculo;
begin
C := TCarro.Create; //tenho um objeto, mas C não conta referencias
C.Placa := 'ABC1234';
I := (C as ICarro); //Conversão joga para I o controle da contagem de referencias
I.DeslocarSe;
I.Parar;
//I agora pode ser convertido para qualquer uma das interfaces ancestrais de ICarro sem perder o controle de referência
(I as ICarro).Rodar;
(I as ICarro).Parar;
(I as IVeiculoTerrestre).Rodar;
(I as IVeiculoTerrestre).Parar;
(I as IVeiculo).DeslocarSe;
(I as IVeiculo).Parar;
C.Rodar; //Funciona normal, mas não se deve misturar objetos comuns com interfaces, por isso para de usar C uma vez convertido para I
C.Parar; //Funciona normal, mas não se deve misturar objetos comuns com interfaces, por isso para de usar C uma vez convertido para I
end;
procedure TForm1.Button4Click(Sender: TObject);
var C: TCarro;
iv1, iv2: IVeiculo;
ivt1, ivt2: IVeiculoTerrestre;
ic: ICarro;
begin
C := TCarro.Create;
C.Placa := 'ABC1234';
ic := (C as ICarro);
ivt1 := (C as IVeiculoTerrestre);
iv1 := (C as IVeiculo);
ivt2 := (ic as IVeiculoTerrestre);
iv2 := (ic as IVeiculo);
ic.DeslocarSe;
ic.Parar;
ivt1.Rodar;
ivt1.Parar;
iv1.DeslocarSe;
iv1.Parar;
ivt2.Rodar;
ivt2.Parar;
iv2.DeslocarSe;
iv2.Parar;
end;
procedure TForm1.Button5Click(Sender: TObject);
var C: TCarro;
iv1, iv2: IVeiculo;
ivt1, ivt2: IVeiculoTerrestre;
ic: ICarro;
begin
C := TCarro.Create;
C.Placa := 'ABC1234';
ic := (C as ICarro);
ic.DeslocarSe;
ic.Parar;
if Supports(c, IVeiculoTerrestre, ivt1) then
begin
ivt1.Rodar;
ivt1.Parar;
end;
if Supports(c, IVeiculo, iv1) then
begin
iv1.DeslocarSe;
iv1.Parar;
end;
if Supports(ic, IVeiculoTerrestre, ivt2) then
begin
ivt2.Rodar;
ivt2.Parar;
end;
if Supports(ic, IVeiculo, iv2) then
begin
iv2.DeslocarSe;
iv2.Parar;
end;
end;
procedure TForm1.Button6Click(Sender: TObject);
var C: TCarro;
iv1, iv2: IVeiculo;
ivt1, ivt2: IVeiculoTerrestre;
ic: ICarro;
begin
C := TCarro.Create;
C.Placa := 'ABC1234';
ic := ICarro(C);
ivt1 := IVeiculoTerrestre(C);
iv1 := IVeiculo(C);
ivt2 := IVeiculoTerrestre(ic);
iv2 := IVeiculo(ic);
ic.DeslocarSe;
ic.Parar;
ivt1.Rodar;
ivt1.Parar;
iv1.DeslocarSe;
iv1.Parar;
ivt2.Rodar;
ivt2.Parar;
iv2.DeslocarSe;
iv2.Parar;
end;
end.
Ele
Ele não deu erro como eu esperava no primeiro botão, mesmo assim eu não aconselho a fazer um uso de interfaces + objetos como aquele, principalmente quando seus objetos são passados para métodos ou retornados deles.
Infelizmente não consegui reproduzir esse erro hoje. Compilei e rodei esse código no Delphi XE, no Delphi XE4 e no Lazarus, e em todos os casos funcionou.
Mesmo assim, cuidado redobrado ao se passar variáveis do tipo interface como parâmetros.
Quando se chega ao final do método o contador de referencias da interface chega a zero, porque ele não conta com a referência armazenada na variável objeto FORA do escopo da função. Sendo assim o free é chamado e o objeto destruido prematuramente.
Isso acontece quando você faz algo como:
var
objAviao: TAviao;
objCarro: TCarro;
objBarco: TBarco;
UmObjetoQualquer: TInterfacedObject;
OutroVeiculo: IVeiculo;
OutroCarro: ICarro;
OutroBarco: IBarco;
UmSerVivo: ISerVivo;
begin
//cria os objetos e os mantem com referencia em uma variavel do tipo objeto
objaviao:= TAviao.Create;
objCarro:= TCarro.Create;
objBarco:= Tbarco.Create;
UmObjetoQualquer := TCarro.Create;
//não se deve fazer isso direto com os objetos a não ser que exista uma interface apontando para eles
//fazendo isso o contador de referencia pode zerar
FazVeiculoAndar(objaviao); //saindo do escopo dessa função o argumento atinge refcount zero, destruindo objaviao sem ligar para sua referencia objaviao fora da função
FazVeiculoAndar(objCarro); //saindo do escopo dessa função o argumento atinge refcount zero, destruindo objCarro sem ligar para sua referencia objCarro fora da função
FazVeiculoAndar(objBarco); //saindo do escopo dessa função o argumento atinge refcount zero, destruindo objBarco sem ligar para sua referencia objBarco fora da função
//a partir daqui os objetos não funcionam mais porque o contador de referência foi zerado enquanto o objeto foi
//convertido para interface
objAviao.Voar;
objCarro.Rodar;
objBarco.Velejar;
//a partir daqui o erro pode ocorrer a qualquer momento
(objAviao as IAviao).Voar;
(objCarro as ICarro).Rodar;
(objBarco as IBarco).Velejar;
(UmObjetoQualquer as ICarro).Rodar;
Mas o certo seria criar algo como:
var
aviao: IAviao;
carro: ICarro;
barco: IBarco;
OutroVeiculo: IVeiculo;
begin
//conversão objeto/interface
aviao := TAviao.Create;;
carro := TCarro.Create;
barco := Tbarco.Create;
//isso seria o correto
FazVeiculoAndar(aviao);
FazVeiculoAndar(carro);
FazVeiculoAndar(barco);
aviao.Voar;
carro.Rodar;
barco.Velejar;
No meu código original eu tentei mostrar em um mesmo botão a maneira certa e errada de se fazer isso, deixando parte do código comentado, e isso não ficou muito didático. Eu mesmo me enrolei depois de ler.
Separei esse código em 2 botões, sendo o antigo button3 o modo incorreto, e o novo button8 o método correto. O novo exemplo pode ser baixado em: http://www.vitorrubio.com.br/downloads/CD134_Interfaces_V2_Reformulado.zip



