Array
(
)

Referência circular. Existe solução sem type casts?

Vinicius2k
   - 05 mar 2006

Colegas,

Peço que, por favor, me esclareçam uma dúvida...
Situação:
[list:f058a83c93][*:f058a83c93]TClasseBase = class(TObject), abstrata. Unit ´untClasseBase´.

[*:f058a83c93]TClasseA = class(TClasseBase), implementando as propriedades e métodos necessários de acordo com com necessidade X. Unit ´untClasseA´, contendo na uses da interface, ´untClasseBase´.

[*:f058a83c93]TClasseB = class(TClasseBase), implementando as propriedades e métodos necessários de acordo com com necessidade Y. Unit ´untClasseB´, contendo na uses da interface, ´untClasseBase´.

[*:f058a83c93]TMinhaClasse = class(TObject) que contém uma propriedade ´Classe´ do tipo TClasseBase. Esta propriedade ´Classe´ é inicializada pelo constrututor de forma a ser TClasseA ou TClasseB. Qual das duas classes ela será, dependerá do parametro passado ao construtor. Unit ´untMinhaClasse´, contendo na uses da interface as units ´untClasseBase´, ´untClasseA´ e ´untClasseB´
#Código

TQualClasse = (qcClasseA, qcClasseB);

TMinhaClasse = class(TObject)
private
FClasse: TClasseBase;
public
{...}
property Classe read FClasse write FClasse;
end;

implementation

constructor TMinhaClasse.Create(AQualClasse: TQualClasse);
begin
{...}
case AQualClasse of
qcClasseA: Classe := TClasseA.Create;
qcClasseB: Classe := TClasseB.Create;
end;
{...}
end;
[/list:u:f058a83c93]
Eu gostaria de manter na mesma unit TClasseBase e TMinhaClasse, como ´untClasseGeral´, porém ela necessitaria ter em sua uses da interface, ´untClasseA´ e ´untClasseB´. Por sua vez, ´untClasseA´ e ´untClasseB´ necessitam em sua uses da interface de ´untClasseGeral´. Daí vem o problema de referencia circular.

Fiz alguns testes básicos e talvez seja possível implementar isso utilizando type casts, mas é a melhor (ou única) solução? Ou seria esta a forma correta de trabalhar esta questão?
Não é um problema crítico, mas gostaria, se possível, de organizar melhor o código, com menos units.

Desde já agradeço a colaboração.


Michael
   - 06 mar 2006

Olá Vinícius!

Como TClasseBase é abstrata e TClasseA e TClasseB são implementações dela, vc pode fazer isso:

#Código

type
TClasseBaseClass = class of TClasseBase;

TClasseBase = class
(...)

constructor TMinhaClasse.Create(_Class: TClasseBaseClass);
begin
{...}
FClasse = _Class.Create;
end;


Desta forma vc não precisa incluir a unit onde TClasseA e TClasseB estão declaradas, eliminando o erro de referência circular. Pessoalmente neste contexto prefiro esta abordagem do que fazer uso de parâmetros para saber qual classe instanciar. E fica bem mais limpo o código.

Porém, isso não vai funcionar se estas classes, além de implementarem os métodos e propriedades de TClasseBase, publicarem algum atributo específico, pois para ter acesso às características de cada classe vc precisaria declarar as units delas.

Vc então usa este esquema assim:

#Código
uses untClasseGeral, untClasseA, untClasseB;

var
ClasseA, ClasseB: TMinhaClasse;
begin
ClasseA := TMinhaClasse.Create(TClasseA);
ClasseB := TMinhaClasse.Create(TClasseB);
end;


E viva a orientação a objetos! ;-)

[]´s


Vinicius2k
   - 06 mar 2006

Olá Michael!

Muito obrigado pela resposta!
Veja bem, apesar das classes que implementam a classe abstrata não publicarem nada de novo, eu acho que não vou conseguir utilizar a sua abordagem.

TMinhaClasse será instanciada pela aplicação e a sua propriedade Classe será diretamente manipulada. Exemplo:

#Código

var
MinhaClasse: TMinhaClasse;
{...}
case opcao of
1: MinhaClasse := TMinhaClasse.Create(qcClasseA);
2: MinhaClasse := TMinhaClasse.Create(qcClasseB);
end;
MinhaClasse.Classe.FacaIsto;
MinhaClasse.Classe.FacaAquilo;


As implementações A e B de TClasseBase são bem distintas e a aplicação irá decidir qual das duas classes será utilizada em Run Time. Por este motivo ainda vejo a necessidade de que o construtor conheça a classe que irá instanciar para a propriedade Classe.

Estou correto com meu raciocício?
T+


Michael
   - 06 mar 2006

A não ser que a implementação de TMinhaClasse faça algo assim:

#Código

if FClasse is TClasseA then
FClasse.FacaIsso
else if FClasse is TClasseB then
FClasse.FacaAquilo;


A minha sugestão vai funcionar, com certeza. Porém, pelo exemplo que vc passou, parece que vc vai fazer como eu esperava: os mesmos métodos serão chamados, independentemente de quem seja FClasse. Correto?

A vantagem de usar um parâmetro do tipo class of ao invés de um tipo enumerado é a abstração que se ganha. TMinhaClasse vai funcionar não importando quem seja FClasse. Se no futuro vc criar TClasseC, não vai precisar mudar nada em TMinhaClasse. Será apenas necessário chamar o construtor passando a classe correta.

#Código
Classe := TMinhaClasse.Create(TClasseC);


Mas se eu entendi mal e no seu caso TMinhaClasse realmente precisa saber quem é FClasse, há pelo menos duas soluções. Uma já foi apresentada, e repito aqui:

#Código
if FClasse is TClasseA then
FClasse.FacaIsso
else if FClasse is TClasseB then
FClasse.FacaAquilo;


Entretanto para que TClasseA e TClasseB sejam reconhecidas vc vai precisar declarar suas units na cláusula uses, e vamos ter novamente o problema de referência circular.

A outra solução - que realmente resolve o problema - é usar os atributos de TObject, mais precisamente a propriedade ClassName ou o método ClassNameIs. Ambos servem para se obter a representação string do nome de uma classe. O exemplo anterior poderia ser mudado para:

#Código
if FClasse.ClassNameIs(´TClasseA´) then
FClasse.FacaIsso
else if FClasse.ClassNameIs(´TClasseB´) then
FClasse.FacaAquilo;


Isso elimina a necessidade de se referenciar as units.

Se o seu caso for realmente esse, parece que o acoplamento entre TMinhaClasse e TClasseA e TClasseB é alto o suficiente para justificar a permanência de todas as classes na mesma unit, o que anularia o problema que motivou vc a postar essa mensagem. Creio porém que não é este o seu caso. Ou é? ;-)

[]´s


Vinicius2k
   - 06 mar 2006


Citação:
...pelo exemplo que vc passou, parece que vc vai fazer como eu esperava: os mesmos métodos serão chamados, independentemente de quem seja FClasse. Correto?

Exatamente. Os mesmos métodos serão executados, independente de qual classe esteja instanciada em FClasse de TMinhaClasse.
O caso é que as classes ´filhas´ (A, B... N) sempre terão a sua própria implementação dos métodos e Gets/Sets da classe abstrata.


Citação:
A vantagem de usar um parâmetro do tipo class of ao invés de um tipo enumerado é a abstração que se ganha. TMinhaClasse vai funcionar não importando quem seja FClasse. Se no futuro vc criar TClasseC, não vai precisar mudar nada em TMinhaClasse. Será apenas necessário chamar o construtor passando a classe correta.
#Código

Classe := TMinhaClasse.Create(TClasseC);


Mas se eu entendi mal e no seu caso TMinhaClasse realmente precisa saber quem é FClasse
Não... acho que fui eu quem não entendi a sua proposta da primeira vez... :oops: Não percebi na sua sugestão o class of, desta forma eu penso que poderia agrupar as classes na mesma unit.

Porém, ainda desta forma, eu também precisaria ter todas as units das classes filhas (A, B... N) na uses da unit aonde eu for criar TMinhaClasse e efetuar aquele case de acordo com a opção do usuário, certo?
No aspecto organização, não ficaria melhor então deixar como está (units independentes para TClasseBase e TMinhaClasse e o parametro do tipo enumerado)? Isto porque, neste caso eu só preciso ter na uses de onde estou criando TMinhaClasse a unit aonde se encontra TMinhaClasse.

Afe... meio confuso isso... :roll:


Michael
   - 06 mar 2006


Citação:
Porém, ainda desta forma, eu também precisaria ter todas as units das classes filhas (A, B... N) na uses da unit aonde eu for criar TMinhaClasse e efetuar aquele case de acordo com a opção do usuário, certo?


Não. Acho que vc não pegou o ´espírito da coisa´... ;-)

Veja com um exemplo:

unit untClasseGeral
#Código

type
TClasseBaseClass = class of TClasseBase;

TClasseBase = class
public
constructor Create; virtual; abstract;
end;

TMinhaClasse = class
private
FClasse: TClasseBase;
public
constructor Create(_Class: TClasseBaseClass);
end;

implementation

{ TMinhaClasse }

constructor TMinhaClasse.Create(_Class: TClasseBaseClass);
begin
FClasse := _Class.Create;
end;


unit untClasseAB (coloquei na mesma unit para economizar espaço aqui)
#Código
interface
uses Unit2, Dialogs;

type
TClasseA = class(TClasseBase)
public
constructor Create; override;
end;

TClasseB = class(TClasseBase)
public
constructor Create; override;
end;

implementation


{ TClasseA }

constructor TClasseA.Create;
begin
inherited;
ShowMessage(´Construtor da classe TClasseA´);
end;

{ TClasseB }

constructor TClasseB.Create;
begin
inherited;
ShowMessage(´Construtor da classe TClasseB´);
end;


unit Teste
#Código
uses untClasseGeral, untClasseAB;
(...)
var
MinhaClasse1, MinhaClasse2: TMinhaClasse;
begin
MinhaClasse1 := TMinhaClasse.Create(TClasseA);
MinhaClasse2 := TMinhaClasse.Create(TClasseB);
end;


Desta forma abstrai-se TMinhaClasse da existência de TClasseA e TClasseB, ou de qualquer outra derivada de TClasseBase. Vc não precisa usar o case com o tipo enumerado pq o construtor de TMinhaClasse vai receber a classe certa que ele tem que inicializar. Vc não vai precisar declarar as units de TClasseA e TClasseB na unit de TMinhaClasse, pois ela não tem conhecimento delas.


Citação:
No aspecto organização, não ficaria melhor então deixar como está (units independentes para TClasseBase e TMinhaClasse e o parametro do tipo enumerado)? Isto porque, neste caso eu só preciso ter na uses de onde estou criando TMinhaClasse a unit aonde se encontra TMinhaClasse.

Pessoalmente acho que não, pelas razões já apresentadas. Para que fazer um case para diferenciar o parâmetro se vc já sabe que ele vem correto, pronto para vc usar? ;-)

Pegou agora? ;-)

Se ainda estiver com dúvidas avise aqui q mando um exemplo para vc.

[]´s


Vinicius2k
   - 06 mar 2006

Agora, acho que foi vc não ´pegou´ :)

No exemplo que você passou:
#Código

uses untClasseGeral, untClasseAB;
(...)
var
MinhaClasse1, MinhaClasse2: TMinhaClasse;
begin
MinhaClasse1 := TMinhaClasse.Create(TClasseA);
MinhaClasse2 := TMinhaClasse.Create(TClasseB);
end;


Note que na unit Teste você precisou declarar a unit untClasseAB, porque precisa passar a classe que deseja criar como parametro para o construtor de TMinhaClasse.

O case ao qual me referi e na unit aonde TMinhaClasse é criada, no caso, a unit Teste. Qual classe será criada dependerá de uma escolha do usuário e neste caso, eu precisaria preparar para qualquer escolha, colocando na uses todas as classes filhas de TClasseBase. Então, meu código ficaria assim:
#Código
uses untClasseGeral, untClasseA, untClasseB,..., untClasseN;
(...)
var
MinhaClasse: TMinhaClasse;
begin
case opcao of
1: MinhaClasse := TMinhaClasse.Create(TClasseA);
2: MinhaClasse := TMinhaClasse.Create(TClasseB);
{...}
N: MinhaClasse := TMinhaClasse.Create(TClasseN);
end;
end;


Entende porque neste caso, eu fico com a impressão de que seria preferível as ´uses´ na TMinhaClasse? Para que não seja necessário para a unit Teste, conhecer todas a implementações de TClasseBase.

O que está ´pegando´ é: Quem deve conhecer todas as implementações? A unit de TMinhaClasse ou a unit Teste

Só para melhorar o contexto, desde o princípio eu falei sempre em A e B, mas na verdade eu tenho previsão de possuir entre 15 e 20 implementações diferentes para TClasseBase.


Vinicius2k
   - 06 mar 2006

Michael,

Só para registrar, implementei conforme sua primeira sugestão e funcionou perfeitamente. A única questão que persiste é:

É o mais correto (segundo as ´boas maneiras´ da OOP) que a unit do projeto que instancia TMinhaClasse tenha em sua ´uses´ todas as units que implementam TClasseBase (todas apenas porque eu preciso de todas as possibilidades no meu case)?

Analisando mais calmamente, penso que sim. Pois se eu implementar uma nova classe descendente de TClasseBase, precisaria modificar apenas a unit do projeto...

Grato pela ajuda ! ;)


Michael
   - 07 mar 2006

A minha dica era para resolver o problema de referência circular que vc indicou. Porém, lendo novamente as mensagens deste post, constatei que bastaria vc declarar untClasseA e untClasseB na seção implementation da unit onde TClasseBase e TMinhaClasse estão declaradas.

Desta forma o seu código inicial ainda pode funcionar. A não ser que vc precisa declarar untClasseA..untClasseN na seção interface. Mas pelos posts isso não ficou claro.

Sobre as units, se as implementações de TClasseBase forem só suas, então não vejo problema em tê-las na mesma unit untGeral. Inicialmente eu pensei que vc criaria as classes gerais e posteriormente outro programador poderia, ou vc mesmo no futuro, implementar TClasseA..TClasseN. Desta forma acho que não seria indicado o acesso à unit untGeral.

De qualquer jeito não conheço nenhuma regra da OO que fale sobre isso. Na verdade isso parece mais composição de classes, de UML.

[]´s


Vinicius2k
   - 07 mar 2006

Valeu Michael!

Não sei se você ainda não compreendeu esta parte, mas a unit ´Geral´ é apenas para a TClasseBase e TMinhaClasse. As implementações ´filhas´ de TClasseBase ficarão uma em cada unit.

O problema da referência circular vinha de que (imaginando units distintas):

1. A TClasseBase está em uma unit sozinha, apenas com as assinaturas dos métodos e propriedades.

2. Cada implementação da TClasseBase chamada de TClasseA, TClasseB...TClasseN, cada uma delas estando em units distintas e, logicamente, por serem derivadas da classe abstrata TClasseBase precisam ter em sua uses a unit de TClasseBase (1).

3. A unit de TMinhaClasse precisa ter em sua uses da interface a unit de TClasseBase (1). Isto porque existe esta classe possui uma propriedade chamada ´Classe´ do tipo TClasseBase.
Como meu construtor antigo de TMinhaClasse precisava saber qual classe ´filha´ instanciar para a prorpiedade ´Classe´ eu também precisava de das units de cada classe ´filha´ de TClasseBase em sua uses (2).

Até aqui, sem problemas: ´2´ referenciava ´1´ e ´3´ referenciava ´1´ e ´2´. A referência circular ocorria quando eu pretendia agregar na mesma unit (a untGeral) os ítens 1 e 3. Então, ´2´ refenciava ´1´ e ´1´ referenciava ´2´ = erro.

Eu consegui fazer a agregação usando ´class of´ no construtor de TMinhaClasse e retirando da uses da interface a lista de units das classes ´filhas´ de TClasseBase porque o meu construtor não precisa mais saber qual classe ´filha´ instanciar, ele irá recebê-la como parametro.

No form aonde TMinhaClasse é instanciada, eu tenho na uses da interface a unit de TMinhaClasse, porque um objeto chamado ´MinhaClasse´, do tipo TMinhaClasse é pertencente ao form. Na uses da implementation deste form estão as units das classes ´filhas´ de TClasseBase porque no ´Create´ do form é lida a opção do usuário e efetuado o case para instanciar ´MinhaClasse´.
#Código

case opcao of // este código está no form que instancia TMinhaClasse
1: MinhaClasse: TMinhaClasse.Create(TClasseA);
2: MinhaClasse: TMinhaClasse.Create(TClasseB);
{... etc ...}
end;


Não falei que era confuso! É difícil explicar isso...

Mas o importante é que está como eu gostaria agora ;)


Michael
   - 08 mar 2006

Eu havia entendido o que vc queria. Ficou confuso só uma coisa, mas deixa para lá. Já está funcionando e é isso que importa. ;-)

E a parada da DevMedia? Mandou o email?

[]´s