Herança de interfaces e implementação explícita de interfaces-base #AQuemPossaInteressar

Categoria: Artigos
Categoria Pai: Addicted 2 Delphi!
Acessos: 3465
Imagem meramente ilustrativa

Se você leu o texto introdutório deste artigo, deve ter ficado bem confuso quando eu disse que ia falar a respeito da herança de interfaces e da necessidade de "explicitar" interfaces específicas de uma hierarquia quando da criação de uma classe que implementa uma interface que possui uma linha hierárquica já definida. Calma Calma, não é tão complicado, apenas eu não achei um meio mais fácil de explicar com palavras. Um exemplo vai resolver isso, por ora, vamos começar com algo mais simples.

Como se sabe, classes são herdáveis. Uma classe que herda de outra classe é uma extensão da sua classe pai, o que significa que a classe filha tem todas as características da classe pai e mais um conjunto de características próprias. É por isso que outras linguagens utilizam a palavra-chave "extends" para realizar uma herança. No Delphi, como sabemos, isso é bem mais simples:

type
	TParentClass = class
	private
		FName: String;
	public
		property Name: String read FName write FName;
	end;
	
	TChildClass = class(TParentClass)
	private
		FAge: Byte;
	public
		property Age: Byte read FAge write FAge;
	end;

No exemplo anterior, TChildCass tem, na prática, duas propriedades (Name e Age), diz-se portanto que TChildClass estendeu TParentClass ao herdar sua propriedade Name e introduzir uma propriedade exclusiva (Age). Todos já sabemos disso, mas talvez o que alguns não saibam é que algo semelhante acontece com as interfaces. Ignore a simplicidade do exemplo e até mesmo seu eventual afastamento da realidade, atenha-se ao que ele representa:

type
	IAnimal = interface
		procedure Walk;
		procedure Eat;
	end;

	IBird = interface(IAnimal)
		procedure Fly;
	end;

	ICat = interface(IAnimal)
		procedure Run;
	end;

No exemplo anterior, IBird herda características de IAnimal, assim como ICat. Uma classe que implementa IBird ou ICat, precisará implementar os métodos existentes tanto em IBird e ICat como em IAnimal:

type
	TMyPet = class(TInterfacedObject,ICat)
	public
		procedure Run;
		procedure Walk;
		procedure Eat;
	end;

Para quem nunca trabalhou com interfaces, saiba que toda interface exige a implementação dos métodos QueryInterface_AddRef_Release. Ao herdar de TInterfacedObject não precisamos implementar estes métodos porque eles já são implementados de forma correta nesta classe, dito isso, o exemplo anterior mostra que ao implementar ICat, nós precisamos implementar os métodos Walk e Eat, de IAnimal (pai de ICat) e Run de ICat (filha de IAnimal).

Neste momento você já deve ter entendido como funciona a herança de interfaces e é agora onde entra a explicação a respeito da necessidade de "explicitar" interfaces específicas de uma hierarquia quando da criação de uma classe que implementa uma interface que possui uma linha hierárquica já definida. Observe o exemplo a seguir:

type
	TMyPet = class(TInterfacedObject,ICat,IAnimal)
	public
		procedure Run;
		procedure Walk;
		procedure Eat;
	end;

O exemplo anterior, compila sem qualquer modificação adicional e se você entendeu mesmo a herança de interfaces você deve estar se perguntando por que eu explicitei IAnimal, se ICat já herda de IAnimal e se eu já implementei os métodos requeridos de ambas as interfaces. Normalmente não é necessário incluir nenhuma das interfaces ancestrais ao se criar uma classe que implementa uma interface. Como no primeiro exemplo de TMyPet, basta implementar a interface mais específica da hierarquia, entretanto certamente existe um bom motivo para incluir interfaces ancestrais.

Ao utilizar interfaces, é muito comum passar instâncias de classes que implementam interfaces em parâmetros de tipos de interfaces ou instanciar classes atribuindo o objeto a uma variável do tipo de uma interface. Observe o trecho de pseudocódigo a seguir:

procedure Feed(AAnimal: IAnimal);
begin
	AAnimal.Eat;
end;

var
	MyPet: TMyPet;
	
MyPet := TMyPet.Create;

Feed(MyPet);

No exemplo anterior definimos um procedure Feed que serve para alimentar qualquer animal. Por definição, se todos os animais se alimentam, é muito óbvio que o parâmetro do procedure Feed seja do tipo da interface IAnimal, a qual estabelece "um contrato" que diz que a uma classe compatível precisa ter um método Eat implementado. Ora, nossa classe TMyPet implementa um método Eat, logo ela é compatível, certo? Depende! Se a classe TMyPet implementar apenas ICat, o exemplo não vai compilar (E2010 Incompatible types: 'IAnimal' and 'TMyPet') e isso acontece porque ao passar uma instância de TMyPet em um parâmetro do tipo IAnimal, uma tentativa de conversão é realizada. Implicitamente o que é feito é a utilização do operador "as" da seguinte forma, ANTES da atribuição ser efetivada AAnimal := (MyPet as IAnimal):

var
	MyPet: TMyPet;
	Animal: IAnimal;
	
	MyPet := TMyPet.Create;
	
	Animal := MyPet; // E2010 Incompatible types: 'IAnimal' and 'TMyPet'
	Animal := (MyPet as IAnimal); // E2015 Operator not applicable to this operand type

O exemplo anterior mostra os erros que ocorrem quando se tenta converter implicitamente e explicitamente uma instância de TMyPet em IAnimal. Falando de uma forma mais correta, o erro ocorre porque não é possível acessar TMyPet como IAnimal (esta é a forma correta de se referir a este tipo de conversão entre classe e interface). Este erro somente ocorrerá se TMyPet não implementar explicitamente IAnimal, isto é, se TMyPet = class(TInterfacedObject,ICat). Caso IAnimal seja implementado explicitamente, isto é, se TMyPet = class(TInterfacedObject,ICat,IAnimal), tanto a conversão implícita como a explícita ocorrerão perfeitamente.

Do ponto de vista técnico o erro ocorre porque o compilador apenas conhece as interfaces que estiverem listadas explicitamente na definição de uma classe. Este tipo de "limitação" resulta na prática comum de incluir na lista de interfaces da declaração de uma classe, todas as interfaces ancestrais, ou pelo menos, todas as interfaces que, em algum momento, poderão ser utilizadas em algum tipo de conversão a partir da classe atual, seja ela explícita ou implícita.

Isso parece muito estranho, porque eu deveria me preocupar?

Se você está desenvolvendo um sistema utilizando interfaces, provavelmente você não precisará se preocupar com a declaração explícita de interfaces porque você saberá quando, e se, conversões serão realizadas, sendo assim, o máximo que você poderá fazer é incluir na declaração de suas classes apenas as interfaces estritamente necessárias, e não todas as constituintes de uma hierarquia, porém quando você precisa implementar interfaces que não foram construídas por você isso se torna potencialmente necessário.

Há alguns meses eu recomecei a estudar OTA (Open Tools API) e tive todas estas dúvidas que eu expliquei neste artigo. Em todos os exemplos que eu achei, uma coisa que sempre me chamava atenção era porque era necessário declarar explicitamente certas interfaces que já estavam implicitamente incluídas por meio de interfaces filhas. Praticamente todas as funções usadas no OTA usam parâmetros que são interfaces e o que ocorre é que ao informar em um destes parâmetros uma classe que não implementa todas as interfaces esperadas, erros acontecem, e sendo assim, é muito mais seguro ao se trabalhar com OTA, sempre implementar todas as interfaces (ou boa parte delas) de uma hierarquia, mesmo não parecendo ser necessário. Isso só é necessário porque muitas das interfaces contidas na unit ToolsAPI, são "evoluídas" de forma incremental, herdando sempre de uma interface anterior e assim, em algum momento da implementação, algumas funções ficaram muito acopladas a interfaces específicas, o que nos obriga a implementar interfaces adicionais só por segurança.