29 de julho de 2009

Design Patterns com Delphi : Abstract Factory

Imagine uma situação em que, dependendo de certas condições do ambiente operacional em que seu programa irá executar, um conjunto diferente de classes tenha que estar disponível para criação. Um exemplo típico disso é a criação de skins para permitir variações no visual do programa de acordo com opção selecionada pelo usuário. Nesse cenário, o conjunto de classes que representam componentes visuais têm que ser diferente para atender o visual selecionado.

Para resolver esse tipo de problema, foi desenvolvido uma variação do Design Pattern Factory Method, descrito no post anterior. Esse novo Pattern Criacional foi chamado Abstract Factory e a ideia dele é criar uma interface padrão (uma função) para decidir qual o Factory Method a ser utilizado. Na prática, equivale a implementar um Factory Method dentro de outro. Veja o esquema que representa este cenário:

Esquema para Abstract Factory
Neste exemplo, o usuário poderia optar por um de dois visuais disponíveis para a aplicação (Windows ou Modern). Internamente, a aplicação possui classes bases para desenhar os EditText e os ComboBox além de disponibilizar dois conjuntos de heranças dessas classes, um para cada "skin" possível. A função GetSkinFactory da classe TWSkinFactory é a responsável por instanciar a "fábrica" correta de acordo com o tipo de skin desejado:
type
TWTipoSkin = (tsWindows, tsModern);
TWSkinFactory = class
public
class function GetSkinFactory (ATipo: TWTipoSkin): TWSkinFactory;
function CreateEditText: TWEditText;virtual;abstract;
function CreateComboBox: TWComboBox;virtual;abstract;
end;
TWWindowsSkinFactory = class(TWSkinFactory)
public
function CreateEditText: TWEditText;override;
function CreateComboBox: TWComboBox;override;
end;
TWModernSkinFactory = class(TWSkinFactory)
public
function CreateEditText: TWEditText;override;
function CreateComboBox: TWComboBox;override;
end;

implementation

class function TWSkinFactory.GetSkinFactory (ATipo: TWTipoSkin): TWSkinFactory;
begin
Result := Nil;
case ATipo Of
tsWindows: Result := TWWindowsSkinFactory.Create;
tsModern: Result := TWModernSkinFactory.Create;
end;
end;

Veja que a função GetSkinFactory é uma função de classe (não precisa de uma instância para ser usada) enquanto as funções de criação dos componentes visuais são abstratas já que o objetivo dessa classe é apenas introduzir o comportamento esperado para as heranças. Neste exemplo, o comportamento desejado é criar instâncias para os componentes visuais:
function TWWindowsSkinFactory.CreateEditText: TWEditText;
begin
Result := TWEditWindows.Create;
end;
function TWModernSkinFactory.CreateEditText: TWEditText;
begin
Result := TWEditModern.Create;
end;

Cada classe de "fábrica" cria instâncias dos componentes apropriados para o tipo de "skin" selecionado e o polimorfismo faz o resto, deixando o código do programa mais limpo e mais fácil de manter. Supondo que haja um ComboBox para escolha do visual:
var factory : TWSkinFactory;
ed1 : TWEditText;
begin
factory := TWSkinFactory.GetSkinFactory (TWTipoSkin (ComboBox1.ItemIndex));
{...}
ed1 := factory.CreateEditText;
ed1.Desenha;
{...}

A variável factory é instanciada com base na opção do usuário e o resto do programa a utiliza para criar a interface visual. Isto é, o programa não sabe de antemão com qual conjunto de classes visuais está trabalhando. Da mesma forma que no Factory Method, usar o Abstract Factory deixará um único ponto do código para ser alterado caso novas heranças de "skin" sejam necessárias.

As situações onde esses Patterns podem ser usados também são parecidas: é preciso ter conjuntos de classes com relação de herança e o programa tem que ter conhecimento prévio das classes disponíveis pois elas têm que estar presentes na função Factory.

4 comentários :

Luiz Carlos Alves disse...

Primeiramente, gostaria de parabenizá-lo por esta iniciativa. Você tem ajudado a muitos desenvolvedores, entres os quais eu me incluo.

Sobre abstract factory, vamos ver se eu consegui compreender o método:

Algum tempo atrás eu criei um componente para geração de boletos e arquivos de remessa. A princípio, eu havia criado uma classe para titulos (sendo que uma de suas propriedades é o número do banco, ex: 001, 104, 237, etc.), e para cada banco criei uma classe especifica. Através do persistentclass eu escolhia a classe específica de cada banco. Funcionou até bem.

Comecei a estudar os padrões de projeto e vi que era possível melhorar a solução que adotei para o módulo de boleto. Achei que o abstract método funcionaria para o meu caso, então busquei na internet artigos que utilizassem o método no delphi (visto que a maioria dos exemplos era feito em java). Com isso, cheguei até seu blog.

Li atentamente o texto, e percebi que o factory method se encaixaria melhor do que o factory então, fiz então da seguinte forma:

- Criei uma classe TBanco (com os dados nossonumero, linhadigitavel, codigobarras, formato da conta corrente, etc);

- Criei para cada banco uma classe herdada de TBanco (e com construtor que formata os campos da super classe de acordo com as regras de cada banco);

- Criei uma classe chamada TFactoryBanco, com um método chamado getBanco(ABanco: string): TBanco;

Na chamada de cada título, para colher o formato do banco selecionado, é feito:
loBanco := TFactoryBanco.getBanco(titulo.NumBanco);

E com isso posso criar boleto parar qualquer banco que eu precise implantar no software.

Me diga se a escolha pelo factory method foi correta?

Abraços

Luiz Carlos
luiz_sistemas@hotmail.com

Luís Gustavo Fabbro disse...

Luiz Carlos

O cenário que você descreveu se adapta perfeitamente ao uso do padrão Factory Method.

No geral, não há "certo" ou "errado" na seleção de um padrão - apenas o "adequado" para o cenário atual e sua previsão de evolução. Mas é comum não termos controle sobre como um sistema evoluirá - isto depende de demandas dos Cliente, fatores de mercado, novas tecnologias que surjam e outras variáveis. Assim, não é raro termos que reavaliar um padrão escolhido, readaptando de acordo com o desenrolar da evolução do sistema.

Luiz Carlos Alves disse...

Realmente, foi o que aconteceu com a minha primeira solução. Como eu tinha cliente que utilizava apenas o Banco do Brasil e Bradesco, não havia sentido a necessidade de melhorar o código. Mas com o surgimento de novos clientes, e estes com bancos ainda não implementados, comecei a ter problemas. Com o factory method conseguirei resolver.

Continuo buscando melhorias. O desafio agora é melhorar a forma como faço a persistência dos dados. Atualmente, crio minhas classes (produtos, clientes, fornecedores, etc.) separadas do código sql necessário à manipulação dos dados. Para isso, crio classes chamadas Dao, exemplo: ClienteDao, FornecedorDao... estas com todos os comandos CRUD. Ok, funciona!

Porém, percebo que ainda estou longe de abstrair meu software do banco de dados, ou seja, eu queria ter o mesmo poder que o java oferece através das ferramentas "hibernate + JPA", onde é possível escrever todo um sistema sem mexer numa linha sequer de SQL. Com isso, é possível mudar de banco de dados mudando apenas uma string. Você já passou por esta situação?

Luiz Carlos
luiz_sistemas@hotmail.com

Luís Gustavo Fabbro disse...

Luiz Carlos

Na ABC71, empresa onde trabalho, nós temos uma infraestrutura de acesso a dados baseada no ADO sobre a qual representamos cada tabela do sistema. Por herança, cada tabela passa suas características (campos, chaves) para a infraestrutura que então consegue montar os comandos básicos CRUD automaticamente, respeitando as peculiaridades de sintaxe do banco de dados em uso.

Nossas classes de regras de negócio lidam com a persistência de dados criando instâncias dessas classes que representam tabelas, mantendo separados regras de negócio e persistência. Isso permite que nosso sistema hoje esteja homologado para executar em SQL Server, Oracle, Sybase e Postgre.

Você pode se basear nessa ideia e construir algo semelhante, mais apropriado para seu sistema.

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.