image[1]

Este é o quinto post sobre IronRuby. Veja os anteriores:

  1. Rodando Ruby com C#: Parte 1
  2. Rodando Ruby com C#: Parte 2 – chamando uma classe Ruby no C#
  3. Rodando Ruby com C#: Parte 3 – chamando uma classe C# a partir do Ruby
  4. Rodando Ruby com C#: Edição especial: ASP.Net MVC 2.0, .Net 4.0 e Visual Studio 2010

Este é o post que eu queria escrever desde o começo. Nele vou mostrar um exemplo de uso real do IronRuby na sua aplicação, vou jogar uma idéia, que vai dar a vocês muitas outras idéias.

A idéia é que tenho uma aplicação feita com C#, mas tenho um pequeno pedaço da minha regra de negócios que quero deixar o usuário editar. Uma opção alternativa seria criar um formulário onde o usuário poderia tentar compor uma regra com textboxes, checkboxes e outros controles, e funcionaria, só que eu limitaria a liberdade do usuário na criação da regra. Com IronRuby posso dar o potencial do cliente escrever a regra em código.

Observação: A princípio eu ia fazer com ASP.Net MVC, usando C#. Por algum motivo não está rodando. Os testes funcionam, e funciona com uma aplicação console, mas não funciona com ASP.Net, seja MVC ou Webforms. Se alguém quiser testar, e não funcionar, votem lá no Codeplex para que o time possa olhar. O bug está aqui. Clique aqui para baixar a solution para testar. O projeto original com MVC está lá, assim como o de WebForms.

Como ficou o projeto:

Criei uma camada de apresentação em console app, só com C#, uma camada de domínio com C# e IronRuby, e os testes, só em C#, e às vezes passando código em IronRuby para testar.

Criei um sistema de pedidos. O cliente pode solicitar um pedido. Vejam as entidades:

Dessas, vale a pena ver a de pedido:

public class Pedido
{
    public Cliente Cliente { get; set; }

    public Pedido(Cliente cliente)
    {
        if (cliente == null)
            throw new InvalidOperationException("Cliente não pode ser nulo.");
        Cliente = cliente;
    }

    private List<ItemPedido> _itens = new List<ItemPedido>();

    public void Adicionar(ItemPedido itemPedido)
    {
        _itens.Add(itemPedido);
    }

    public IEnumerable<ItemPedido> Itens { get { return _itens; } }

    public decimal Valor
    {
        get
        {
            return _itens.Sum(i => i.Quantidade * i.Produto.Preco);
        }
    }

    public int Id { get; set; }
}

As classes do projeto são simples, sem grandes comportamento, praticamente só getters e setters. A classe de pedido é mais esperta e expõe apenas os itens como um Enumerable, mas só permite incluir itens através de seu método.

Há dois serviços também:

O ServicoCriacaoDePedido cria pedidos com o método Criar, passando a eles um objeto de valor chamado de CriarPedido:

Ele então retorna um pedido criado.

O serviço mantém ainda na propriedade RegrasPosCriacaoDoPedido uma lista estática de regras que acontecem após a chamada de criação do pedido.  Você pode criar regras chamando o método Incluir, e pode limpá-las chamando o método Limpar.

Quando o serviço criar um pedido, ele submete o pedido ao método privado RodarRegraSobreOPedido. É lá que as regras são aplicadas. As regras podem ser envio de e-mails, descontos, etc. Vejam a classe de regras:

public abstract class AposCriacaoPedido
{
    public AposCriacaoPedido()
    {
        Id = Guid.NewGuid();
    }
    public abstract void Rodar(Pedido pedido);
    public string Codigo { get; set; }
    public Guid Id { get; set; }
}

Tem um método rodar, que recebe um pedido e é abstrato, ou seja, vai ter que ser implementado por clases herdeiras. É neste método que acontece a regra. A classe tem ainda um Id para identificação. A parte onde você percebe que há algo diferente é na propriedade Codigo. É lá que fica o código do IronRuby. Ele fica lá apenas para referência, já que, vocês verão em breve, teremos classes IronRuby filhas desta classe.

É aí que entra o serviço de criação de regras, a outra classe de serviços, a da direita. Ela tem somente um método público chamado CriarRegraPosCriacaoPedido. Bamos vê-lo:

public AposCriacaoPedido CriarRegraPosCriacaoPedido(string codigo)
{
    const string templateDoMetodo =
        @"
        include RegrasDinamicas::Model
        class {0} < AposCriacaoPedido
            def Rodar(pedido)
                {1}
            end
        end";

    var nomeClasseTemp = "AposCriacaoPedido_" + Guid.NewGuid().ToString().Replace("-", string.Empty);
    var codigoCompleto = string.Format(templateDoMetodo, nomeClasseTemp, codigo);
    _engine.Execute(codigoCompleto, _scope);
    var classe = _runtime.Globals.GetVariable<RubyClass>(nomeClasseTemp);
    var retornoCriacao = _operations.CreateInstance(classe);
    var aposCriacaoPedido = (AposCriacaoPedido)retornoCriacao;
    aposCriacaoPedido.Codigo = codigo;
    return aposCriacaoPedido;
}

Notem que este método cria com código Ruby uma classe com nome aleatório. O template desta classe é uma string, que possui dois placeholders. O zero é o nome da classe, gerado via Guid, e colocado na variável nomeClasseTemp para depois ser substituído no template. O placeholder 1 é o corpo do método da regra, passado pelo usuário, e que fica na variável codigo. O código completo fica na variável codigoCompleto. A classe gerada herda da classe de regras, e portanto pode ser instanciada e feito cast dela de uma classe dinâmica do Ruby para a classe do C#. A partir daí, a classe de regra está gerada. Ela recebe na propriedade Codigo o código passado pelo usuário, e é então repassada para o chamador, que a coloca na coleção estática de regras do serviço de criação de pedidos, que a aplica sempre que um pedido é criado.

O resto desta classe de serviço é código de infra que já vimos nos outros posts.

Tem mais uns negócios interessantes no projeto, como repositórios, e um padrão MVC para escrita na console. Vale a pena dar uma olhada.

A partir daí é só utilizar. Vejam o controller de regras (um pouco resumido):

class RegrasController : IRegrasController
{
    private IList<AposCriacaoPedido> _regras;
    private IRegrasView _regrasView;
    private readonly ServicoCriacaoDeRegras _servicoCriacaoDeRegras = new ServicoCriacaoDeRegras();

    public RegrasController(IRegrasView regrasView)
    {
        _regrasView = regrasView;
    }

    public void ExibirRegras()
    {
        if (_regras == null)
            _regras = ServicoCriacaoDePedido.RegrasPosCriacaoDoPedido.ToList();
        var proximaAcao = _regrasView.Listar(_regras);
        proximaAcao(this);
    }

    public void CriarNova()
    {
        var proximaAcao = _regrasView.ExibirNova();
        proximaAcao(this);
    }

    public void CriarNova(string codigo)
    {
        var regra = _servicoCriacaoDeRegras.CriarRegraPosCriacaoPedido(codigo);
        ServicoCriacaoDePedido.IncluirRegraPosCriacaoDoPedido(regra);
        _regras = null;
        ExibirRegras();
    }

    public void ExibirRegra(int posicaoSelecionada)
    {
        if (posicaoSelecionada == -1)
            return;
        var pedido = _regras[posicaoSelecionada];
        var proximaAcao = _regrasView.ExibirRegra(pedido);
        proximaAcao(this);
    }
}

Posso listar as regras, que passa pelo método ExibirRegras:

Posso listar uma regra:

Notem no método CriarNova, que o serviço de regras é utilizado. Isso me permite criar uma regra (com multi line):

E entao executá-la ao inserir um pedido:

Note a mudança no preço. Ao alterarmos o preço de um produto, todos os pedidos mudam (pequeno erro de modelagem…).

O código está disponível para vocês baixarem. Depois vou postar sobre o MVC que usei no projeto, que achei que ficou interessante.

Giovanni Bassi

Arquiteto e desenvolvedor, agilista, escalador, provocador. É fundador e CSA da Lambda3. Programa porque gosta. Acredita que pessoas autogerenciadas funcionam melhor e por acreditar que heterarquia é mais eficiente que hierarquia. Foi reconhecido Microsoft MVP há mais de dez anos, dos mais de vinte que atua no mercado. Já palestrou sobre .NET, Rust, microsserviços, JavaScript, TypeScript, Ruby, Node.js, Frontend e Backend, Agile, etc, no Brasil, e no exterior. Liderou grupos de usuários em assuntos como arquitetura de software, Docker, e .NET.