19 de março de 2014

Configurando sites do IIS com Delphi e WMI - Part I

Dois dos produtos no portfólio da ABC71 são soluções para internet e que, portanto, necessitam de configurações num servidor de aplicações web antes de ficarem disponíveis para os usuários. Configurar o IIS (Internet Information Services) manualmente em si nem é tão complexo mas, dada a quantidade de detalhes envolvidos, acaba se tornando um processo bastante sujeito a falhas.

Minimizamos as possibidades de erro deste processo criando um instalador em Delphi para realizar a parte mais braçal. Ele se baseia nos recursos da tecnologia WMI (Windows Management Instrumentation) para o IIS, o que permite criar sites e pools de aplicação, gerenciar as propriedades de ambos, adicionar tipos de arquivos reconhecidos pela aplicação (MIMEs), aplicar permissões de acesso, iniciar, interromper e reciclar sites, etc.

As classes WMI para acessar esses recursos não são instalados por padrão com o IIS mas podemos garantir que eles estejam presentes com o DISM, programa do Windows que serve, entre outras coisas, para gerenciar quais recursos do sistema operacional estarão habilitados. A linha de comando a seguir habilita as ferramentas para gerenciar o ISS via WMI; ela deve ser executada no servidor do IIS com o usuário Administrador para ter efeito.
dism.exe /Online /Enable-Feature /FeatureName:IIS-WebServerManagementTools /FeatureName:IIS-ManagementScriptingTools

O comando acima pode ser extendido para incluir a instalação do IIS e ativar os recursos que sejam necessários para o funcionamento do seu site, tais como extensões e filtros ISAPI, aplicações ASP ou CGI, etc.

Falei em outra ocasião aqui no blog sobre o WMI, tecnologia da Microsoft para administração centralizada de diversos aspectos de um computador. No post Obtendo nível de sinal do Wifi usando WMI com Delphi eu importei a interface COM do WMI, obtendo acesso às funções desse mecanismo. Neste post, usarei um recurso do Delphi para tornar o acesso mais prático: late binding; ou seja, acessarei diretamente os nomes das funções e propriedades de uma classe WMI, deixando para a linguagem resolvê-los em tempo de execução.

Antes de partir para qualquer tipo de ação, precisamos obter em nosso programa uma instância do WMI. O WMI é organizado hierarquicamente, tendo uma raiz (o namespace) para cada grupo de aspectos gerenciáveis do computador, representados por classes. Dado um nome de computador e um namespace, podemos obter a respectiva instância do WMI através da função abaixo:
function GetWMIObject(wmiHost, wmiRoot: string): IDispatch;
var chEaten: Integer;
BindCtx: IBindCtx;
Moniker: IMoniker;
objectName : String;
begin
{ Monta o nome do objeto WMI desejado }
objectName := Format('winmgmts:\\%s\%s',[wmiHost,wmiRoot]);
OleCheck(CreateBindCtx(0, bindCtx));

{ Obtém um "moniker" para o objeto indicado pelo nome montado a partir do host e do root }
OleCheck(MkParseDisplayName(BindCtx, StringToOleStr(objectName), chEaten, Moniker));

{ Recupera o IDispatch para facilitar o uso do objeto no programa }
OleCheck(Moniker.BindToObject(BindCtx, nil, IDispatch, Result));
end;

O resultado deste código é a instância do WMI criada como uma interface genérica IDispatch. Para ter acesso aos recursos do IIS no computador local, chamamos esta função passando o valor '.' como Host e 'root\WebAdministration' como raiz.
var FWmiObj : IDispatch;
begin
FWmiObj := GetWMIObject('.', 'root\WebAdministration');
{ ... }

De posse da instância do WMI, agora podemos iniciar a administração de sites propriamente dita. No caso do ERP Pronto da ABC71, nós criamos um Application Pool específico para isolar a execução de nossa aplicação, protegendo-a de problemas que eventualmente surjam em outros sites no mesmo servidor. Usando o WMI, podemos perguntar ao IIS se o pool já está criado e, então, criá-lo se for necessário:
var FObj, FEnum, FItem, FAppPool: OLEVariant;
lEnum: IEnumVariant;
FNome: String;
qtde: LongWord;
begin
FObj := FWmiObj;
FNome := 'ERPProntoServApp';
FENum := FObj.ExecQuery('SELECT * FROM ApplicationPool WHERE Name="' + FNome + '"', 'WQL', 0)._NewEnum;

{ Recupera instância do IEnumerateVariant para poder navegar pelos registros encontrados. }
lEnum := IUnknown(FEnum) As IEnumVariant;

{ Navega para o 1o registro do conjunto. Se achou, FItem contem o item encontrado. }
if (lEnum.Next(1, FItem, qtde) <> 0) then begin
{ Não achou, cria o pool aqui }
FAppPool := FObj.Get('ApplicationPool');
FAppPool.Create(FNome, false);

{ Localiza o pool recém criado para poder configurá-lo }
FENum := FObj.ExecQuery('SELECT * FROM ApplicationPool WHERE Name="' + FNome + '"', 'WQL', 0)._NewEnum;

lEnum := IUnknown(FEnum) As IEnumVariant;
if (lEnum.Next(1, FItem, qtde) <> 0) then
Raise Exception.Create ('Não foi possível criar pool ' + FNome);
end;

{ Outras configurações para o Pool }
FItem.AutoStart := true;
FItem.Enable32BitAppOnWin64 := true;
FItem.ManagedPipelineMode := 0; {integrado }
FItem.ManagedRuntimeVersion := ''; { sem .NET / código gerenciado }
FItem.RapidFailProtection := false;

{ Efetiva as alterações, gravando-as no IIS }
FItem.Put_();
{ ... }

Para forçar o Delphi a usar late binding, declaro uma variável OLEVariant e lanço nela o objeto WMI instanciado. Com isso, posso inquerir a classe ApplicationPool sem me preocupar em declarar a função ExecQuery do WMI. O resultado da chamada a essa função é um variant contendo um IEnumVariant, interface que permite percorrer a lista de registros encontrados. Como este resultado não implementa a interface IDispatch, o Delphi não consegue descobrir seus métodos e propriedades automaticamente, razão pela qual a conversão explícita é obrigatória neste caso. Já para os itens extraídos do enumerado, podemos contar com o late binding normalmente, conforme demonstra o código.

Se o ApplicationPool com o nome proposto não for encontrado pela consulta, o programa cria o pool. Isto é feito obtendo uma instância estática da classe de pools e invocando o método Create dela. Após isto, o pool já está criado e pode ser posicionado normalmente através de query.

Nossa aplicação é construída em C++ 32 bits, isto é, sem código gerenciado da plataforma .NET. Assim, o final do código é reservado para garantir que o pool execute corretamente neste ambiente, ligando as configurações apropriadas e gravando-as no IIS.

Num próximo post, mostro como criar a aplicação no IIS e associá-la ao pool criado aqui.

17 de janeiro de 2014

Facilitando a criação de queries complexas no SQL Server

Criar certas queries para um banco de dados pode ser um trabalho bastante intrincado. É o caso de consultas cujos filtros (cláusula WHERE) são baseados em cálculos independentes, bem como atualizações que são vinculadas ao resultado de alguma das funções de agregação (como SUM, MIN ou MAX). O cenário fica ainda pior se a manutenção em questão envolver mais de uma tabela, necessitando acessar outros dados através de joins.

Em geral, estas situações podem ser resolvidas mais facilmente usando-se cursores ou outra técnica de programação similar. Para quem usa o SQL Server, no entanto, a solução pode ser bem simples. Desde a versão 2005, esse gerenciador de Banco de Dados dispõe de um recurso chamado CTE - Common Table Expression ou "Expressão de Tabela Comum".

Basicamente, a CTE constrói uma tabela temporária com os registros resultantes de um comando SELECT especificado. A tabela temporária é, então, disponibilizada para o próximo comando SQL executado na mesma conexão com o banco de dados. Assim, esse segundo comando pode executar qualquer query (SELECTs, UPDATEs, DELETEs, INSERTs) referenciando a CTE como se este fosse uma tabela ordinária do banco de dados. Após esse comando ser executado, o SQL Server remove a tabela temporária criada pela CTE.

A sintaxe para declararmos uma CTE consiste na palavra chave WITH seguida pelo nome da tabela temporária, uma lista opcional para indicar os nomes que os campos assumirão, a palavra chave AS e, entre parênteses, a query propriamente dita. O quadro a seguir retrata a sintaxe descrita acima:
WITH nome-da-expressão [ ( nome-coluna-1 [ ,...nome-coluna-n ] ) ] AS
(
texto-do-SELECT
)
Para exemplificar na prática o uso desse recurso, considere o seguinte cenário: você possui uma tabela para registrar todas as alterações feitas num determinado documento em seu banco de dados. Cada alteração é cadastrada com um novo valor sequencial num campo que é parte da chave dos registros.

Em um determinado momento, você decide fazer uma faxina nos registros, limpando o banco de dados para manter apenas a última alteração registrada de cada documento. Como cada documento possui uma quantidade diferente de alterações registradas, não há um número de sequência que você possa usar como corte. A solução, então, é calcular a maior sequência de cada documento e remover os registros cujas sequências sejam menores que o valor calculado em cada documento diferente.

Veja abaixo uma definição simplificada da tabela e um conjunto hipotético dos registros contidos nela:
NUMERO_DOC TIPO_DOC SEQUENCIA USUARIO XML_ALTERACOES
1 NOTA FISCAL 0 GUSTAVO 0x00012010322032...
1 NOTA FISCAL 1 FULANO 0x00012A10962032...
1 NOTA FISCAL 2 GUSTAVO 0x012018650322027...
2 NOTA FISCAL 0 FULANO 0x012018650322027...
3 NOTA FISCAL 0 FULANO 0x012018650322027...
3 NOTA FISCAL 1 FULANO 0x012018650322027...
1 DUPLICATA CR 0 MANOELA 0x012018650322027...
1 DUPLICATA CR 1 MANOELA 0x012014603546520...
1 PEDIDO 0 JOSE 0x012014603520989...
Primeiro, vamos montar a CTE. Devemos obter um registro para cada documento diferente, calculando junto o maior número de sequência de alteração existente para cada um deles. Conseguimos isso facilmente com uma agregação simples:
WITH sel_docs AS (
SELECT numero_doc, tipo_doc, MAX(sequencia) AS sequencia
FROM docs
GROUP BY numero_doc, tipo_doc
)
Ao declarar esta CTE, omiti o nome das colunas na tabela resultante. Desse modo, os nomes adotados pelo SQL Server correspondem aos nomes na query interna. A coluna contendo o MAX não tem um nome por padrão, razão pela qual usei a cláusla AS para forçar num nome na própria query. No geral, é uma boa prática nomear explicitamente as colunas para evitar conflitos e tornar mais clara a operação como um todo.

A tabela abaixo mostra o conteúdo da nossa CTE, à qual dei o nome de sel_docs:
NUMERO_DOC TIPO_DOC SEQUENCIA
1 NOTA FISCAL 2
2 NOTA FISCAL 0
3 NOTA FISCAL 1
1 DUPLICATA CR 1
1 PEDIDO 0
Agora, a montagem do comando seguinte da conexão SQL considera o quadro acima como uma tabela do nosso banco de dados. Podemos, então, escrever o comando DELETE proposto no exemplo como um JOIN entre a CTE e a tabela original (DOCS):
DELETE docs FROM sel_docs
WHERE docs.numero_doc = sel_docs.numero_doc
AND docs.tipo_doc = sel_docs.tipo_doc
AND docs.sequencia < sel_docs.sequencia
O comando SELECT que compõe uma CTE pode ter cláusula WHERE e dispor de JOINS, se necessário. Poderíamos, portanto, ter restringido o tipo de documento na CTE do nosso exemplo ou até mesmo usar o resultado de algum cálculo envolvendo trechos do texto no campo XML_ALTERACOES para construir uma restrição mais elaborada.

Em tempo: o CTE está previsto na padronização do SQL ANSI-99; por isso, outros gerenciadores de banco de dados também implementaram esse recurso. O Oracle, por exemplo, o introduziu em sua versão 9i release 2, com sintaxe praticamente idêntica à apresentada neste post. Mais detalhes podem ser encontrados no artigo WITH Clause : Subquery Factoring.

29 de novembro de 2013

Convertendo objetos Delphi para o formato JSON

Damos o nome de serialização ao processo de pegar um objeto em memória numa linguagem de programação e convertê-lo para um formato padrão, compreensível para outras linguagens ou ambientes operacionais. A lista de tais formatos inclui sequência de bytes (stream), XML e, mais recentemente, JSON - sendo este último facilmente intercambiável através da internet. Falei sobre esse formato no post Lendo dados JSON em aplicações Delphi, onde mostro como transformar dados JSON em um objeto Delphi.

As classes descritas naquele post podem ser usadas manualmente para realizar o processo inverso, isso é, pegar um objeto no Delphi e representá-lo como um dado JSON. No entanto, o Delphi introduziu classes mais apropriadas para esse serviço, permitindo serializar objetos com facilidade.

Tomemos como exemplo um objeto da classe TWPedido, declarada no quadro abaixo. A classe proposta é complexa, composta tanto de propriedades de tipos atômicos quanto de tipos definidos pelo usuário, como a propriedade Produtos, que é um array dinâmico com instâncias de outra classe (TWProdutoPedido):
TWProdutoPedido = class
public
Codigo : String;
Descr : String;
Qtde: Double;
VrUnitario : Double;
end;

TWPedido = class
public
Numero: Longint;
Data: TDateTime;
VlrTotal: Double;
Produtos: array of TWProdutoPedido;

Constructor Create;
Destructor Destroy;override;

procedure AddProduto(AProd: TWProdutoPedido);
function GetQtdeProdutos : Integer;
end;

{ ... }

Constructor TWPedido.Create;
begin
SetLength (Produtos, 0);
end;

Destructor TWPedido.Destroy;
var lProd: TWProdutoPedido;
begin
for lProd in Produtos do
lProd.Free;

SetLength (Produtos, 0);
inherited;
end;

procedure TWPedido.AddProduto(AProd: TWProdutoPedido);
begin
{ Abre espaço para mais um produto }
SetLength (Produtos, Length (Produtos) + 1);
Produtos [Length (Produtos) - 1] := AProd;

VlrTotal := VlrTotal + (AProd.Qtde * AProd.VrUnitario);
end;

function TWPedido.GetQtdeProdutos : Integer;
begin
Result := Length (Produtos);
end;

function NovoPedido (ANro: Longint) : TWPedido;
var lProd: TWProdutoPedido;
begin
Result := TWPedido.Create;

Result.Numero := ANro;
Result.Data := Now;

lProd := TWProdutoPedido.Create;
lProd.Codigo := 'P001';
lProd.Descr := 'Computador';
lProd.Qtde := 1.000;
lProd.VrUnitario := 1500.00;
Result.AddProduto (lProd);

{ ... }

lProd := TWProdutoPedido.Create;
lProd.Codigo := 'P003';
lProd.Descr := 'Projetor';
lProd.Qtde := 1.000;
lProd.VrUnitario := 745.00;
Result.AddProduto (lProd);
end;

Serializar esse objeto em Delphi passa a ser questão de criarmos uma instância da classe TJSONMarshal. Tal classe se baseará nas informações adicionadas via RTTI (Run Time Type Information) para exportar automaticamente todos os campos públicos do objeto, mesmo estruturas e classes complexas criadas pelo próprio programador. Isso é feito num processo recursivo, de modo que toda a estrutura do objeto é considerada. O código a seguir mostra como usar a TJSONMarshal para fazer a serialização básica de uma instância da classe TWPedido:
var lMarshal : TJSONMarshal;
lPedido: TWPedido;
strJSON : string;
begin
{ Cria um novo pedido }
lPedido := NovoPedido (1234);

{ Realiza a serialização do pedido }
lMarshal := TJSONMarshal.Create (TJSONConverter.Create);

strJSON := lMarshal.Marshal(lPedido).ToString();

lMarshal.Free;
end;

O resultado será um texto no formato JSON semelhante ao publicado a seguir:
{"type":"WJsonObj.TWPedido","id":1,"fields":{"Numero":1234,"Data":41606.7632623727,"VlrTotal":2543,
"Produtos":[{"type":"WJsonObj.TWProdutoPedido","id":2,"fields":{"Codigo":"P001","Descr":"Computador","Qtde":1,"VrUnitario":1500}},

...

{"type":"WJsonObj.TWProdutoPedido","id":4,"fields":{"Codigo":"P003","Descr":"Projetor","Qtde":1,"VrUnitario":745}}]}}

Note que o mecanismo do Delphi inclui no resultado o nome do tipo de dado que originou o texto JSON, uma propriedade fields para armazenar todos os campos do objeto e uma identificação (id) para cada objeto. Esses valores são úteis no processo de transformação do texto JSON de volta para um objeto Delphi, o que pode ser conseguido através da classe TJSONUnMarshal.

O mecanismo possui ainda alguns truques para flexibilizar a serialização. Por exemplo, para impedir determinados campos sejam incluídos no resultado, podemos marcá-los com o atributo JSONMarshalled. Veja uma redeclaração da classe TWPedido solicitando a omissão do campo de valor total:
TWPedido = class
public
Numero: Longint;
Data: TDateTime;

{Marca VlrTotal para não ser incluído na serialização }
[JSONMarshalled(false)]
VlrTotal: Double;

Produtos: array of TWProdutoPedido;

{ ... }
end;

Um recurso mais elaborado consiste em registrar uma função anônima para realizar conversões personalizadas. É possível converter uma classe inteira para um outro tipo de dado mas também é permitido pinçar um único campo e tratá-lo conforme a necessidade. Por exemplo, o campo Data na classe de pedido é, por padrão, serializada como um double. Podemos interceptar sua conversão e forçá-lo a ser serializado como uma data textual:
lMarshal := TJSONMarshal.Create (TJSONConverter.Create);

{ Registra um conversor exclusivo para o campo Data do pedido }
lMarshal.RegisterConverter(TWPedido, 'Data',
function (Data: TObject; Field: string): string
var lProd : TWPedido;
begin
lProd := Data As TWPedido;
Result := DateToStr (lProd.Data);
end
);

strJSON := lMarshal.Marshal(lPedido).ToString();

lMarshal.Free;

Como podemos ver, a função RegisterConverter da classe de serialização deve ser chamada antes de fazermos a serialização em si. Após isso, o conversor que registramos é válido para quaisquer serializações que realizarmos com a instância do TJSONMarshal onde o registro foi feito. Embora o exemplo tenha registrado um único conversor, é permitido registrar tantos conversores quantos forem necessários para o trabalho. Há outras sobrecargas de RegisterConverter para atender diferentes demandas de particularização do processo; a documentação delas pode ser acessada neste link.

Para finalizar, uma última forma de interceptar a serialização é usando o TJSONConverter, cuja instância é passada ao construtor do TJSONMarshal. O TJSONConverter implementa uma série de eventos relativos aos diferentes tipos de dados tratados pela serialização e publica-os como métodos virtuais. Isto possibilita a criação de uma herança do conversor para modificar um ou mais desses eventos e personalizar o tratamento da serialização do respectivo tipo de dado.

12 de setembro de 2013

Usando funções anônimas em Delphi

Como o próprio nome diz, uma função anônima é um trecho de código de programação declarado sem um nome, podendo ser armazenado como uma variável e chamado num momento conveniente, de acordo com a necessidade. Essa característica possibilita a criação de algoritmos genéricos que podem ser aplicados em uma ampla gama de situações, independente do tipo de dado envolvido.

As situações mais comumente resolvidas com funções anônimas são as associadas à ordenação de uma lista de valores (sort), seleção de elementos de uma lista que atendam determinadas condições (filtragem) e a aplicação de cálculos envolvendo os valores contidos numa lista.

Muitas linguagens adotaram esse tipo de recurso em suas especificações, tais como C#, C++ 11, JavaScript e Perl. No caso do Delphi, as funções anônimas são um recurso nativo desde a versão 2009.

Para exemplificar o uso desse recurso em Delphi, trabalharemos com uma lista de produtos, realizando com ela operações tais como totalizar o custo dos produtos, aplicar uma porcentagem de aumento nos preços deles e obter uma sub lista dos produtos com características determinadas dinamicamente.

Em qualquer situação, o primeiro passo é definir um tipo de dado para referenciar a função genérica, estabelecendo os parâmetros e o tipo de retorno esperados, caso haja algum. Para somar o custo dos produtos, podemos desenhar uma função recebendo uma instância do produto atual e retornando o custo desse produto:
type
TWProduto = class
{ ... }
public
Codigo: String;
Qtde: double;
VrUnit: double;
CustoUnit: double;
end;

{ Declaração do tipo de dado para referência à função anônima }
TWObtemValorProduto = reference to function (AProd: TWProduto): double;

Com essa declaração feita, podemos montar a função genérica para percorrer uma lista de produtos e somar seus respectivos custos:
function CalculaCusto(Lista: TList<TWProduto>; ObtemValorProd: TWObtemValorProduto) : double;
var lProd : TWProduto;
begin
Result := 0.0;
for lProd in Lista do
Result := Result + ObtemValorProd(lProd);
end;

Observe que o segundo parâmetro é uma referência à uma função, representada pelo tipo que declaramos no primeiro quadro. Ele é usada no código como se fosse uma função comum mas, na realidade, podemos aproveitá-lo para realizar qualquer cálculo com os produtos da lista. Como demonstra o trecho de código a seguir, basta passar uma função anônima com o cálculo desejado:
var Lista: TList<TWProduto>;
soma : double;
begin
Lista := ObtemListaProdutos();

{ função anônima para somar, usando valor unitário }
soma := CalculaCusto (Lista, function (AProd: TWProduto) : double
begin
Result := AProd.Qtde * AProd.VrUnit;
end);
ShowMessage (FloatToStr(soma));

{ Usa outro tipo de cálculo, somando o custo unitário }
soma := CalculaCusto (Lista, function (AProd: TWProduto) : double
begin
Result := AProd.Qtde * AProd.CustoUnit;
end);
ShowMessage (FloatToStr(soma));

O mesmo conceito pode ser aplicado para criar um método de filtragem da lista, construindo uma nova que contenha apenas produtos selecionados. Neste caso, a função deve ter como retorno uma indicação determinando se um produto específico atende ou não o critério estipulado.
type
TWFiltroProduto = reference to function (AProd: TWProduto): boolean;

{ ... }

function Filtro(Lista: TList<TWProduto>; AFiltro: TWFiltroProduto) : TList<TWProduto>;
var lProd : TWProduto;
begin
Result := TList<TWProduto>.Create;

for lProd in Lista do
{ Se esse produto atende o critério, adiciona-o na lista de retorno }
if AFiltro (lProd) then
Result.Add(lProd);
end;

{ ... }
procedure TrataLista;
var Lista, ListaCaros: TList<TWProduto>;
begin
Lista := ObtemListaProdutos();

ListaCaros := Filtro(Lista, function (AProd: TWProduto) : boolean
begin
{ Filtra produtos custando mais que 5 reais }
Result := (AProd.VrUnit > 5.00);
end);
{ ... }
ListaCaros.Free;
Lista.Free;
end;

Observe que o algoritmo em si é genérico o suficiente para receber qualquer tipo de dado. Ou seja, poderíamos usar TObjectList no lugar da coleção TList, trocar o TWProduto por TObject e usar a função de filtro com qualquer classe do Delphi.

Pelo que vimos, o objetivo das funções anônimas é permitir a personalização da resposta a uma determinada ocorrência dentro de seu programa. Mas essa não é justamente a definição de eventos em Delphi e em outros ambientes de programação?

Há algumas diferenças entre responder a um evento e trabalhar com uma função anônima. Um evento normalmente está associado à instância de um componente. A resposta a ele exige, portanto, o uso de um ponteiro duplo: um para indicar qual objeto responderá ao evento e o outro para a função (nomeada) desse objeto que deve ser chamada. Já uma função anônima é somente um ponteiro para função, independente da existência de um objeto para funcionar.

Outra diferença importante está no escopo das variáveis. No caso do evento, as variáveis disponíveis são as mesmas acessíveis pelo objeto que está providenciando uma resposta, isto é, a resposta pode acessar os membros do objeto, as variáveis globais e os parâmetros fornecidos pela assinatura do evento. Já uma função anônima consegue enxergar também as variáveis locais existentes no ponto em que ela foi definida.

Para exemplificar essa última afirmação, vamos criar uma função que varra a lista aplicando uma regra a cada produto. A regra, que é passada como parâmetro, será o aumento no valor de cada produto usando uma porcentagem estipulada fora da função anônima:
{Processa a lista de produtos, aplicando a cada um os cálculos definidos no parâmetro DoProcProd }
procedure ProcessaProduto(Lista: TList<TWProduto>; DoProcProd: TWObtemValorProduto);
var lProd : TWProduto;
begin
for lProd in Lista do
DoProcProd(lProd);
end;

procedure TrataLista;
var Lista: TList<TWProduto>;
PorcCorrecao: double;
RegraCorrecao : TWObtemValorProduto;
begin
{ ... }
PorcCorrecao := 6.52; { Aumentar 6.52 % }
RegraCorrecao := function (AProd: TWProduto) : double
begin
AProd.VrUnit := AProd.VrUnit * (1+(PorcCorrecao/100.0));
Result := AProd.VrUnit;
end;
ProcessaProduto(Lista, RegraCorrecao);
{ ... }
end;
O trecho de código acima também mostra que é possível armazenar uma função anônima numa variável e aplicar exatamente a mesma regra em outros pontos do programa onde forem necessárias.

No próximo post eu mostro como usar funções anônimas para flexibilizar a serialização de objetos JSON, isto é, como personalizar a leitura e gravação de objetos no formato JSON para troca de informações entre programas, especialmente no ambiente web.

Mais Informações
Anonymous functions