Mostrando postagens com marcador Design Patterns. Mostrar todas as postagens
Mostrando postagens com marcador Design Patterns. Mostrar todas as postagens

13 de março de 2012

Design Patterns com Delphi: Visitor - Parte II

No último post, eu apresentei o conceito do Design Pattern Visitor usando para isso um diagrama UML com um exemplo prático da aplicabilidade do padrão. O exemplo consiste na representação de um Produto Acabado composto por uma lista de recursos (as matérias primas, máquinas e instruções) usados para fabricá-lo. O objetivo é permitir aplicar ao produto operações externas cujo resultado depende da aplicação da operação em cada parte que compõe a estrutura. No exemplo, há duas operações: uma que exporta a arquitetura do produto em formato XML e outra que faz a impressão dessa estrutura.

Neste post, mostro uma sugestão de implementação em Delphi para o exemplo do Visitor. Para facilitar a referência, publico novamente o diagrama que o retrata:
Diagrama UML para o padrão Visitor
O primeiro passo é definir as classes que representam o propósito do sistema, estabelecendo as regras de negócio que constituirão o cerne dele. No nosso caso, essas classes são o recurso de produção, suas heranças e o produto acabado. O quadro abaixo mostra as declarações delas, que são bastante simples :
{ Recursos para a produção }
TWRecursoProducao=class
public
_Nome: String;

Constructor Create (ANome: String);virtual;
procedure Accept (AOper: TWOperacoesPA);virtual;
end;

TWMateriaPrima=class(TWRecursoProducao)
public
_CProd: String;
_Qtde: Double;

Constructor Create (ANome: String);override;
procedure Accept (AOper: TWOperacoesPA);override;
end;

TWMaquina=class(TWRecursoProducao)
public
_Cod: String;
_Tempo: Double;
_TempoSetup: Double;

Constructor Create (ANome: String);override;
procedure Accept (AOper: TWOperacoesPA);override;
end;

TWRoteiro=class(TWRecursoProducao)
public
_Cod: String;
_Texto: String;

Constructor Create (ANome: String);override;
procedure Accept (AOper: TWOperacoesPA);override;
end;

{ Produto Acabado }
TWProdutoAcabado=class
protected
_MatPrimas : TObjectList;
_Maquinas : TObjectList;
_Roteiro : TWRoteiro;
procedure ClearListaRec (ALista: TObjectList);

public
_Nome: String;

Constructor Create (ANome: String);
Destructor Destroy; override;

procedure ClearRecursos;
procedure AddMatPrima (ARecurso: TWRecursoProducao);
procedure AddMaquina (ARecurso: TWRecursoProducao);
procedure SetRoteiro (ARoteiro: TWRoteiro);

procedure DoOperacao (AOper: TWOperacoesPA);
end;
Veja que a classe que representa o produto acabado (TWProdutoAcabado) possui membros do tipo TObjectList e também um roteiro separado. Esses membros armazenarão os recursos necessários para fabricar o produto acabado. Neste exemplo, as classes que compõem a agregação são heranças de uma mesma classe base - a TWRecursoProducao - mas o padrão Visitor não faz essa exigência. Assim, ele ainda é aplicável mesmo quando a agregação é composta por classes que não guardam relação entre si. Então, usei variáveis separadas pra cada tipo de recurso apenas para deixar clara essa possibilidade.

Repare ainda na função Accept introduzida na classe base de recursos. É ela quem define a família de classes como Visitable, determinando que os recursos de produção que elas representam poderão ser visitados por qualquer operação externa herdada de TWOperacoesPA. Na verdade, Accept simplesmente solicitará à operação que "visite" o recurso em questão, o que fará com que a operação seja aplicada ao recurso :
procedure TWMateriaPrima.Accept (AOper: TWOperacoesPA);
begin;
inherited;
AOper.Visit (Self);
end;

procedure TWMaquina.Accept (AOper: TWOperacoesPA);
begin;
inherited;
AOper.Visit (Self);
end;

procedure TWRoteiro.Accept (AOper: TWOperacoesPA);
begin;
inherited;
AOper.Visit (Self);
end;
A seguir, podemos definir a hierarquia de classes que implementarão as operações externas nos recursos de produção. Tais classes exercerão o papel de Visitors:
TWOperacoesPA=class
public
procedure InitOper(AInfo: TObject);virtual;
procedure TermOper;virtual;

procedure Visit (pRecurso: TWMateriaPrima);overload;virtual;
procedure Visit (pRecurso: TWMaquina);overload;virtual;
procedure Visit (pRecurso: TWRoteiro);overload;virtual;
end;

TWRecursoSaveToXml=class(TWOperacoesPA)
protected
_Xml : String;
public
procedure InitOper(AInfo: TObject);override;
procedure TermOper;override;

procedure Visit (pRecurso: TWMateriaPrima);overload;override;
procedure Visit (pRecurso: TWMaquina);overload;override;
procedure Visit (pRecurso: TWRoteiro);overload;override;

function GetXML : String;
end;
A operação para imprimir o produto acabado é bastante similar à de salva para XML e, por isso, eu a omiti do quadro.

Veja que a classe TWOperacoesPA, que é base para todas as operações, possui 3 métodos Visit sobrecarregados (overload). Cada um deles trata um tipo diferente de recurso, o que, na prática, nos permite adequar o comportamento da operação. Isso quer dizer que a operação será realizada de um modo coerente com o tipo do recurso.

Um outro detalhe nessa classe é a função InitOper. Ela é desenhada para realizar procedimentos iniciais da operação, aceitando um parâmetro genérico do tipo TObject com informações que façam sentido para a operação. Por exemplo, pode representar uma impressora para a operação de Imprimir ou um objeto DOM para a exportação em formato XML.

O trecho de código abaixo mostra as funções de inicialização e encerramento da operação de exportação para XML. Também retrata a versão da função Visit sobrecarregada para os tipos de recurso "Matéria Prima" e "Máquina":
procedure TWRecursoSaveToXml.InitOper(AInfo: TObject);
var lProd: TWProdutoAcabado;
begin
inherited;

{Abre a tag raiz para o XML }
lProd := AInfo As TWProdutoAcabado;
_XML := '<?xml version="1.0" encoding="ISO-8859-1"?>' + #13#10;
_XML := _XML + '<produtoAcabado name="' + lProd._Nome + '">' + #13#10;
end;

procedure TWRecursoSaveToXml.TermOper;
begin
{ Fecha a tag XML raiz }
_XML := _XML + '</produtoAcabado>';
inherited;
end;

procedure TWRecursoSaveToXml.Visit (pRecurso: TWMateriaPrima);
begin
inherited;

{ Versão específica dessa operação p/ o recurso "Matéria Prima" }
_XML := _XML + ' <materiaPrima>' + #13#10;
_XML := _XML + ' <codigo>' + pRecurso._CProd + '</codigo>' + #13#10;
_XML := _XML + ' <nome>' + pRecurso._Nome + '</nome>' + #13#10;
_XML := _XML + ' <qtde>' + FormatFloat ('#,##0.00', pRecurso._Qtde) + '</qtde>' + #13#10;
_XML := _XML + ' </materiaPrima>' + #13#10;
end;

procedure TWRecursoSaveToXml.Visit (pRecurso: TWMaquina);
begin
inherited;
{ Versão específica dessa operação p/ o recurso "Máquina" }
_XML := _XML + ' <maquina>' + #13#10;
_XML := _XML + ' <codigo>' + pRecurso._Cod + '</codigo>' + #13#10;
_XML := _XML + ' <nome>' + pRecurso._Nome + '</nome>' + #13#10;
_XML := _XML + ' <setup>' + FormatFloat ('#,##0.00', pRecurso._TempoSetup) + '</setup>' + #13#10;
_XML := _XML + ' <tempo>' + FormatFloat ('#,##0.00', pRecurso._Tempo) + '</tempo>' + #13#10;
_XML := _XML + ' </maquina>' + #13#10;
end;
Por fim, podemos implementar a função que aplica a operação ao produto acabado. Por definição, isso implica aplicar a mesma operação sobre os recursos que compõe esse produto acabado. Por isso, teremos que passar por todos os recursos, chamando a função Accept para determinar como a operação deve ser executada em cada um deles:
procedure TWProdutoAcabado.DoOperacao (AOper: TWOperacoesPA);
var i : integer;
lMatPrima: TWMateriaPrima;
lMaquina : TWMaquina;
begin
AOper.InitOper (Self);

{ Aplica a operação a cada matéria prima da lista }
for i := 0 to _MatPrimas.Count - 1 do
begin
lMatPrima := _MatPrimas.Items[i] As TWMateriaPrima;
lMatPrima.Accept (AOper);
end;

{ Aplica a operação a cada máquina da lista }
for i := 0 to _Maquinas.Count - 1 do
begin
lMaquina := _Maquinas.Items[i] As TWMaquina;
lMaquina.Accept (AOper);
end;

{ Aplica a operação no roteiro, se houver um }
if (_Roteiro <> Nil) then
_Roteiro.Accept (AOper);

AOper.TermOper;
end;

{ ... }

var lOperacao : TWRecursoSaveToXml;
lProd : TWProdutoAcabado;
lXml : String;
begin
lProd := TWProdutoAcabado.Create ('Computador');

lOperacao := TWRecursoSaveToXml.Create;

lProd.DoOperacao(lOperacao);

{ Publica o resultado da operação }
lXml := lOperacao.GetXML ();
{ ... }
end;

A parte inferior do quadro demonstra como se dá a aplicação de uma operação.

O projeto com esse exemplo pode ser salvo a partir desse link. Ele foi criado em Delphi 2005 mas deve ser possível compilá-lo em outras versões do IDE sem problemas.

27 de fevereiro de 2012

Design Patterns com Delphi: Visitor - Parte I

Coleções de objetos são uma das estruturas mais comumente usadas na construção de programas de computador. Elas podem aparecer na forma de listas, vetores ou em agregações e composições, onde diferentes tipos de objeto são mantidos juntos para representar um objeto mais complexo.

Um dos propósitos que nos leva a agruparmos objetos numa mesma estrutura é facilitar a execução de operações envolvendo as partes que constituem esse objeto. Ou seja, operações nas quais percorreremos todos os elementos e aplicaremos a cada um deles a operação desejada. Nesse processo, a existência de objetos de tipos distintos deve ser levada em conta porque uma mesma operação pode ter significado diferente, dependente do tipo de objeto. Isto nos leva a ter que escolher a versão correta da operação que deve ser executada em cada elemento. Por exemplo, considere que você tem uma classe representando a lista de recursos necessários para fabricar um produto. A lista inclui as matérias primas, as máquinas envolvidas e até roteiros descrevendo o processo de fabricação. Uma operação que imprima a estrutura terá que considerar as diferenças entre cada uma dessas partes pois elas têm informações específicas distintas.

Em princípio, parece que uma herança simples pode resolver a questão. Mas, e se for necessário acrescentar novos tipos de operação, com nomes e parâmetros diferentes - salvar em formato XML, por exemplo ? Teríamos que alterar a estrutura da agregação para que ela comporte a nova operação.

O intuito do Design Pattern comportamental Visitor é facilitar a manutenção desse tipo de estrutura heterogênea. Ele nos obriga a manter separada a estrutura de um objeto e os algoritmos das operações que podem ser aplicadas sobre seus componentes. Com isso, podemos introduzir novas operações à estrutura sem que seja preciso modificar as classes onde essas operações serão aplicadas. Em outras palavras, o que o Visitor propõe é transferir a implementação das operações, removendo-as das classes que compõem a agregação e passando-as para classes próprias. Com isso, preserva-se a estrutura original tanto da agregação quanto das partes que a compõem.

O diagrama abaixo mostra um exemplo prático da aplicabilidade desse padrão. Nele, um Produto Acabado é composto de uma lista de recursos, incluindo as matérias primas, máquinas e instruções para a fabricação dele. São definidas duas operações externas : uma para exportar a arquitetura do produto para XML e outra para imprimí-la.
Diagrama UML para o padrão Visitor
De acordo com o papel que exercem na implementação de uma solução para o padrão Visitor, as classes envolvidas são formalmente conhecidas pelos nomes que seguem:
O Visitor é uma abstração que define quais classes de objetos poderão ser submetidas às operações que serão disponibilizadas. Ele faz isso ao introduzir métodos sobrecarregados, um para cada tipo de elemento da agregação que sofrerá a operação. Por convenção, este método é nomeado Visit. No diagrama, este é o papel da classe TWOperacoesPA.

A implementação das operações em si é feita em classes chamadas Concrete Visitor. Essas classes são heranças simples do Visitor, o que significa que elas terão que fornecer sua própria versão de cada método Visit definido na classe base. Com isso, a operação tratará de forma condizente os diferentes componentes da coleção. No exemplo, as classes TWRecursoImprimir e TWRecursoSaveToXml fazem esse papel.

Chamamos de Visitable (ou Element) a classe que define um método Accept capaz de receber uma instância da classe de operação e executar a versão correta do método Visit dessa operação. Ela, portanto, serve de ponto de entrada da execução, definindo o comportamento esperado para todas as heranças que poderão ser "visitadas". São às vezes chamadas de Element por coincidir com a classe base dos elementos que compõem uma agregação. A classe que exerce esse papel no diagrama de exemplo é a TWRecursoProducao.

Os Concrete Visitable (ou Concrete Element) são as classes que efetivamente implementam o método Accept, direcionando a execução da operação para o tipo de objeto que representam. Isto é, chamando o método Visit correto da operação passada como parâmetro. As classes TWMateriaPrima, TWMaquina e TWRoteiro são exemplos disso no diagrama.

O Object Structure é a classe que contém a coleção de elementos sobre os quais uma operação será executada. Ela, então, terá que prover um meio para que a coleção seja percorrida de modo que todos os seus componentes possam ser submetidos à operação desejada. Este é o papel da classe TWProdutoAcabado no diagrama.

O Client é responsável por manter a instância tanto do Object Structure quanto da operação que será executada. Ela também navega os elementos da estrutura, executando em cada um a operação solicitada.

O padrão Iterator também é projetado para permitir a navegação entre elementos que compõem um objeto. A diferença é que no Iterator os elementos devem ser herança de uma mesma classe enquanto no Visitor isso não é obrigatório.

O cenário onde o padrão Visitor é aplicável se assemelha ao uso de interfaces, já que em ambos os casos, uma estrutura externa estabelece as operações que a classe é capaz de executar. No entanto, interfaces forçam a implementação das operações dentro da classe ao invés de fazê-lo em classes separadas, como no Visitor. Obviamente, ambos os recursos podem ser utilizados em conjunto para produzir uma solução mais flexível.

Alguns críticos alegam que o Visitor fere o encapsulamento das classes ao obrigar que parte de seu funcionamento seja delegada a outra classe. Mas, podemos encarar essa classe extra como uma operação distinta, minimizando ou até mesmo eliminando o efeito da quebra de encapsulamento.

No próximo post eu mostro uma sugestão de implementação desse pattern usando o Delphi.

Mais Informações
Posts sobre Design Patterns

21 de dezembro de 2011

Design Patterns com Delphi: Template - Parte II

No meu último post, introduzi o conceito do Design Pattern comportamental Template. Em resumo, esse padrão estabelece uma sequência de passos para a execução de uma determinada tarefa, permitindo que sejam criadas heranças que implementem suas próprias versões de um ou mais dos passos, de acordo com a necessidade.

Neste post, eu sugiro uma implementação em Delphi do exemplo envolvendo criação de infraestruturas que usei para apresentar o Template no outro post. Para facilitar o entendimento, reproduzo novamente abaixo o diagrama UML que modela a infraestrutura da transações de negócio usada como exemplo:
Diagrama UML para o padrão Template
Como dá para intuir a partir da simplicidade do diagrama, a codificação também não é complexa. A primeira providência é criar a classe que define o formato geral do algorítimo, os passos que o comporão, em qual ordem esses passos devem ser chamados para completar a tarefa e quais deles poderão ser particularizados em heranças do algorítimo. Esta classe - chamada de Abstract Class - servirá, portanto, de base para as demais implementações do mesmo algorítimo. No nosso exemplo, é a classe TWTransacao que tem essa responsabilidade:
TWTransacao=class
protected
_Erros: TWErros; { Lista de erros da transação }

procedure BeginTrans;
procedure CommitTrans;
procedure RollbackTrans;

function ConsisteDados : boolean;virtual;
function GravaDados : integer;virtual;

public
function ExecutarTransacao : integer;
end;

{ ... }

function TWTransacao.ConsisteDados : boolean;
begin
Result := true;
end;

function TWTransacao.GravaDados : integer;
begin
Result := 0;
end;

function TWTransacao.ExecutarTransacao : integer;
begin
Result := -99;

Try
_Erros.Clear;

{ Inicia uma transação com o banco de dados }
BeginTrans;

{ Se estiver tudo OK com os dados, gravá-los no banco }
if (ConsisteDados) then
Result := GravaDados;

{ Efetiva a transação }
CommitTrans;
Except
{ Se houve algum erro, aborta a transação com o banco de dados}
RollbackTrans;
Result := -100;
end;
end;

Observe que há apenas uma função pública : ExecutarTransacao. As partes do programa que desejarem executar uma transação de negócio precisam ter acesso apenas a ela, não importando quais ou quantos são os passos necessários para executá-la.

Em contraste, tanto os passos do algorítimo que poderão ser estendidos pelas heranças quanto os métodos internos auxiliares foram colocados na área protegida, escondendo os detalhes para códigos externos. Aqui, as funções ConsisteDados e GravaDados poderiam ter sido declaradas como abstratas, o que forçaria as heranças a fornecer-lhes uma implementação. No entanto, não é mandatório que elas tenham essa característica, de modo que o exemplo ilustra como a classe base do Template pode providenciar uma versão padrão dos passos do algorítimo, mesmo que sejam versões bem simples. Elas poderiam, por exemplo, registrar um log da execução.

As funções relativas a transações com o banco de dados (BeginTrans, CommitTrans e RollbackTrans) apenas repassam suas respectivas chamadas diretamente ao banco; por isso, suas implementações foram omitidas no quadro acima.

Já temos a classe base, podemos então codificar as heranças previstas para transações de produtos, pedidos de venda e notas fiscais:
TWTrnProduto = class(TWTransacao)
private
_prod : TWProduto;
protected
function ConsisteDados : boolean;override;
function GravaDados : integer;override;
{ ... }
end;

TWTrnPedidoVenda = class(TWTransacao)
private
_Pedido : TWPedidoVenda;
protected
function ConsisteDados : boolean;override;
function GravaDados : integer;override;
{ ... }
end;

TWTrnNotaFiscal = class(TWTransacao)
private
_Nota : TWNotaFiscal;
protected
function ConsisteDados : boolean;override;
function GravaDados : integer;override;
{ ... }
end;

{ ... }

function TWTrnProduto.ConsisteDados : boolean;
begin
Result := inherited ConsisteDados;

if (TWProduto.ExisteProd (_Prod) ) then
begin
Result := false;
_Erros.Add ('Produto já existe');
end;
end;

function TWTrnProduto.GravaDados : integer;
begin
inherited GravaDados;

_Produto.Insert();
end;

function TWTrnPedidoVenda.ConsisteDados : boolean;
begin
Result := inherited ConsisteDados;

if (TWPedidoVenda.ExistePedido (_Pedido) ) then
begin
Result := false;
_Erros.Add ('Já existe pedido com esse número.');
end;

if (not _Pedido.VerifSaldoProdutos) then
Result := false;

if (not _Pedido.VerifDadosComerciais) then
Result := false;

{ Se detectou algum erro, reporta para a transação }
_Erros.Add(_Pedido.Erros);
end;

function TWTrnPedidoVenda.GravaDados : integer;
begin
inherited GravaDados;

{ Insere o pedido, atualiza saldo dos produtos e faz outros ajustes }
_Pedido.Insert ();
end;
Vemos no quadro anterior que as transações reais são heranças da classe de transação básica e que elas fornecem suas próprias versões de passos do algorítimo base. Com isso, elas garantem que suas próprias regras de negócio sejam respeitadas, enquanto são mantidas a ordem de chamada dos passos e outras regras estipuladas pela classe TWTransacao. A transação de nota fiscal foi omitida no quadro mas a ideia dela é bastante similar à de pedidos de venda.

Para completar o exemplo, falta a classe Client, isto é, aquela parte do código que utilizará o nosso template.
TWTela = class
protected
_Trn : TWTransacao;

public
{ ... }
procedure ExecutarTransacao(); end;

{ ... }

procedure TWTela.ExecutarTransacao();
begin
_Trn := ObtemTransacao ();
_Trn.ExecutarTransacao;
end;
Como mostra o quadro, o código da classe TWTela acaba ficando extremamente simples. Aqui, o maior problema talvez seja decidir o melhor meio de obter uma instância da transação correta. Dependendo das circunstâncias, podemos optar pelo método Factory ou organizar as telas usando o padrão Mediator - o que permitiria cada tela instanciar diretamente a transação adequada.

13 de dezembro de 2011

Design Patterns com Delphi: Template - Parte I

Criar uma infraestrutura de software é padronizar a forma como as coisas acontececerão dentro de um programa, estabelecendo regras e ditando as mensagens que cada parte do código está preparada para receber. Por exemplo, uma infraestrutura para realizar transações de negócio dentro do sistema pode ser descrita simplificadamente em poucos passos : abertura de transação com o banco de dados; consistência das informações contidas no(s) objeto(s) de negócio envolvido(s); persistência das informações nas respectivas tabelas; encerramento da transação. Todo objeto com capacidade de realizar uma transação terá necessariamente que implementar esses passos.

Se cada Caso de Uso implementar uma versão independente dos passos, mudanças nessa sequência teriam que ser replicadas em todas as implementações. O alvo do Design Pattern comportamental Template é justamente situações como esta, onde um algorítimo é definido de modo centralizado, moldando a forma de trabalho a ser seguida por todas as classes de objeto que desejarem implementá-lo. Um ou mais passos podem ser sobrepostos pelas heranças, permitindo que elas implementem comportamentos diferenciados quando for necessário.

Essa idéia é similar à do Pattern Strategy. A diferença mais importante entre ambos é que o Template permite que heranças alterem partes de um algorítimo enquanto no Strategy temos que optar por uma das versões de um algorítimo bem conhecido e executar todo o trabalho com esse algorítimo.

O quadro a seguir traz um diagrama UML mostrando as relações esperadas entre as classes participantes da solução para o Template. O cenário abordado é o da transação de negócio citado no primeiro parágrafo deste post:
Diagrama UML para o padrão Template
Neste exemplo, nem todos os métodos são virtuais. São declarados como virtuais apenas aqueles que poderão (ou deverão) ter uma implementação distinta nas classes herdadas. Como podemos ver, o algorítimo é decomposto em métodos mais granulares que podem ser sobrepostos quando ncessários. Neste contexto, tais métodos são chamados de operações primitivas.

No exemplo, o método ExecutarTransacao é responsável por chamar cada operação primitiva no tempo correto, moldando efetivamente o algorítimo que deve ser executado por cada classe da hierarquia. Este método é chamado de Template (modelo).

Formalmente, as classes participantes de uma solução para o pattern Template recebem os seguintes nomes:
É denominada Abstract Class a classe base que define as operações primitivas que estarão disponíveis para o algorítimo. Ela também define o esqueleto do algorítimo ao implementar o método Template, chamando cada operação primitiva na ordem correta. No diagrama acima, este é o papel da classe TWTransacao. É importante ressaltar que, apesar do nome, essa classe não precisa ser obrigatoriamente abstrata; se for adequado, a Abstract Class pode prover versões básicas de todas as operações primitivas.

As classes que provêm versões particulares de uma ou mais das operações primitivas são chamadas Concrete Class. Isso permite-lhes realizar ações específicas dentro dos passos básicos que formam o algorítimo determinado pela classe base. No exemplo, as classes TWTrnProduto, TWTrnNotaFiscal e TWTrnPedidoVenda são desse tipo. Elas fornecem suas próprias versões dos métodos que consistem e gravam os dados relativos, respectivamente, às transações para inserir um novo produto, criar uma nota fiscal e criar um pedido de venda.

O Client é qualquer parte do sistema que esteja apta a disparar o algorítimo definido na classe base. A classe TWTela do diagrama de exemplo detem esse papel.
No próximo post, eu apresento uma sugestão de como implementar o exemplo proposto usando Delphi.

Mais Informações
Posts sobre Design Patterns

6 de outubro de 2011

Design Patterns com Delphi: Strategy - Parte II

No último post, eu apresentei o conceito e a aplicabilidade do Design Pattern comportamental Strategy. Aqui, vou mostrar como implementar em Delphi o exemplo prático utilizado naquele post, relacionado a pagamento de títulos. Para facilitar o entendimento, reproduzo abaixo o diagrama UML com a sugestão de modelagem proposta:
Diagrama UML para o padrão Strategy
A implementação das relações expostas no diagrama não é especialmente complexa. Ela envolve apenas conceitos comuns da programação orientada a objetos, como herança e polimorfismo.

O primeiro passo é declarar uma classe abstrata (ou interface, se preferir), definindo nela as linhas mestras que deverão ser respeitadas por todos as implementações reais do algoritmo em questão. Isto é, essa classe introduz funções que determinam a estratégia para execução da tarefa, impondo-a a todas as classes que queiram atuar como um algoritmo alternativo para a tarefa - daí o nome do padrão ser Strategy:
TWTitulo = class;

TWPagamento=class
public
function efetuaPgto (ATitulo: TWTitulo) : integer;virtual;abstract;
end;
Nesse exemplo, há uma única função abstrata que obrigatoriamente terá que existir em cada nova implementação do algoritmo. Essa função aceita como parâmetro uma instância da classe Context da solução, papel encarnado aqui pelo título a pagar (TWTitulo). Com isso, qualquer algoritmo que adote a estratégia definida tem acesso às informações relevantes do título, bem como a suas operações. Permitir essa comunicação é imprescindível para que a tarefa (o pagamento do título) possa ser concretizada.

Agora que temos a interface estabelecida, podemos adicionar os diferentes algoritmos previstos no diagrama, criando cada um deles como uma herança simples que forneça código para a função abstrata da interface:
TWContaBancaria = class;

TWPgtoBoleto=class(TWPagamento)
{ ... }
public
function efetuaPgto (ATitulo: TWTitulo) : integer;override;
end;

TWPgtoDebitoAuto=class(TWPagamento)
{ ... }
_Conta : TWContaBancaria;
public
constructor Create (AConta: TWContaBancaria);
function efetuaPgto (ATitulo: TWTitulo) : integer;override;
end;

TWPgtoInternet=class(TWPagamento)
{ ... }
public
function efetuaPgto (ATitulo: TWTitulo) : integer;override;
end;

implementation

{ ... }
constructor TWPgtoDebitoAuto.Create (AConta: TWContaBancaria);
begin
_Conta := AConta;
{ ... }
end;

function TWPgtoDebitoAuto.efetuaPgto (ATitulo: TWTitulo) : integer;
begin
_Conta.Conecta;
_Conta.DebitaValor(ATitulo.ObtemValor);
ATitulo.RegistraPagto;
{ ... }
end;
A implementação da TWPgtoDebitoAuto no quadro acima mostra que podem ser necessárias outras informações para que a tarefa seja executada num determinado algorítmo. A função que executa a tarefa, no entanto, foi fixada na classe base (a interface) e não deve ser alterada já que isso implicaria que todas as outras heranças teriam que respeitar os novos parâmetros, o que nem sempre é desejável. Então, uma solução é passar as informações extras no construtor da própria classe.

Esse detalhe tem que ser levado em conta pela Factory responsável pela criação de instâncias da classe de pagamento. De acordo com o conceito de Factory, devemos criar uma função que centraliza a instanciação de uma família de classes:
TWTipoPagto = (tpBoleto, tpDebitoAuto, tpInternet);

function CriaNovoPagto (ATipo: TWTipoPagto; AContaDebito : TWContaBancaria) : TWPagamento;
begin
Result := Nil;

case (ATipo) of
tpBoleto:
Result := TWPgtoBoleto.Create;
tpDebitoAuto:
Result := TWPgtoDebitoAuto.Create (AContaDebito);
tpInternet:
Result := TWPgtoInternet.Create;
end;
end;
O último aspecto que falta abordar é com relação às características da classe de contexto TWTitulo. Além de propriedades inerentes a títulos - como seu valor, data de vencimento, código de barras, identificação do beneficiário, etc. - essa classe terá que manter uma instância da classe base de estratégia (o pagamento) para poder trocar mensagens com ela.
TWTitulo=class
protected
_Id : String;
_DataVencto : TDateTime;
_Valor : Double;
_Pagto : TWPagamento;
_FoiPago : Boolean;
procedure RegistraPagto;
{ ... }
public
function ObtemValor : Double;
function Pagar (AMeioPgto: TWTipoPagto): integer;
{ ... }
end;

{ ... }

function TWTitulo.Pagar (AMeioPgto: TWTipoPagto): integer;
begin
if (_Pagto = Nil) then
_Pagto := CriaNovoPagto (AMeioPgto, _Sessao.ObtemContaBancaria);

_Pagto.efetuaPgto (Self);
end;
Com isso, temos a classe de contexto armazenando internamente uma instância do pagamento (a estratégia) e controlando seu ciclo de vida. A instância fica disponível para que o contexto possa utilizá-la sempre que for necessário executar a tarefa embutida nela.

Note que a solução fica aberta para receber facilmente novos algoritmos, preservando o fraco acoplamento entre as classes já previstas e implementadas. Assim, poucos pontos têm que ser alterados para se introduzir uma nova forma de realizar a tarefa: basta codificar a nova classe e prever a criação de suas instâncias na função factory.

29 de setembro de 2011

Design Patterns com Delphi: Strategy - Parte I

Certas tarefas em um sistema computacional podem ser implementadas de diferentes maneiras sem que isso afete o resultado final esperado. Isto é, partindo de um determinado contexto, podemos selecionar um algoritmo entre vários possíveis e atingir o mesmo objetivo com qualquer um deles. Por exemplo, dada uma lista que precisa ser ordenada, há diversos algoritmos de ordenação que podem realizar a tarefa, como o método da bolha, quick sort ou busca binária. Independentemente do algoritmo escolhido, ao final do processo teremos a lista classificada. É claro que há diferenças de performance de um método pra outro mas isso é parte do processo de escolha daquele que melhor se adapta a uma situação. Cada situação encontrada pode levar a uma escolha distinta, sem que uma seja necessariamente preferida em relação às outras.

O programa pode, então, ter que intercambiar dinamicamente o algoritmo, seja por opção explícita do usuário ou por causa de outras questões circunstanciais encontradas durante a execução. Em projetos orientados a objetos, podemos desenhar uma solução para esse tipo de cenário usando o Design Pattern comportamental Strategy.

O objetivo do Strategy é permitir que o programa utilize de forma transparente qualquer algoritmo capaz de realizar uma determinada tarefa, construindo para isso uma estrutura de classes com baixo acoplamento. Com isso, fica fácil adicionar novos algoritmos, enquanto os códigos que os utilizarão continuam independentes da escolha feita.

Num sistema real, essa solução pode ser aplicada, por exemplo, no pagamento de títulos. Considere que um título pode ser pago levando o respectivo boleto ao banco, cadastrando a conta como débito automático ou registrando o pagamento no Internet Banking. Qualquer que seja a forma selecionada, o resultado é o título pago. O diagrama UML abaixo mostra as classes e suas relações para o Strategy aplicado a este cenário:
Diagrama UML para o padrão Strategy
A nomenclatura formal para as classes que participam da solução com o pattern Strategy é a seguinte:
A classe Strategy é uma interface onde é definido o comportamento comum a ser respeitado por todos os algoritmos. Em outras palavras, devemos incluir nesta classe funções virtuais (e provavelmente abstratas) que fixarão a forma com que a tarefa estará acessível às outras partes do programa. A classe TWPagamento do diagrama acima exerce esse papel, definindo que pagamentos deverão ser feitos através da função efetuaPgto.

São chamadas de Concrete Strategy as classes que atendem a definição introduzida no Strategy e que, portanto, implementam um algoritmo que efetivamente realize a tarefa proposta. No diagrama, esse papel cabe a três classes distintas : TWPgtoBoleto, TWPgtoDebitoAuto e TWPgtoInternete. Cada uma delas executará a tarefa a sua maneira, providenciando uma versão própria da função efetuaPgto.

O Context é a classe que usará o comportamento definido pela Strategy, invocando as funções disponibilizadas por ela. Portanto, o Context deverá armazenar internamente uma referência ao Strategy. Em geral, ele também terá que expor formas de interagir com o Strategy, publicando propriedade e funções. No nosso exemplo, o Context é a classe TWTitulo; a interação com o Strategy é conseguida passando-se para a tarefa uma instância do próprio título a ser pago.

Um Client é qualquer parte do sistema que solicite uma operação à classe Context. Esse tipo de classe foi omitido do diagrama.
No próximo post, mostro uma sugestão de como implementar na prática essa solução usando o Delphi.

Mais Informações
Posts sobre Design Patterns

7 de julho de 2011

Design Patterns com Delphi: State - Parte II

No último post, apresentei o conceito do Design Pattern comportamental State. Como exemplo de situação onde o padrão é aplicável, criei um controle simples de conta corrente no qual a conta tem comportamentos diferentes, reagindo ao nível do saldo que ela atualmente contém.

Para ficar mais fácil visualizar, reproduzo abaixo o diagrama de classes que representa uma solução para a situação do exemplo.
Diagrama UML para o padrão State

O modo como uma conta tem que se comportar é ditado por uma classe abstrata simples, chamada de TWEstadoConta no diagrama. Essa classe é apenas uma interface que introduz as funções e propriedades esperadas para a operação básica da conta em si. Os estados reais podem, então, ser implementados como heranças dessa interface, providenciando a diferenciação necessária.
TWEstadoConta = class
protected
_Saldo : Double;
_PorcRendim : Double;
_LimInf, _LimSup : Double;
_Conta : TWConta; { ... }

public
{ ... }
procedure Depositar (AValor : Double);virtual;abstract;
procedure Sacar (AValor : Double);virtual;abstract;
procedure AplicarRendimento;virtual;abstract;
end;

TWContaComum = class(TWEstadoConta)
protected
_TaxaServico : Double;
Constructor Create (AConta : TWConta);override;
public
procedure Depositar (AValor : Double);override;
procedure Sacar (AValor : Double);override;
procedure AplicarRendimento;override;
end;


TWContaDiferenciada = class(TWContaComum)
protected
_TaxaJuros : Double;
Constructor Create (AConta : TWConta);override;

public
procedure Depositar (AValor : Double);override;
procedure Sacar (AValor : Double);override;
procedure AplicarRendimento;override;
end;

TWContaOuro = class(TWEstadoConta)
protected
Constructor Create (AConta : TWConta);override;

public
procedure Depositar (AValor : Double);override;
procedure Sacar (AValor : Double);override;
procedure AplicarRendimento;override;
end;

As operações para depósito, saque e a que aplica rendimentos ao saldo da conta são abstratas na interface mas implementadas pela classe de cada estado possível. Veja, por exemplo, as diferenças da operação de saque:
procedure TWContaComum.Sacar (AValor : Double);
var lValorReal : Double;
begin
{ Conta comum, aplica a taxa de serviço }
lValorReal := AValor + _TaxaServico;
_Saldo := _Saldo - lValorReal;
end;

procedure TWContaDiferenciada.Sacar (AValor : Double);
var lJuros, lValorReal : Double;
begin
{ Na Conta diferenciada, aplica taxa de serviço, critica se extrapolar o limite de crédito e aplica uma taxa de juros se a conta ficar negativa }
lValorReal := AValor + _TaxaServico;
if (_Saldo - lValorReal) < _LimInf then
raise Exception.Create('Saque não pode ser efetivado. Conta sem saldo.');

lJuros := 0.0;
if (_Saldo - lValorReal) < 0.0 then
lJuros := (lValorReal - _Saldo) * _TaxaJuros / 100.0;

_Saldo := _Saldo - lValorReal - lJuros;
end;

procedure TWContaOuro.Sacar (AValor : Double);
begin
{ Conta Ouro, não se preocupa com nada - apenas saca. }
_Saldo := _Saldo - AValor;
end;

Os construtores das classes que representam estados têm que alimentar os valores das propriedades internas, caracterizando os respectivos estados. É isso que permitirá à classe da conta decidir qual estado deve estar ativo num determinado momento:
Constructor TWContaComum.Create (AConta : TWConta);
begin
inherited;
_PorcRendim := 1.0; { Rendimento padrão : 1%}
_TaxaServico := 5.00; { Saques são cobrados }
{ Valores de saldo que delimitam um estado }
_LimInf := 100.00;
_LimSup := 999.99;
end;

Constructor TWContaDiferenciada.Create (AConta : TWConta);
begin
inherited;
_PorcRendim := 0.0; { Sem rendimento }
_TaxaJuros := 1.00; { Taxa de juros sobre saques qdo a conta fica sem saldo }
_TaxaServico := 10.00; { Saques são cobrados }

{ Valores de saldo que delimitam um estado }
_LimInf := -100.00;
_LimSup := 99.99;
end;

Constructor TWContaOuro.Create (AConta : TWConta);
begin
inherited;
_PorcRendim := 2.00; { Rendimento Ouro : 2% }
{ Valores de saldo que delimitam um estado }
_LimInf := 1000.00;
_LimSup := 99999.99;
end;

A classe da Conta, então, deve guardar uma referência do estado atual para poder realizar as operações solicitadas. Portanto, ela age como uma ponte, transferindo para a classe de estado a responsabilidade de executar efetivamente a operação.
TWConta = class
private
_Estado : TWEstadoConta;
{ ... }
public
{ ... }
procedure Depositar (AValor : Double);
procedure Sacar (AValor : Double);
procedure AplicarRendimento;
{ ... }
function ObtemSaldo : double;
function ObtemEstado : TWEstadoConta;
end;
Para que a implementação desse padrão funcione, é preciso trocar dinamicamente a instância de classe que representa o estado atual. E quando é necessário efetuar essa troca da instância de estado ? O bom senso diz que isso deve acontecer após qualquer operação que possa afetar o estado monitorado. No nosso caso, isso significa atuar em todas as operações que modifiquem o saldo da conta.
procedure TWConta.VerificaStatus;
var lNovoEstado : TWEstadoConta;
begin
{ ... }
{ Testa as variações de estado }
lNovoEstado := Nil;
if (_Estado._Saldo < _Estado._LimInf) then
begin
{ Rebaixa de COMUM para DIFERENCIADA }
if (_Estado.InheritsFrom (TWContaComum)) then
lNovoEstado := TWContaDiferenciada.Create (Self);

{ Rebaixa de OURO para COMUM }
if (_Estado.InheritsFrom (TWContaOuro)) then
lNovoEstado := TWContaComum.Create (Self);
end;

if (_Estado._Saldo > _Estado._LimSup) then
begin
{ Eleva de COMUM para OURO }
if (_Estado.InheritsFrom (TWContaComum)) then
lNovoEstado := TWContaOuro.Create (Self);

{ Rebaixa de OURO para COMUM }
if (_Estado.InheritsFrom (TWContaOuro)) then
lNovoEstado := TWContaComum.Create (Self);
end;

{ Efetiva a troca de estado }
if (lNovoEstado <> Nil) then
SetEstado (lNovoEstado);
end;

procedure TWConta.Depositar (AValor : Double);
begin
_Estado.Depositar(Avalor);
VerificaStatus ();
end;

procedure TWConta.Sacar (AValor : Double); begin
_Estado.Sacar (AValor);
VerificaStatus ();
end;

procedure TWConta.AplicarRendimento;
begin
_Estado.AplicarRendimento ();
VerificaStatus ();
end;

Pelo quadro acima, vemos que quem faz a troca é a própria conta pois as classes de estado não conhecem o fluxo de mudança, isto é, nenhuma delas sabe qual é o estado seguinte para o qual a conta pode ser lançada. Isso facilita a inclusão de novos estados já que apenas um ponto precisa ser modificado para levá-lo em consideração.

A recuperação de uma instância da classe de estado pode ser modelada para usar o Design Pattern criacional Factory ou ainda o Singleton.

Usar uma Factory como fizemos em VerificaStatus pode acarretar problemas de desempenho se as trocas de estado ocorrerem com muita frequência. O mesmo é válido se for grande o volume de dados que caracterizam os estados. Neste caso, optar por um Singleton é recomendável, sem esquecer de que ele deve se restringir ao escopo da classe. Isto é, cada instancia da classe deve ter seu Singleton particular; senão, o estado de uma instância poderá se confundir com o de outras que porventura existam ao mesmo tempo.

O download do projeto Delphi com esse exemplo pode ser feito a partir desse link.

27 de junho de 2011

Design Patterns com Delphi: State - Parte I

Ao longo de sua existência, um objeto em um sistema computacional pode sofrer variações em seu estado interno. Isto é, cada operação realizada com ele ou cada propriedade ajustada altera o valor de variáveis internas (membros), introduzindo mudanças no seu estado. Se o objeto tiver que se comportar de forma diferente de acordo com seu estado interno, a implementação dele tende a se tornar cada vez mais complexa à medida que a quantidade de estados possíveis aumenta, dificultando a manutenção do sistema como um todo.

O Design Pattern comportamental State foi concebido para atender a situações como as descritas no parágrafo anterior, simplificando a manutenção do sistema. Como exemplo do cenário onde o padrão é aplicável, imagine um sistema que controle contas correntes. Dependendo do saldo total, a conta é classificada de uma maneira que determina a disponibilidade de certas operações. Também determina se os serviços disponíveis para a conta são cobrados, a taxa de remuneração que se aplicará ao saldo (se aplicável), taxa juros sobre empréstimos (se aplicável), etc.

A base do conceito para implementarmos esse padrão é a existência de uma interface que torna públicas as operações disponíveis para a classe principal – a conta, no nosso exemplo. O estado interno é mantido por outra classe que tem o conhecimento de como se comportar adequadamente para esse estado específico. Quando a classe principal precisa realizar uma operação, ela delega a responsabilidade para a classe que mantém o estado. Podemos perceber, então, que cada conjunto diferente de comportamentos baseado no estado da classe principal será tratado por uma classe de estado diferente. O resultado desse arranjo é que a classe principal consegue comportar-se da forma planejada apenas trocando a instância interna da classe de estado, o que ocorre de forma transparente para quem usa a classe principal.

Veja no diagrama UML abaixo a relação entre as classes previstas para nosso exemplo:
Diagrama UML para o padrão State

Classes de objetos que podem alternar seu estado de acordo com uma lista previamente conhecida normalmente têm um Diagrama de Estados associado a elas quando modelamos o projeto usando UML. Esse diagrama documenta as mudanças de estado permitidas para o objeto. Com base no saldo existente, a conta do exemplo pode ser classificada como Ouro (melhores taxas de rendimento, menor preço para serviços), Comum (as taxas e preços normais se aplicam) e Diferenciada (o saldo baixo ou negativo leva a menor rendimento, serviços mais caros e proibição de saques e empréstimos).

Formalmente, as classes que participam da solução para o pattern State recebem os seguintes nomes:
Context é a classe que publica as operações que estarão disponíveis para as demais partes do sistema, sendo, por isso, a classe principal da solução. Ela mantém uma referência para a classe que contém o estado atual e o comportamento associado a este estado. No diagrama acima, a classe Context é a TWConta.

State é uma interface que define quais as operações do Context terão comportamento baseado num estado. Ela contém ainda as propriedades relevantes para determinar quando uma troca de estado deve acontecer. Esse papel é executado pela classe TWEstadoConta no diagrama acima. Por ser uma interface, ela normalmente será abstrata, isto é, muitas das funções serão implementadas somente por suas heranças.

Concrete State são as classes que provêem o comportamento adequado para cada estado possível. Elas herdam da interface State, promovendo a implementação necessária para cada operação introduzida por ela. No diagram há 3 classes nessa categoria: TWContaComum, TWContaOuro e TWContaDiferenciada.

Um Client é qualquer parte do sistema que solicite uma operação à classe Context. Esse é o papel da classe TWTelaOperConta no diagrama anterior.

O tipo de hierarquia proposta pelo State é muito semelhante àquela adotada no padrão estrutural Adapter. A diferença é que o Adapter serve para ocultar detalhes de implementação de objetos diferentes que têm a capacidade de executar tarefas similares, adaptando esses objetos a uma interface padronizada. Já o State simplifica a diferenciação de comportamentos dentro de um mesmo objeto. Ou seja, o primeiro foca no aspecto estrutural e envolve várias classes enquanto o segundo foca o comportamento de um único objeto.

No próximo post, mostro uma sugestão de como implementar em Delphi o exemplo usado aqui.

29 de abril de 2011

Design Patterns com Delphi: Observer - Parte II

No post anterior, apresentei o tipo de problema que o Design Pattern comportamental Observer se propõe a resolver, introduzindo os conceitos envolvidos na implementação de uma solução. Aqui, mostro uma sugestão de como efetivamente implementá-lo em projetos Delphi.

Dependendo do contexto onde o padrão Observer será empregado, a implementação dele poderá sofrer ligeiras variações. Uma das diferença mais comuns está relacionada a quantas instâncias de classes poderão ser notificadas sobre um mesmo evento. Eventos da VCL, por exemplo, podem ser respondidos por no máximo uma única função, vinculada a uma classe. Por isso, uma variável simples é o bastante para conter a referência da função que será chamada. Já no exemplo que aparece no post anterior, diversos usuários podem requerer notificação para a mesma atualização num objeto de negócio. Por isso, nossa implementação precisará armazenar os observers em algum tipo de lista.

Para facilitar, reproduzo abaixo o diagrama UML representando a proposta de implementação do exemplo do post anterior:
Diagrama UML para o padrão Observer

Pelo diagrama, a classe que define o comportamento base de todo Subject é a TWBusinessObj. É ela, portanto, quem tem que ser capaz de armazenar as instâncias dos Observer que receberão avisos de alteração. Veja no código abaixo que o controle de quais instâncias devem receber os avisos é feito com uma lista de objetos, ou seja, tantas instâncias quantas forem necessárias podem "assinar" o serviço e serem avisadas :
TWBOState=class;
TWBOObserver=class;

TWBusinessObj=class
protected
_ListaObsv : TObjectList;
{ ... }
public
procedure AttachObsv (AObsv: TWBOObserver);
procedure DetachObsv (AObsv: TWBOObserver);
procedure Notify;

function getState: TWBOState;virtual;abstract;

{ ... }

Constructor Create;
Destructor Destroy;override;
end;

implementation

Constructor TWBusinessObj.Create;
begin
_ListaObsv := TObjectList.Create (false);
end;

Destructor TWBusinessObj.Destroy;
begin
FreeAndNil (_ListaObsv);
inherited;
end;

procedure TWBusinessObj.AttachObsv (AObsv: TWBOObserver);
begin
_ListaObsv.Add(AObsv);
end;

procedure TWBusinessObj.DetachObsv (AObsv: TWBOObserver);
begin
_ListaObsv.Remove(AObsv);
end;

procedure TWBusinessObj.Notify;
var i : integer;
lObsv : TWBOObserver;
begin
for i := 0 to _ListaObsv.Count - 1 do
try
lObsv := _ListaObsv.Items [i] As TWBOObserver;
lObsv.Update(Self);
except
end;
end;

Há outros detalhes para prestar atenção. O construtor da lista recebe um parâmetro com valor false para indicar que ela não é responsável por devolver a memória utilizada pelos objetos que ela contém. Isso não pode acontecer pois ainda podemos precisar dos objetos inseridos na lista mesmo que o TWBusinessObj seja deletado. Lembre-se de que, neste exemplo, os objetos que receberão as notificações são instâncias que representam usuários no sistema e que eles podem requerer notificações de vários objetos de negócio diferentes ao mesmo tempo.

Um outro ponto é que a classe TWBusinessObj é abstrata e, portanto, não poderá ser diretamente instanciada. Cada herança dela precisará remover a abstração, implementando a função getState para fornecer informações específicas sobre o estado interno de suas propriedades. Veja um exemplo:
TWPedidoVenda=class(TWBusinessObj)
protected
{ ... }
_State : TWBOState;
public
{ ... }
function getState : TWBOState;override;
procedure setDataEntrega (ADt: TDateTime);
end;

{ ... }
function TWPedidoVenda.getState : TWBOState;
begin
Result := _State;
end;

procedure TWPedidoVenda.setDataEntrega (ADt: TDateTime);
begin
_State.ModifyProperty ('DT_ENTREGA', ADt);
Notify;
end;

A classe TWBOState que aparece nesse código é uma base que eu montei para conter as informações de estado específicas de cada objeto de negócio, permitindo recuperar o valor atual de cada propriedade e aquele que havia antes, caso ela tenha sofrido uma alteração. No trecho mostrado acima, uma instância dessa classe registra a alteração na data de entrega do pedido e, em seguida, o próprio pedido dispara notificações dessa alteração usando a função Notify mostrada no primeiro quadro desse post.

Fica faltando, então, a segunda parte da operação: fazer com que cada Observer interprete como lhe for conveniente as modificações notificadas, implementando a função Update:
TWBOObserver=class
public
procedure Update (AObj: TWBusinessObj);virtual;abstract;
end;

TWAuditoria=class(TWBOObserver)
public
procedure Update (AObj: TWBusinessObj);override;
end;

TWUsuario=class(TWBOObserver)
public
procedure Update (AObj: TWBusinessObj);override;
end;

{ ... }

procedure TWAuditoria.Update (AObj: TWBusinessObj);
var lState : TWBOState;
begin
lState := AObj.getState();
GravaAuditoria (lState);
end;

procedure TWUsuario.Update (AObj: TWBusinessObj);
var lState : TWBOState;
begin
lState := AObj.getState();
EnviaEMail (lState);
end;

Em algum ponto do sistema teremos que usar a função AttachObsv para vincular a classe de auditoria e os usuários apropriados, isto é, fazer com que essas classes "assinem" o serviço de notificação de um objeto de negócio:
var _Auditoria : TWAuditoria;
_Gerente : TWUsuario;
{ ... }
Constructor TWFormVenda.Create (AOwner: TComponent);
begin
inherited;
_Pedido := TWPedidoVenda.Create;
_Pedido.AttachObsv (_Auditoria);
_Pedido.AttachObsv (_Gerente);
{ ... }
end;

O planejamento do exemplo incluiu ainda a possibilidade de uma classe cancelar a assinatura, suspendendo as notificações para si. Para isso, basta chamar a função DetachObsv quando (e se) for necessário.