26 de janeiro de 2010

Um Factory Method mais flexível em Delphi

No ano passado, eu mostrei aqui no blog o funcionamento do Design Pattern criacional chamado Factory Method. A implementação apresentada naquela ocasião seguia a solução tradicional para este pattern, isto é, exigia a criação de uma função - o método Factory - que, baseada nos valores de um ou mais parâmetros, decidia qual classe deveria ser instanciada.

No entanto, essa abordagem tem uma característica que pode se tornar um inconveniente em certas situações: o Factory Method obrigatoriamente tem que conhecer de antemão todas as classes que forem passíveis de serem criadas por ele. O "conhecer" aqui significa que o constructor de cada classe é chamado diretamente dentro da função Factory.

Neste momento, por exemplo, estou montando uma infraestrutura que a ABC71 usará para criar programas de assistência à implantação do nosso ERP. Dependendo do assistente desejado, os tipos e as quantidades de passos incluidos em cada programa vão variar. Por isso, o Factory Method tradicional não satisfaz plenamente já que ele exigiria que eu incluísse todos os tipos de passos possíveis em todos os programas, o que deixaria esses programas muito maiores do que deveriam.

Para resolver esse inconveniente, fiz uma pequena alteração no Factory Method original para que ele usasse uma lista de ponteiros para funções indexada pelo nome da classe que a função é capaz de criar. Quando quero instanciar uma determinada classe, informo ao Factory o nome dela. O Factory, então, recupera o ponteiro para a função associada e chama essa função para criar a instância correta. Para isso funcionar de acordo, as classes disponíveis devem ser adicionadas à lista. No caso do Assistente citado, as próprias classes de cada tipo de Passo se registram na lista, de modo que apenas aqueles passos necessários em cada programa são de fato incluidos.

O primeiro passo para implementar essa solução foi declarar um tipo "ponteiro para função" com os parâmetros necessários para se criar qualquer Passo para o Assistente:
{ AObj são dados que o construtor do passo pode usar para gerar a instância, podendo ser um nó XML ou outro dado relevante. }
TWPassoCreationFnc = function (AWizMan: TWWizardManager; AObj: TObject): TWPasso;

O primeiro parâmetro representa o gerenciador do Assistente onde o Passo deve ser criado, enquanto o segundo parâmetro é genérico, podendo conter qualquer informação relevante para o construtor da classe.

Em seguida, montei uma função para incluir na lista de tipos de passos disponíveis uma associação entre o nome de uma classe e a função que a constrói. Uma outra função permite construir um passo a partir do nome de sua classe. Ambas estão listadas abaixo:
procedure RegisterPassoInFactory (AClassName: String; AConstructor: TWPassoCreationFnc);
var idx : integer;
begin
idx := _FactoryPassos.Add (AClassName);
_FactoryPassos.Objects [idx] := @AConstructor;
end;

function CreatePassoFromFactory (AWizMan: TWWizardManager; AClassName: String; AObj: TObject) : TWPasso;
var lConstructor: TWPassoCreationFnc;
idx : Integer;
begin
idx := _FactoryPassos.IndexOf (AClassName);
if (idx >= 0) then
begin
@lConstructor := _FactoryPassos.Objects[idx];
Result := lConstructor (AWizMan, AObj);
end
else
Raise Exception.Create('Classe desconhecida: ' + AClassName);
end;

Observe que nos pontos onde o ponteiro para função é parte de uma atribuição eu utilizo um arroba (@) para me referir a ele. A sintaxe @lConstructor evita que a função (ou procedimento) seja chamada, indicando que eu estou apenas recuperando o endereço onde ela está na memória. Depois que o ponteiro foi recuperado, uso-o para fazer uma chamada à função que ele representa, passando-lhe os parâmetros conforme declarado no início. Para completar esta etapa, declarei a lista global na área implementation da minha unit e usei os blocos initialization e finalization dela para, respectivamente, criar e destruir tal lista.
implementation

var

_FactoryPassos : TStringList;

{ ... }
initialization
_FactoryPassos := TStringList.Create;

finalization
FreeAndNil (_FactoryPassos);
end.

Com isso, o cenário está preparado. Agora resta fazer com que cada classe que eu queira construir dessa maneira seja apropriadamente registrada na lista. Veja um exemplo para um passo cuja classe foi denominada TWPassoOpcao:
implementation

function CreateWPassoOpcao (AWizMan: TWWizardManager; AObj: TObject): TWPasso;
begin
Result := TWPassoOpcao.Create (AWizMan);
end;

{ ... }
initialization
RegisterPassoInFactory ('TWPassoOpcao', CreateWPassoOpcao);

end.

O mesmo terá que ser feito para as outras classes que denotam passos. Neste exemplo, eu omiti o uso do parâmetro AObj mas, no contexto onde isso é aplicado aqui na ABC71, esse parâmetro é um nó de uma estrutura XML a partir da qual os passos são criados. No entanto, como ele é um TObject, pode representar qualquer outra informação que seja necessária para criar com sucesso a instância da classe.

Esta implementação permite, agora, que cada programa assistente seja compilado apenas com os passos que ele realmente vai precisar, bastando fazer chamadas à função CreatePassoFromFactory para criá-los.

2 comentários :

Anônimo disse...

Olá Luiz!

Excelente artigo, só estou com uma dúvida.
Este modo de implementação não aumenta o consumo de memória haja visto que todas as sub-classes são instanciadas quando se chama o método RegisterPassoInFactory?

Luís Gustavo Fabbro disse...

Na forma como está implementado, RegisterPassoInFactory apenas associa o nome da classe de um tipo de passo com a função que efetivamente faz a criação. Ou seja, a instância de um passo só será criada quando o programa chamar a função CreatePassoFromFactory, o que pode acontecer quantas vezes forem necessárias.

Por exemplo, suponha que há um tipo de passo capaz de registrar uma opção de sistema no banco de dados. Se o sistema tiver 10 opções diferentes, terei que criar 10 instâncias desse tipo de passo, chamando o CreatePassoFromFactory para cada uma. Em contrapartida, seu eu não tiver que registrar opções, nenhuma instância precisará ser criada, embora a função capaz de criá-la esteja na lista
interna.

Em resumo, o consumo de memória para cada passo possível nesse controle fica restrito aos 4 bytes do endereço do construtor mais o tamanho do nome da classe, o que é sistemicamente irrisório.

[]s

Postar um comentário

OBS: Os comentários enviados a este Blog são submetidos a moderação. Por isso, eles serão publicados somente após aprovação.

Observação: somente um membro deste blog pode postar um comentário.