Anonymous type para todos!

É, como previsto, demorou um tempo entre meu último post e este. Neste post, abordarei uma situação que aconteceu comigo hoje e achei por bem compartilhar o caso e o desfecho. Fato é que acho que encontrei uma forma de passar Anonymous Types por parâmetros e consumi-los futuramente sem precisar fazer a gambiarra sugerida no MSDN. Calma que eu explico…

Se sim, sim. Se não… Talvez?

Não é de hoje que tenho uma extrema aversão pelo uso de IF’s. Diria até que um dos primeiros pontos que indicariam a necessidade de refactoring em um código é quando existe uma classe ou método gigante. Normalmente, boa parte de seu corpo está tomado por blocos condicionais que escondem comportamentos que não o que a classe / método deveria se ater.

Vamos ao primeiro exemplo de “não faça isto em casa”:

  public static class ParentalControl
    {
        public static void Enter(User user)
        {
            if (user.Age < 18)
            {
                throw new Exception(string.Concat("Dear user ", user.Name, ", you must be over 18 to view this site"));
            }
            else
            {
                /// Redirects to next page...
            }
        }
    }
    public class User
    {
        public string Name { get; set; }
        public int Age { get; set; }
    }

A primeira vista, você pode não achar que temos um problema com o código acima. O código é relativamente simples e não apresenta dificuldade de entendimento, certo?

Ok. Mas todos sabemos que código algum tem garantia de ser o mesmo pra sempre e, fatalmente este modelo de código ditará a forma com que as próximas implementações serão realizadas nesta classe, concorda? Imaginemos que por uma mudança de escopo, o código seja alterado para o seguinte:

public static class ParentalControl
    {
        public static void Enter(User user)
        {
            if (user.Age < 18)
            {
                throw new Exception(string.Concat("Dear user ", user.Name, ", you must be over 18 to view this site"));
            }
            else if (user.LastPayment < DateTime.Now.AddMonths(-1))
            {
                throw new Exception(string.Concat("Dear user ", user.Name, ", you have to pay to continue."));
            }
            else if (!string.IsNullOrEmpty(user.NickName))
            {
                throw new Exception(string.Concat("Dear user ", user.Name, ", please choose a nickname to continue."));
            }
            else
            {
                /// Redirects to next page...
            }
        }
    }

É, acredito que este não é o tipo de código que você diria pra sua mãe que foi você que escreveu. Agora que demonstrei o problema, vamos tentar arrumar a casa.

Talvez a forma mais simples de higienizar o código que temos em mãos seria separar a porção de validação dos dados do usuário do método responsável por levar o usuário à próxima página. Uma forma simples de fazer isto seria com o seguinte código:

public static class ParentalControl
    {
        private static bool UserCanProceed(User user)
        {
            if (user.Age < 18)
            {
                throw new Exception(string.Concat("Dear user ", user.Name, ", you must be over 18 to view this site"));
            }
            else if (user.LastPayment < DateTime.Now.AddMonths(-1))
            {
                throw new Exception(string.Concat("Dear user ", user.Name, ", you have to pay to continue."));
            }
            else if (!string.IsNullOrEmpty(user.NickName))
            {
                throw new Exception(string.Concat("Dear user ", user.Name, ", please choose a nickname to continue."));
            }
            return true;
        }

        public static void Enter(User user)
        {
            if (ParentalControl.UserCanProceed(user))
            {
                /// Redirects to next page...
            }
        }
    }

Gosto da idéia de usar um retorno boleano para encapsular a ação de redirecionamento pois, desta forma, só de olhar o código você tem a visão de que para aquele bloco de código ser executado ele depende do resultado do método que o precede.

Uma pessoa normal talvez se dê por satisfeita com esta separação, mas este blog não faria jus ao nome se não tentasse algo “louco”.

PS: Idealmente, você migraria o método “UserCanProceed” para uma classe que contenha os métodos de validação necessários para se executar quando interagindo com a classe “ParentalControl” (algo como uma “ParentalControlRules”), mas para simplificar o artigo e os exemplos, vamos com esta solução mais simples.

Go IGINC

E se pudéssemos fazer algo do tipo:

private static bool UserCanProceed(User user)
        {
            return GenericRules
           .ForParameter(user)

           /// Verifica idade mínima
           .WithContext(new { MinAge = 18 })
           .ThrowingExceptionAs((context) => { return string.Concat("Dear user ", user.Name, ", you must be over 18 to view this site"); })
           .Check((parameter, context) => { return user.Age > context.MinAge; })

           /// Verifica data último pagamento
           .WithContext(new { BasePaymentDate = DateTime.Now.AddMonths(-1) })
           .ThrowingExceptionAs((context) => { return string.Concat("Dear user ", user.Name, ", you have to pay to continue."); })
           .Check((parameter, context) => { return user.LastPayment > context.BasePaymentDate; })

           /// Verifica se o nickname foi escolhido
           .ThrowingExceptionAs((context) => { return string.Concat("Dear user ", context.Name, ", you have to pay to continue."); })
           .Check((parameter, context) => { return !string.IsNullOrEmpty(user.NickName); })

            /// Executa todas as validações
           .ValidateRules();
        }

Com este código nós seriamos capazes de não só mapear as validações que precisamos executar como também de transformar estas regras em uma historinha para as gerações futuras de programadores que passarem por ali, fazendo com que você seja lembrado por toda a eternidade como alguém que tem muito amor ao próximo. :)

Tá, mas como faremos isto?

Segundo a MSDN, Anonymous Type são definidos da seguinte forma:

An anonymous type has method scope. To pass an anonymous type, or a collection that contains anonymous types, outside a method boundary, you must first cast the type to object. However, this defeats the strong typing of the anonymous type. If you must store your query results or pass them outside the method boundary, consider using an ordinary named struct or class instead of an anonymous type.

Ou seja, AT tem uma vida chata, limitada ao método onde é criada. Pelo que lemos ali, pra passar o AT adiante você precisaria fazer um cast dele para Object (sic), aniquilando assim com a beleza da tipagem. Não queremos isto, queremos?

Bom, para resolver 0 problema de passar o AT como parâmetro, existe uma série de posts na web sobre o assunto “anonymous type as parameters“, dentre eles, um em Português com uma solução “diferente” mas que parece funcionar. Entretanto, nenhum deles me deixou muito satisfeito.

No final das contas, as soluções mundo afora se resumem a de uma forma ou de outra realizarem o cast para Object e extrair por inferência o tipo real do objeto anônimo. Bom, o que fazer para evitar o Cast então?

Antes de continuar: Não estou dizendo aqui que ninguém pensou nesta solução antes, estou sim dizendo que não encontrei uma fonte que estivesse fazendo uso do mesmo formato que vou demonstrar abaixo. Sem mais delongas, vamos voltar ao código:


public GenericRuleContext WithContext<contextType>(contextType context)
{
...
}

Acredite ou não, a solução é basicamente esta. O segredo todo está em trabalhar com um método genérico que recebe um parâmetro tipado para este tipo dinâmico. O que acontecerá é que teremos uma inferência do tipo fornecido pelo tipo anônimo gerado e, conseqüentemente, poderemos trabalhar com ele, dali por diante, como qualquer outro tipo do sistema, sem diferenciação ou limitações (seja de escopo de método, classe, ou dll).

E, o mais legal disto tudo ainda está por vir (pelo menos eu achei)! Ver o Intellisense do VS reconhecendo este objeto que a documentação disse que não conseguiria é poesia para meus olhos!

Visual Studio - AT Intellisense

Visual Studio - AT Intellisense

Agora que passei pela parte mais estranha, segue o link para download do código da biblioteca que permite a sintaxe fluente de validação de dados.

PS: O código da infraestrutura fluente que expliquei neste artigo acabou ficando muito complexo para ser utilizado em produção. O ponto do código vinculado ao artigo é demonstrar algumas técnicas de desenvolvimento de interfaces fluentes e claro, a utilização de anonymous types como parâmetros.

Leia também: Why are anonymous types generic?

2 comentários

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s

%d blogueiros gostam disto: