O programador

Tornando-se dinâmico com a biblioteca Gemini

Ted Neward

Ted NewardOs leitores de meus artigos ou posts de blog vão saber isso já, mas para aqueles que já encontrara este artigo (seja por acidente ou porque eles pensaram que era um horóscopo), tendem a gastar muito tempo olhando para outros idiomas e plataformas. Isto é feito geralmente em busca de conceitos ou idéias que ajudam o software modelo mais eficaz, eficiente ou com precisão.

Uma tendência recente — embora não seja tão recente — dentro da Web comunidade tem sido a busca de linguagens "scripts" ou "dinâmicas", particularmente dois deles: Ruby (para o qual o rubi no framework Rails, também chamado de RoR, foi escrito) e JavaScript (para o qual temos node. js para executar aplicativos do lado do servidor, juntamente com centenas de quadros).Ambas estas línguas são caracterizadas por uma falta do que estamos acostumados no mundo C# e Visual Basic : aderência estrita a uma classe que define-se como a única definição para que um objeto consiste.

Em JavaScript (uma linguagem que por vezes é caracterizada pela espertinho apresentadores como eu como "Lisp com chaves"), por exemplo, um objeto é uma entidade totalmente mutável, ou seja, você pode adicionar propriedades ou métodos como necessários (ou desejado):

var myCar = new Object();
myCar.make = "Ford";
myCar.model = "Mustang";
myCar.year = 1969;
myCar.makeSounds = function () {
  console.log("Vroom!
Vroom!")
}

O objeto myCar, quando primeiro construído, não tem propriedades ou métodos nele — estas são implicitamente adicionadas quando os valores de dados ("Ford," "Mustang", 1969 e a função) são definidos como aqueles nomes (marca, modelo, ano e makeSounds). Em essência, cada objeto em JavaScript é apenas um dicionário de pares nome/valor, onde o valor do par pode ser um elemento de dados ou uma função a ser invocado. Entre os designers da linguagem, a capacidade de reproduzir com informações do tipo como este é muitas vezes chamada de um protocolo de meta-objeto (MOP), e um subconjunto restrito de isto é muitas vezes chamado de programação orientada a aspecto (AOP). É uma abordagem flexível e poderosa para objetos, mas um que é muito diferente do c#. Um pouco de tentar criar uma hierarquia de classe complexa em que você tenta capturar todas as variações possíveis através de herança, como sugere c# object design tradicional, a abordagem MOP diz que as coisas no mundo real não são todas exatamente iguais (exceto seus dados, claro) e a maneira em que você a modelo não deveriam ser, também.

Os desenvolvedores que já parte da Comunidade do Microsoft .NET Framework por muitos anos agora vão lembrar que uma versão anterior do c# introduziu o palavra-chave/tipo dinâmico, que permite que você declarar uma referência a um objeto cujos membros são descobertos em tempo de execução, mas isso é um problema diferente. (O conjunto dinâmico recurso torna mais fácil escrever código de reflexão-estilo, não criar tipos de espanador de objetos). Felizmente, os desenvolvedores de c# tem a opção de ambos: definições de tipo estático tradicional, através do c# classe projeto mecanismos padrão; ou definições de tipo flexível, através de uma biblioteca de código aberto chamado Gemini que constrói em cima de funcionalidade dinâmica para dar-lhe características de perto-JavaScriptian.

Noções básicas de Gemini

Como muitos dos pacotes que eu tenho discutido nesta coluna, Gemini está disponível através do NuGet: "Gemini instalar o pacote" no Console do Gerenciador de Pacotes traz a bondade em seu projeto. Ao contrário de outros pacotes, você tem visto, no entanto, quando o Gemini é instalado no projeto não traz um assembly ou dois (ou três ou mais). Em vez disso, ele traz consigo vários arquivos de origem e coloca-los em uma pasta chamada "Oak" e adiciona-los diretamente para o projeto. (Redação deste artigo, Gemini 1.2.7 consiste em quatro arquivos: Gemini.cs, GeminiInfo.cs, ObjectExtensions.cs e um arquivo de texto contendo as notas de lançamento.) A razão para a pasta chamada Oak é na verdade muito razoável: Gêmeos é na verdade um subconjunto de um projeto maior (chamado, não surpreendentemente, carvalho) que traz um monte desta bondade programação dinâmica ao mundo MVC ASP.NET — vou explorar o maior pacote de carvalho em uma coluna futura.

Por sua própria vontade, o fato de que gêmeos é entregue como fonte não é grande coisa — o código vive em seu próprio namespace (carvalho) e simplesmente será ser compilado para o projeto, como o resto dos arquivos de origem são. Em uma nota prática, no entanto, ter os arquivos de fonte torna absurdamente fácil de depurar o código fonte do Gemini quando algo dá errado, ou sequer folhear o código só para ver o que está disponível, porque o IntelliSense é às vezes completamente derrotado pelo uso do palavra-chave/tipo dinâmico.

Guia de Introdução

Novamente, como é meu hábito, gostaria de começar por criar um projeto de teste de unidade na qual deseja gravar alguns testes de exploração; em que projeto Gemini de instalar e testá-lo para fora, criando um simples "hello world"-como o teste:

[TestMethod]
public void CanISetAndGetProperties()
{
  dynamic person = new Gemini(
    new { FirstName = "Ted", LastName = "Neward" });
  Assert.AreEqual(person.FirstName, "Ted");
  Assert.AreEqual(person.LastName, "Neward");
}

O que está acontecendo aqui é sutil, mas poderosa: Gemini, o objeto do outro lado da referência "pessoa", é um tipo que é essencialmente vazio de todas as propriedades ou métodos, até que esses membros são atribuídos a (tal como no caso do código anterior) ou explicitamente adicionados ao objeto por meio dos métodos SetMember e GetMember, da seguinte forma:

[TestMethod]
public void CanISetAndGetPropertiesDifferentWays()
{
  dynamic person = new Gemini(
    new { FirstName = "Ted", LastName = "Neward" });
  Assert.AreEqual(person.FirstName, "Ted");
  Assert.AreEqual(person.LastName, "Neward");
  person = new Gemini();
  person.SetMember("FirstName", "Ted");
  person.SetMember("LastName", "Neward");
  Assert.AreEqual(person.GetMember("FirstName"), "Ted");
  Assert.AreEqual(person.GetMember("LastName"), "Neward");
}

Enquanto faço isto para membros de dados aqui, também é igualmente fácil de fazer isso para membros comportamentais (isto é, métodos), definindo-los para instâncias de DynamicMethod (que retorna void) ou Dynamic­função (o que espera para retornar um valor), cada uma delas leva sem parâmetros. Ou você pode defini-las para seus parceiros de "WithParam", se o método ou função pode levar um parâmetro, da seguinte forma:

[TestMethod]
public void MakeNoise()
{
  dynamic person =
    new Gemini(new { FirstName = "Ted", LastName = "Neward" });
  person.MakeNoise =
    new DynamicFunction(() => "Uh, is this thing on?");
  person.Greet =
    new DynamicFunctionWithParam(name => "Howdy, " + name);
    Assert.IsTrue(person.MakeNoise().Contains("this thing"));
}

Interessante um pequeno petisco surge fora da biblioteca de Gemini, a propósito: Objetos de Gemini (ausente qualquer tipo de implementação alternativa) usam "digitação estruturais" para determinar se são iguais ou se estes corresponderem a uma implementação específica. Ao contrário do OOP-tipo sistemas, que utilizam o herança/IS-teste para determinar se um determinado objeto pode satisfazer as restrições colocadas no tipo de um parâmetro objeto, sistemas estruturalmente digitados em vez disso perguntem se o objeto passado tem todos os requisitos (Membros, neste caso) necessários para fazer o código funcionar corretamente. Digitação estrutural, como é conhecido entre as linguagens funcionais, também vale pelo termo "duck typing" em linguagens dinâmicas (mas isso não soa tão legal).

Considere, por um momento, um método que utiliza um objeto e imprime uma mensagem amigável sobre esse objeto, como mostrado na Figura 1.

Figura 1 método que aceita um objeto e imprime uma mensagem

string SayHello(dynamic thing)
{
  return String.Format("Hello, {0}, you are {1} years old!",
    thing.FirstName, thing.Age);
}
[TestMethod]
public void DemonstrateStructuralTyping()
{
  dynamic person = new Gemini(
    new { FirstName = "Ted", LastName = 
      "Neward", Age = 42 });
    string message = SayHello(person);
    Assert.AreEqual("Hello, Ted, you are 42 years old!", 
      message);
    dynamic pet = new Gemini(
      new { FirstName = "Scooter", Age = 3, Hunter = true });
  string otherMessage = SayHello(pet);
  Assert.AreEqual("Hello, Scooter, you are 3 years old!", 
      otherMessage);
}

Normalmente, em uma hierarquia tradicional e orientada a objeto, pessoa e animal de estimação que provavelmente veio muito diferentes ramos da árvore de herança — pessoas e animais de estimação geralmente não compartilham um monte de atributos comuns em um sistema de software (apesar do que pensam os gatos). Em um estruturalmente ou sistema pato-digitado, no entanto, menos trabalho precisa entrar para fazer a cadeia de herança, profunda e abrangente — se não há um ser humano que também caça, então, Ei, que humanos tem um membro de "Hunter" sobre ele, e qualquer rotina que quer verificar o status de Hunter do objeto passado podem usar esse membro, se é um ser humano, gato ou drone Predator.

Interrogatório

O trade-off na abordagem de pato-digitação, como muitos vão notar, é que o compilador não pode impor que somente determinados tipos de objetos podem ser passados, e o mesmo é verdadeiro de tipos Gemini — especialmente porque a maioria dos código Gemini idiomaticamente armazena o objeto atrás de uma referência dinâmica. Você precisa tomar um pouco de tempo extra e esforço para garantir o objeto sendo entregue satisfaz os requisitos, ou então enfrentar algumas exceções de tempo de execução. Isto significa que interrogar o objeto para ver se ele tem o necessário membro, que é feito no Gemini usando o método de RespondsTo; Há também alguns métodos para retornar os vários membros que Gemini reconhece como sendo uma parte de um determinado objeto.

Considere, por exemplo, um método que espera um objeto que sabe como caçar:

int Hunt(dynamic thing)
{
  return thing.Hunt();
}

Quando o Scooter é passado, as coisas funcionam bem, como mostrado na Figura 2.

Figura 2 programação dinâmica quando funciona

[TestMethod]
public void AHuntingWeWillGo()
{
  dynamic pet = new Gemini(
    new
    {
      FirstName = "Scooter",
      Age = 3,
      Hunter = true,
      Hunt = new DynamicFunction(() => new Random().Next(4))
    });
  int hunted = Hunt(pet);
  Assert.IsTrue(hunted >= 0 && hunted < 4);
  // ...
}

Mas quando algo que não sabe como caçar é passado em, exceções resultará, conforme mostrado no Figura 3.

Figura 3 programação dinâmica quando ele falhar

[TestMethod]
public void AHuntingWeWillGo()
{
  // ...
dynamic person = new Gemini(
    new
    {
      FirstName = "Ted",
      LastName = "Neward",
      Age = 42
    });
  hunted = Hunt(person);
  Assert.IsTrue(hunted >= 0 && hunted < 4);
}

Para evitar isso, o método de caça deve testar para ver se o membro em questão existe usando o método RespondsTo. Este é um wrapper simples para o TryGetMembermétodo e é destinado para Boolean simples sim/não respostas:

int Hunt(dynamic thing)
{
  if (thing.RespondsTo("Hunt"))
    return thing.Hunt();
  else
    // If you don't know how to hunt, you probably can't catch anything.
return 0;
}

A propósito, se tudo isso parece clichê bastante simples ou um wrapper em torno de um dicionário < string, objeto >, isso não é uma avaliação incorreta — subjacente a classe Gemini é aquela interface exata do dicionário. Mas os tipos de invólucro ajudam a aliviar alguns dos movimentos de sistema tipo que seriam necessários, como faz o uso do palavra-chave dynamic.

Mas o que acontece quando vários objetos compartilham semelhantes tipos de comportamento? Por exemplo, todos os quatro gatos sabem como caçar, e seria um pouco ineficiente para escrever uma nova definição de método anônimo para todos os quatro, particularmente como todos os instintos de caça felina de quatro partes. Em OOP tradicional isto não seria um problema, porque eles seria todos os membros da classe gato e, assim, compartilham a mesma implementação. Em sistemas MOP, tais como JavaScript, normalmente há um mecanismo para permitir que um objeto para adiar ou "cadeia" uma chamada propriedade ou solicitação para outro objeto, chamado um "protótipo". Em gêmeos, você usar uma interessante combinação de tipagem estática e MOP chamado "extensões".

Protótipo

Primeiro, você precisa de um tipo base que identifica os gatos:

public class Cat : Gemini
{
  public Cat() : base() { }
  public Cat(string name) : base(new { FirstName = name }) { }
}

Observe que a classe gato herda de Gemini, que é o que permitirá que a classe gato ter toda a flexibilidade dinâmica que foi discutida até agora — de fato, o segundo Construtor gato usa o construtor de Gemini mesmo que foi usado para criar todas as instâncias dinâmicas até agora. Isto significa que toda a prosa anterior mantém-se para qualquer instância de gato.

Mas gêmeos também nos permite fazer declarações de como gatos podem ser "estendido", para que cada gato ganha a mesma funcionalidade sem ter que adicioná-lo explicitamente para cada instância.

Estendendo uma classe

Para um uso prático dessa funcionalidade, presumo que se trata de um aplicativo Web que está sendo desenvolvido. Freqüentemente, você precisa de HTML -­escapar os valores de nome sendo armazenado e retornado, a fim de evitar acidentalmente, permitindo injeção HTML (ou pior, injeção de SQL):

string Htmlize(string incoming)
{
  string temp = incoming;
  temp = temp.Replace("&", "&amp;");
  temp = temp.Replace("<", "&lt;");
  temp = temp.Replace(">", "&gt;");
  return temp;
}

Esta é uma dor para lembrar em cada objeto de modelo definido em seu sistema; Felizmente, MOP permite sistematicamente "chegar em" e definir novos membros comportamentais nos objetos do modelo, como mostrado na Figura 4.

Figura 4-escrevendo métodos sem escrever métodos

[TestMethod]
public void HtmlizeKittyNames()
{
  Gemini.Extend<Cat>(cat =>
  {
    cat.MakeNoise = new DynamicFunction(() => "Meow");
    cat.Hunt = new DynamicFunction(() => new Random().Next(4));
    var members =
      (cat.HashOfProperties() as IDictionary<string, object>).ToList();
    members.ForEach(keyValuePair =>
    {
      cat.SetMember(keyValuePair.Key + "Html",
        new DynamicFunction( () =>
          Htmlize(cat.GetMember(keyValuePair.Key))));
    });
  });
  dynamic scooter = new Cat("Sco<tag>oter");
  Assert.AreEqual("Sco<tag>oter", scooter.FirstName);
  Assert.AreEqual("Sco&lt;tag&gt;oter", scooter.FirstNameHtml());
}

Essencialmente, a chamada Extend está adicionando novos métodos para cada tipo de gato, com "Html", o sufixo para a propriedade FirstName pode ser acessada em uma versão HTML-cofre, chamando o método FirstNameHtml em vez disso.

E isso pode ser feito inteiramente em tempo de execução para qualquer Gemini -­herdando tipo no sistema.

Persistência e muito mais

Gêmeos não não destinado a substituir a totalidade do ambiente c# com um monte de objetos dinamicamente resolvidos — longe disso. No seu uso em casa, dentro do framework MVC de carvalho, Gemini é usado para adicionar outro comportamento útil e persistência para classes de modelo (entre outras coisas) e adicionar validação sem desordenar o código do usuário ou que requerem classes parciais. Mesmo fora de carvalho, no entanto, Gemini representa alguns mecânica projeto poderosa, o que alguns leitores podem lembrar da parte 8 do minha série .NET com vários paradigmas de um tempo atrás (msdn.microsoft.com/magazine/hh205754).

Falando de carvalho, que é na torneira para a próxima, então fique por perto para ver como isto tudo dinâmico se desenrola em um cenário do mundo real.

Boa codificação.

Ted Neward -é diretor da Neward & Associates LLC. Ele tem escrito mais de 100 artigos e autor e co-autor de vários livros, incluindo "Professional F # 2.0" (Wrox, 2010). Ele é um MVP de F# e famoso especialista em Java, além de participar como palestrante sobre o Java e o .NET em conferências no mundo todo. Ele atua como consultor e mentor regularmente. Entre em contato com ele pelo email ted@tedneward.com ou Ted.Neward@neudesic.com se estiver interessado em que ele trabalhe com sua equipe. Ele mantém um blog em blogs.tedneward.com e pode ser seguido no Twitter em twitter.com/tedneward.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Amir Ribeiro (melhorando a empresas)
Amir Ribeiro é consultor principal com as empresas a melhorar. Ele é um membro ativo da comunidade de desenvolvimento e tem especialização em ASP.NET MVC, HTML5, arquiteturas de resto, Ruby, JavaScript/CoffeeScript, NodeJS, iOS/ObjectiveC e F #. Ribeiro é uma verdadeira poliglota com uma paixão inabalável para o software. Ele está no Twitter em @amirrajan e na Web em github.com/amirrajan, amirrajan.net e improvingenterprises.com.