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

05/03/2006

Colegas,

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

[*:f058a83c93][b:f058a83c93]TClasseA[/b:f058a83c93] = 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][b:f058a83c93]TClasseB[/b:f058a83c93] = 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][b:f058a83c93]TMinhaClasse[/b:f058a83c93] = 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´
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.


Vinicius2k

Respostas

06/03/2006

Michael

Olá [b:b4f7fc4d08]Vinícius[/b:b4f7fc4d08]!

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

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 [b:b4f7fc4d08]TClasseA [/b:b4f7fc4d08]e [b:b4f7fc4d08]TClasseB [/b:b4f7fc4d08]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 [b:b4f7fc4d08]TClasseBase[/b:b4f7fc4d08], 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:

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


Responder Citar

06/03/2006

Vinicius2k

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:

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+


Responder Citar

06/03/2006

Michael

A não ser que a implementação de [b:9148db7695]TMinhaClasse [/b:9148db7695]faça algo assim:

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 [b:9148db7695]FClasse[/b:9148db7695]. Correto?

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

 Classe := TMinhaClasse.Create(TClasseC);


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

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


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

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

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 [b:9148db7695]TMinhaClasse [/b:9148db7695]e [b:9148db7695]TClasseA [/b:9148db7695]e [b:9148db7695]TClasseB [/b:9148db7695]é 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


Responder Citar

06/03/2006

Vinicius2k

...pelo exemplo que vc passou, parece que vc vai fazer como eu esperava: os mesmos métodos serão chamados, independentemente de quem seja [b:a511771aeb]FClasse[/b:a511771aeb]. 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.

A vantagem de usar um parâmetro do tipo [b:a511771aeb]class of[/b:a511771aeb] ao invés de um tipo enumerado é a abstração que se ganha. [b:a511771aeb]TMinhaClasse [/b:a511771aeb]vai funcionar não importando quem seja [b:a511771aeb]FClasse[/b:a511771aeb]. Se no futuro vc criar [b:a511771aeb]TClasseC[/b:a511771aeb], não vai precisar mudar nada em [b:a511771aeb]TMinhaClasse[/b:a511771aeb]. Será apenas necessário chamar o construtor passando a classe correta.
 Classe := TMinhaClasse.Create(TClasseC);
Mas se eu entendi mal e no seu caso [b:a511771aeb]TMinhaClasse [/b:a511771aeb]realmente precisa saber quem é [b:a511771aeb]FClasse[/b:a511771aeb]

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 [b:a511771aeb]class of[/b:a511771aeb], 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:


Responder Citar

06/03/2006

Michael

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 [b:48e3b0fc6d]untClasseGeral[/b:48e3b0fc6d]
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 [b:48e3b0fc6d]untClasseAB [/b:48e3b0fc6d](coloquei na mesma unit para economizar espaço aqui)
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 [b:48e3b0fc6d]Teste[/b:48e3b0fc6d]
uses untClasseGeral, untClasseAB;
(...)
var
  MinhaClasse1, MinhaClasse2: TMinhaClasse;
begin
  MinhaClasse1 := TMinhaClasse.Create(TClasseA);
  MinhaClasse2 := TMinhaClasse.Create(TClasseB);
end;


Desta forma abstrai-se [b:48e3b0fc6d]TMinhaClasse [/b:48e3b0fc6d]da existência de [b:48e3b0fc6d]TClasseA [/b:48e3b0fc6d]e [b:48e3b0fc6d]TClasseB[/b:48e3b0fc6d], ou de qualquer outra derivada de [b:48e3b0fc6d]TClasseBase[/b:48e3b0fc6d]. 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.

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 [b:48e3b0fc6d]case [/b:48e3b0fc6d]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


Responder Citar

06/03/2006

Vinicius2k

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

No exemplo que você passou:
uses untClasseGeral, untClasseAB;
(...)
var
  MinhaClasse1, MinhaClasse2: TMinhaClasse;
begin
  MinhaClasse1 := TMinhaClasse.Create(TClasseA);
  MinhaClasse2 := TMinhaClasse.Create(TClasseB);
end;


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

O [b:bb7890224a]case[/b:bb7890224a] ao qual me referi e na unit aonde TMinhaClasse é criada, no caso, a unit [b:bb7890224a]Teste[/b:bb7890224a]. 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:
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 [b:bb7890224a]Teste[/b:bb7890224a], conhecer todas a implementações de TClasseBase.

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

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.


Responder Citar

06/03/2006

Vinicius2k

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 ! ;)


Responder Citar

07/03/2006

Michael

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 [b:c2f6e5aa16]implementation [/b:c2f6e5aa16]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 [b:c2f6e5aa16]interface[/b:c2f6e5aa16]. 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


Responder Citar

07/03/2006

Vinicius2k

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 [b:a8e50db3ab]case[/b:a8e50db3ab] para instanciar ´MinhaClasse´.
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 ;)


Responder Citar

08/03/2006

Michael

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


Responder Citar