30 de agosto de 2009

Usando Interfaces em Delphi

Outro dia, estava comentando o uso do Design Pattern Prototype aqui no blog e para implementar o exemplo utilizei uma interface em Delphi. Por causa disso, algumas pessoas me perguntaram o que vem a ser esta tal de interface. Como vou utilizar o conceito para avançar no assunto "Delphi interagindo com scripts", achei melhor postar antes a respeito das interfaces.

Neste contexto, interface não tem nada a ver com telas de um programa ou partes do hardware mas sim com um recurso que algumas linguagens de programação - como Java e, mais recentemente, o Delphi - disponibilizam para tornar mais limpo o código do programa, que de outra forma teria que ser escrito usando heranças múltiplas (uma classe herdando de mais de uma classe base). Além do mais, Delphi e Java não suportam herança múltipla.

Uma interface pode ser encarada como um contrato que tem que ser respeitado. Esse contrato é descrito em termos de métodos e/ou funções que devem ser esperados. Então, ao criar uma nova Classe você pode declará-la como implementadora de uma (ou mais) interface. Isto obriga a nova classe a respeitar fielmente o contrato, ou seja, ela terá obrigatoriamente que providenciar codificação para todas as funções e métodos declarados na interface, inclusive respeitando suas assinaturas - isto é, a quantidade e os tipo dos parâmetros e, no caso de funções, o tipo de dado retornado. Em outras palavras, as Interfaces definem comportamento e as Classes implementam esse comportamento.

E, qual a utilidade disso ? Você poderá criar classes genéricas que façam uso das funções declaradas por uma interface e, então, qualquer classe que implementar a tal interface poderá se beneficiar da classe que a manipula. Um exemplo clássico desse conceito é a criação de uma classe genérica para ordenação. Tudo que é preciso fazer é declarar uma interface com uma função para comparar 2 valores; a classe de ordenação deve manter uma lista com os objetos a serem ordenados e usar a função de comparação para determinar a ordem correta de todos os elementos. Com isso, é possível ordenar listas de objetos de qualquer classe que implemente a interface de ordenação.

Em termos da linguagem de programação, a declaração de uma interface é muito parecida com a declaração de classes. Basicamente, é composto por um nome, a palavra chave interface, uma lista de métodos e/ou funções e end para encerrar. Segue como exemplo a declaração de uma interface para ordenação:
IWSortable = interface
function CompareTo (Elemen: TInterfacedObject): Integer;
end;
Para uma classe dizer que implementa uma determinada interface, usa-se sintaxe semelhante à da herança. No exemplo abaixo, a classe TWPessoa declara que faz uma implementação da interface IWSortable:
TWPessoa = class(TInterfacedObject, IWSortable)
public
Nome : String;
Idade : Integer;
function CompareTo (AElemen: TInterfacedObject) : Integer;
end;
Em Delphi, todas as interfaces são herança de uma mesma interface base chamada IUnknown, o que exige a implementação de alguns métodos. A classe TInterfacedObject do Delphi já faz essa implementação e é por isso que a utilizo como base para TWPessoa. Se fosse necessário incluir outras interfaces na classe TWPessoa, elas deveriam vir separadas por vírgula após o nome da classe pai.

Alguns fatos sobre as interfaces:
Todos os métodos e funções declarados numa interface tem visibilidade public. Por isso, não é permitido incluir nenhuma das declarações de visibilidade e informá-las ocasionará um erro de compilação.
Os métodos e funções de uma interface são sempre abstract e virtual. Portanto, ambas as diretivas são dispensáveis na declaração da interface. Informá-las ocasionará um erro de compilação.
Não é permitido incluir campos numa interface. Propriedadades, no entanto, são admitidas desde que as cláusulas read e write façam referência a funções e métodos.
Uma classe pode implementar quantas interfaces forem necessárias.
É permitido criar heranças de interfaces.


26 de agosto de 2009

Acrescentando suporte a Scripts em aplicações Delphi - parte 3

No último post, montei uma aplicação Delphi mostrando como usar o Script Control para avaliar uma expressão VBScript em tempo de execução. Lá, usei algumas funções básicas do componente e uma técnica simples para interagir com o usuário. Agora, vou avançar um pouco, modificando o programa do post anterior para dar a ele suporte à criação de rotinas completas que poderão também ser usadas para avaliar expressões. Ou seja, o usuário poderá avaliar expressões mais complexas, tendo também a liberdade de criar funções que encapsulam seus cálculos mais comuns.

Acrescentei ao programa uma nova guia com título "Métodos e Funções" na qual inclui um TRichEdit (onde o usuário poderá criar suas funções), um TListBox (apenas para listar as funções disponíveis após a criação) e um botão. A resposta ao clique do botão adiciona ao engine do VBScript as funções criadas pelo usuário:
_ScriptControl.Reset;
InitScriptControl;
try
_ScriptControl.AddCode(reFuncao.Text);
except
end;
A primeira instrução, Reset, faz com que a instância do componente Script Control descarte todo a configuração feita nele anteriormente, incluindo funções e métodos que já tenham sido adicionados. Isto evita que se tente criar uma função com um nome que já existe - todas elas serão recriadas e poderão até mesmo executar cálculos diferentes da versão que havia antes. Como toda a configuração foi descartada, a segunda linha é uma função que agrupa os comandos de configuração que haviam no OnCreate do Form, mostrado no outro post.

O que está dentro do Try/Except adiciona ao engine do VBScript no componente as funções digitadas pelo usuário no Rich Text. Erros de sintaxe embutidos no código das funções só serão reportados quando for avaliada uma expressão que use a função problemática.

Após termos adicionado as funções, podemos recuperar no programa informações a respeito delas na propriedade Procedures do componente. O código abaixo põe no ListBox o nome das funções criadas com o AddCode:
lbFuncoes.Clear;
for i := 1 to _ScriptControl.Procedures.Count do
lbFuncoes.AddItem(_ScriptControl.Procedures.Item[i].Name, Nil);
A propriedade Procedures é uma lista com as funções e métodos que adicionamos ao engine e cada elemento recuperado em Item traz informações sobre uma dessas funções/métodos, dizendo se aceitam parâmetros e em qual quantidade, se têm valor de retorno, etc.

Algo importante a observar aqui é que todo código solto que seja adicionado através do método AddCode é executado imediatamente. Por código solto quero dizer as linhas de script que não façam parte da declaração de uma função ou método. Esta técnica permite executar um script inteiro de uma vez, bastando adicionar todo o código numa única chamada ao AddCode. Isto também dá abertura a uma solução rudimentar para o problema de fazer o script enxergar variáveis do programa. Crie no script uma variável - digamos que se chame y - e a adicione ao componente com AddCode:
Dim y
No programa Delphi, antes de avaliar uma expressão que vá precisar do valor de y, adicione uma atribuição como a seguinte:
_ScriptControl.AddCode('y = ' + IntToStr (Y));
Ao fazer isso, o valor da variável y do seu programa Delphi é imediatamente transferido para a variável y que existe no script. A partir desse momento, qualquer avaliação de expressão que use y enxergará o mesmo valor do programa Delphi.

Há uma forma mais elegante de se fazer isso e a mostrarei em outro post.Para baixar o exemplo completo - incluindo código e executável - clique aqui. O programa foi feito com Delphi 2005 mas deve funcionar com pouca ou nenhuma alteração em outras versões.


Mais Informações
Acrescentando suporte a Scripts em aplicações Delphi - parte 1 e parte 2, Documentação do ScriptControl no MSDN, Programando com VBScript

25 de agosto de 2009

Acrescentando suporte a Scripts em aplicações Delphi - parte 2

Dando continuidade ao post anterior sobre o MS Script Control, vou mostrar aqui um exemplo básico do uso do componente, montando uma aplicação onde o usuário poderá informar uma expressão qualquer em VBScript numa caixa de texto. Pressionar um botão na tela fará com que o programa avalie essa expressão, lançando numa outra caixa de texto o resultado obtido. Estou considerando que a expressão é numérica mas essa é uma questão de desenho da aplicação; se for necessário, pode-se usar qualquer dos outros tipos de dado da linguagem de script - no caso, VBScript.

Considerando que você já tem o fonte com as interfaces do Script Control, o primeiro passo é criar e configurar uma instância do componente. Coloquei esse código no evento OnCreate do Form:
_ScriptControl := TScriptControl.Create (Self);

_ScriptControl.SitehWnd := Handle;
_ScriptControl.Language := 'VBScript';
_ScriptControl.Timeout := -1;
_ScriptControl.AllowUI := true;
_ScriptControl.UseSafeSubset := false;
_ScriptControl.OnError := OnScriptError;
De acordo com o post anterior, este código configura o componente para que ele use 'VBScript'. Também permite interação via interface gráfica usando o próprio Handle do meu Form. O Timeout foi ajustado com valor -1, significando que o componente não gerará eventos relativos à expiraração de tempo enquanto executa um script demorado. Não falei da propriedade UseSafeSubset: ela determina se o componente deve restringir ou não os comandos aceitos de forma que somente os considerados seguros possam ser executados. Ajustei-a aqui para false, garantindo que todo o conjunto de funções do VBScript estará disponível. Ajuste o valor dela para true se segurança for uma preocupação pois assim o componente não permitirá a execução de comandos que possam burlar as permissões de acesso.

O evento OnError é configurado para executar a função OnScriptError sempre que houver um erro durante a execução de scripts - mesmo erros de sintaxe são reportados por este método:
procedure TfTesteScript.OnScriptError(Sender : TObject);
var lMsg : String;
lControl : TScriptControl;
begin
lControl := Sender As TScriptControl;
lMsg := lControl.Error.Source + #13#10;
lMsg := lMsg + 'Erro ' + IntToStr (lControl.Error.Number) + #13#10;
lMsg := lMsg + lControl.Error.Description + #13#10;
lMsg := lMsg + 'Linha ' + IntToStr (lControl.Error.Line);
lMsg := lMsg + ' ,Coluna ' + IntToStr (lControl.Error.Column) + #13#10;
MostraErro (lMsg);
end;
O código reproduzido acima mostra que há uma propriedade do Script Control chamado Error que descreve toda a condição do erro que ocorreu, incluindo a linha e coluna dentro do código, um número de erro e uma descrição do erro. A documentação do objeto Error pode ser encontrada aqui.

Para concluir o exemplo, faltou mostrar como é feita a avaliação da expressão. O código é a resposta ao evento de clique no botão de avaliação:
procedure TfTesteScript.btAvaliarClick(Sender: TObject);
var res : Variant;
begin
try
res := _ScriptControl.Eval(edExpressao.Text);
if VarType (res) <> varEmpty then
edResult.Text := FloatToStrF(res, ffFixed, 15, 4);
except
edResult.Text := '## erro ##';
end;
end;
Veja que o processo é bastante simples: basta chamar o método Eval do Script Control passando como parâmetro a expressão digitada pelo usuário. O valor retornado é um Variant mas como o valor que estou esperando na expressão é de ponto flutuante, posso fazer a conversão de float para string sem problema. Em todo caso, todo o trecho é protegido por um par Try/Except.

Neste exemplo, as expressões que podem ser avaliadas são fixas, isto é, não consigo informar na expressão uma variável que eu tenho criado e alimentado no meu programa. Apenas constantes, funções e operações matemáticas são válidas. Há uma lista aqui com as funções definidas no VBScript e que podem ser utilizadas na caixa com o texto para avaliação de expressões.

No próximo post, mostro como incluir métodos e funções para executar scripts mais complexos. Em outra oportunidade, mostro também como permitir que o script interaja mais efetivamente com o programa através da adição de objetos do programa ao engine de script.

Para baixar o programa de exemplo - incluindo código e executável - clique aqui. O programa foi feito com Delphi 2005 mas deve funcionar com pouca ou nenhuma alteração em outras versões.

24 de agosto de 2009

Acrescentando suporte a Scripts em aplicações Delphi - parte 1

Quando se desenvolve aplicações destinadas a empresas com perfis operacionais e ramos de negócio muito diferentes entre si é inevitável que surjam situações em que um Cliente precise que determinado cálculo seja feito de um jeito enquanto para Clientes de outros ramos esse cálculo tem que ser de outra forma. Há situações que envolvem a criação de fórmulas e cada Cliente pode querer aplicar uma diferente. Qual a melhor abordagem para tratar essas discrepâncias ? Criar telas diferentes resolvem parcialmente o problema já que isso implica em que as opções de cálculo têm que estar disponíveis dentro do programa para serem utilizadas.

Essas situações apareceram em diversos pontos no ERP da ABC71 e, para solucioná-los, sugeri o uso de um componente COM distribuído pela Microsoft chamado MS Script Control. O objetivo desse componente é permitir que programas de terceiros tirem proveito da infraestrutura de Scripts do Windows de uma forma direta e simples, sem ter que se preocupar em conhecer os pormenores dessa infraestrutura nem ter que implementar as interfaces relacionadas a ela. Basta entrar com expressões que se queira avaliar ou incluir um código script para execução e o componente se encarregará do resto.

Listo abaixo as propriedades e métodos disponibilizados pelo Script Control.
Propriedades
Language determina qual a linguagem de script será usada pelo componente. Atualmente, apenas "VBScript" e "JScript" são aceitos por padrão.
Timeout é o tempo (em milisegundos) para execução do script. Se esse tempo decorrer mas o script ainda não terminou, o evento OnTimeout é disparado. Use o valor (-1) para que o timeout seja desconsiderado.
AllowUI deve ser ajustado para true se o código do script puder interagir com o usuário através da interface gráfica, como por exemplo usando a função MsgBox.
SitehWnd é o handle para a janela que exibirá interações com a interface gráfica.

Métodos
AddCode é usado para adicionar código script ao componente. É permitido incluir funções, métodos (sub rotinas), criação de variáveis, etc. Código que estiver fora de uma definição de funções ou métodos será executado imediatamente.
Eval avalia a expressão passada como parâmetro, retornando o valor obtido dessa avaliação. A expressão terá que ser escrita na linguagem estipulada na propriedade Language, estando sujeita à sintaxe e operadorades dessa linguagem. É permitido chamar funções ou métodos adicionados com o AddCode.
Run permite executar uma função ou método adicionado anteriormente ao componente, independendo se eles possuem parâmetros ou não. No caso das funções, o valor de retorno pode ser recuperado.
AddObject mapeia um objeto do seu programa de modo que o script pode utilizá-lo como se fosse nativo da linguagem em que o script foi escrito.
Reset põe o componente em seu estado inicial, isto é, sem qualquer código ou objeto adicionado. As propriedades são ajustadas para o valor padrão e devem ser modificadas conforme a necessidade antes de poder usar de novo o componente.

Eventos
OnError ocorre sempre que um erro é encontrado no código do script.
OnTimeout é disparado quando o tempo para execução terminou mas o script ainda está em execução. Esse tempo é configurado pelo valor da propriedade Timeout.

É recomendável que a execução de scripts com esse componente seja feita na Thread principal da aplicação pois certos comandos não são thread-safe, abrindo a possibilidade de pipocar erros estranhos em pontos aleatórios do código.

O Script Control é distribuído como uma biblioteca COM e, portanto, o primeiro passo é pedir ao Delphi que crie um fonte pascal baseado nessa biblioteca para que possamos utilizá-lo. Há um post aqui mostrando como fazer isso - basta substituir a biblioteca indicada por Microsoft Script Control.

No próximo post, coloco um exemplo prático do uso dos Script Control interagindo com um programa Delphi.