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: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;
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;
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;
{...}
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 :
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
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.
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
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.