quinta-feira, 7 de janeiro de 2010

Interpretando regras dinamicamente com C#, IronRuby e JScript

Desenvolvo sistemas profissionalmente desde 1989, ou seja, já se vão quase 21 anos. Diversas linguagens, sistemas operacionais, e ambientes de desenvolvimento no curriculum. Quando começei a desenvolver, não havia recursos, o Brasil possuia reserva de mercado de informática, o que nos levava a trabalhar com microcomputadores que seriam menos poderosos que qualquer celular de hoje em dia. Um HD de 10MB era um luxo caríssimo, e os sistemas eram construídos para ocuparem no máximo 640kb de RAM. Quando foi lançado o disquete de 3 1/2, lembro-me de ter conversado com um colega: "Quando vamos precisar de um deste? É um mini-HD!!!".

Como linguagens de programação, desenvolviamos em Assembly, C, C++, Pascal, o Delphi não estava disponível, e praticamente ninguém havia ainda tido contato com nenhuma versão preliminar do Windows. Mas a grande vedete das linguagens usadas profissionalmente era o Clipper, sem dúvidas.

O Clipper, além de ter vínculo com um banco de dados nativo (DBF, padrão usado no DBASE), tinha uma série de recursos bons, e podia-se fazer misérias com ele, usando ou não a integração com C e Assembly através das bibliotecas .pll (um precursor do conceito de dll). Um dos recursos que sempre existiu no Clipper era o do Eval, onde se podia executar um código em tempo de execução e pegar seu retorno, e, a partir da versão 5.0, o uso dos blocos de código (algo muito parecido com os Func ou Predicate do .NET atual).

Na época, construía sistemas de epidemiologia médica, e outros para controle de acidentes de trabalho e trânsito. O que era meio que um lugar comum era a grande mutabilidade das regras de extração de dados, o que demandava uma série de relatórios. E um, em específico, que visava a extração de dados de procedimentos médicos para o reembolso pelo SUS, tinha suas regras alteradas quase todos os meses, e sempre gerava uma bela dor de cabeça.

Então, na época, resolvi o problema com a criação de uma base de dados de regras, onde os parâmetros de extração eram alimentados mês a mês, usando como padrão o que se tinha no mês anterior, com reaproveitamento total de fórmulas e regras, mudando apenas o código das regras que sofreram impacto. Então, através do uso do eval (para compilar a regra), e do bloco de código (para usar a regra compilada pelo eval dentro do mesmo), tornando a extração dos dados dinâmica.

Depois disto, já construí uma miríade de outros sistemas, e, invariavelmente, entre um e outro, me deparei com a necessidade de usar regras dinâmicas ou fórmulas, e sempre lamentei o fato que tal ferramenta estava indisponível, sendo necessário lançar mão de outros recursos para obter o mesmo resultado.

No .NET, já utilizei soluções onde havia a codificação de código dinâmico através dos recursos do CodeDom. Havia sempre muito trabalho, mas, com um certo encapsulamento em uma classe genérica, ficou até tranquilo de usar.

Nos últimos tempos, muitas novas linguagens foram agregadas a família .NET, como o F#, e uma destas gratas surpresas foi o IronRuby, ainda em beta, mas muito eficiente. Para este exemplo, utilizei a versão mais atual do IronRuby (http://www.ironruby.net/Download), e o Visual Studio 2008. No entanto, o exemplo funciona perfeitamente no Visual Studio 2010 Beta 2.

É preciso descompactar o IronRuby para o C:\ruby. Seguindo o conselho de vários fóruns, é bom que seja respeitado este diretório.

O exemplo consistirá em criar uma classe utilitária chamada RubyEval, que contemplará 2 métodos específicos:

a) EvalRegra -> Um método destinado a validar regras de negócio, recebendo um determinado tipo como parâmetro, e validando a regra de acordo com as propriedades que este tipo pode conter.

Para nosso exemplo, utilizaremos instâncias da classe

public class RegTeste {
public int Codigo{get;set;}
public decimal ValorSalario { get; set; }
public String Nome { get; set; }
}



Uma expressão de negócio válida, por exemplo, poderia ser algo como "e.ValorSalario > 1000". O método retorna true ou false, indicando que a regra é válida.

b) EvalExpressao -> Um método destinado a receber uma fórmula, executar e retornar o valor obtido. Por exemplo, "1000 * 2000".

Vale ressaltar que estamos falando como fins de EXEMPLO, ou seja, o uso desta abordagem em soluções de mundo real deve ser adaptado a sua realidade.

Criando o projeto

Inicie um novo projeto no Visual Studio 2008 (console, test application), e adicione as seguintes DLLs da pasta c:\ruby\bin: IronRuby.Dll, IronRuby.Libraries.dll, Microsoft.Dynamic.dll, Microsoft.Scripting.Core.dll, Microsoft.Scripting.dll.

Criando a classe RubyEval

A primeira parte deste exemplo (a criação do ScriptEngine para o IronRuby) pode ser encontrado em diversas fontes de pesquisa de forma praticamente igual, e basicamente não vai mudar. Então, vamos lá.

Adicione uma nova classe chamada RubyEval, e a seguinte instrução

using Microsoft.Scripting.Hosting;

Isto serve para podermos ter acesso ao ScriptEngine e ao ScriptRuntime. Deixei, de propósito, sem adicionar o using para o IronRuby.

Primeiro, vamos adicionar a variável estática

protected static ScriptEngine _localscEngine;

Que são estáticas por motivo de performance, e não precisarmos criar a todo o momento a instância do ScriptEngine. Em seguida, temos 2 opções: um construtor estático para RubyEval, ou a criação de uma propriedade estática para obter o ScriptEngine do Ruby. Optei pela primeira alternativa.

Adicione o código abaixo:

static RubyEval()
{
var setup = new ScriptRuntimeSetup();
setup.LanguageSetups.Add(
new LanguageSetup(
"IronRuby.Runtime.RubyContext, IronRuby",
"IronRuby 1.0",
new[] { "IronRuby", "Ruby", "rb" },
new[] { ".rb" }));
var runtime = ScriptRuntime.CreateRemote(AppDomain.CurrentDomain, setup);
_localscEngine = runtime.GetEngine("Ruby");
}


Apesar de termos propriedades que não vamos usar (por exemplo, não vamos abrir arquivos de código com a extensão .rb), os parâmetros são necessários.

Agora, vamos ver o código dos 2 métodos:

public static bool EvalRegra(T dadoEntrada, String codigoRegra)
{
string source = String.Format("Proc.new {{ |e| {0} }}", codigoRegra);
ScriptSource scriptSource = _localscEngine.CreateScriptSourceFromString(source);
var proc = (IronRuby.Builtins.Proc)scriptSource.Execute();
Predicate predicate = p => (bool)proc.Call(p);
return predicate(dadoEntrada);
}


Para a execução do código, vamos receber um determinado T dadoEntrada, e o código por extenso da regra (aderente a sintaxe que o IronRuby pode interpretar). A partir da nossa variável estática _localScEngine, que foi inicializada pelo construtor estático, compilamos o código.

A chamada a Execute() retorna um objeto, que é uma Proc do IronRuby. Então, o que temos a fazer é criar um Predicate, que recebe um objeto do tipo T e retorna bool para a execução do código da regra.

O segundo método é mais simples:

public static T EvalExpressao(String codigoExpressao)
{
ScriptSource scriptSource = _localscEngine.CreateScriptSourceFromString(codigoExpressao);
return (T) scriptSource.Execute();
}


Como receberemos uma fórmula, a chamada a Execute retorna o resultado da execução da mesma.

Para fins de encapsulamento, vamos definir um terceiro método que recebe uma lista de regras a verificar para um mesmo objeto, e retorna true apenas se todas as regras forem contempladas:

public static bool EvalRegras(T dadoEntrada, List listaRegras)
{
bool bRet = true;
foreach (var regra in listaRegras)
{
bRet &= EvalRegra(dadoEntrada, regra);
if (!bRet) break;
}
return bRet;
}


Um teste para as nossas rotinas

Primeiramente, vamos criar uma lista de objetos RegTeste (citado acima):

List lista = new List{
new RegTeste{ Codigo=1,Nome="José",ValorSalario=12211.00M},
new RegTeste{ Codigo=2,Nome="José Maria",ValorSalario=9883.00M} ,
new RegTeste{ Codigo=3,Nome="Maria José",ValorSalario=8828.00M},
new RegTeste{ Codigo=4,Nome="Alessandro",ValorSalario=2235.00M}};


Agora, vamos criar uma lista de regras a verificar (Imagine a lista vinda de uma tela de pesquisa, ou lida de um banco de dados, por exemplo):

List regras = new List { "e.ValorSalario > 3000",
"e.Nome.Contains(\"Maria\")" };


Com RubyEval e Linq, podemos fazer a aplicação do filtro facilmente como abaixo:

foreach(var reg in lista.Where(d=> RubyEval.EvalRegras(d,regras)))
{
Console.WriteLine(reg.Nome);
}


O uso do segundo método é mais direto:

var res = RubyEval.EvalExpressao("1020202020.00 * 22232323");





Uma segunda opção para EvalExpressao, sem o IronRuby


Outra opção para fazer um clone de EvalExpressao é o uso do JScript.

Adicione as seguintes referências a seu projeto: Microsoft.Vsa, Microsoft.JScript.

Adicione a seguinte classe:

using JS = Microsoft.JScript;

public class JScriptEval
{
private static JS.Vsa.VsaEngine _localEngine = JS.Vsa.VsaEngine.CreateEngine();
public static T EvalExpressao(String expressao)
{
var res = Microsoft.JScript.Eval.JScriptEvaluate(expressao, _localEngine);
return (T) Convert.ChangeType(res, typeof(T));
}
}


Daí, podemos usar uma fórmula como abaixo (atentando para o fato que a sintaxe deve ser compatível com o JScript):

var res2 = JScriptEval.EvalExpressao("Math.pow(1212123,3)");

Conclusão

Com o uso das novas linguagens dinâmicas (e aos recursos do .NET FrameWork), é possível implementarmos métodos para interpretação de código em tempo de execução, o que pode acrescentar tremenda flexibilidade ao nosso aplicativo. No entanto, como qualquer recurso, deve-se levar em consideração o custo-benefício da utilização do mesmo, visto que, independente do tempo ser mínimo (em razão dos recursos que possuímos hoje em dia, de processador e memória), em processos pesados este tempo pode ser determinante. Nem sempre uma grande flexibilidade é premissa. Use com critério, e divirta-se!

Forte Abraço !

Nenhum comentário:

Postar um comentário