Duck Typing en C# con Castle.DynamicProxy

El duck typing es una característica bastante frecuente en lenguajes dinámicos y que a veces se echa de menos cuando se trabaja con (casi todos los) lenguajes estáticos. Consiste en poder implementar de forma implícita un interfaz, es decir, si un objeto cumple con el contrato definido en el interfaz, aunque no lo haya declarado explícitamente, se considerará a todos los efectos que implementa ese interfaz.

La idea (y de ahí viene el nombre) es que si camina como un pato y nada como un pato, es que es un pato o al menos me sirve como tal, es decir, no me importa lo que sea siempre y cuando haga lo que necesito.

Veamos un ejemplo para entenderlo mejor:

public interface ICalculator
{
    int Add(int a, int b);
}

public class ValidCalculator : ICalculator
{
    public int Add(int a, int b) { return a + b; }
}

public class NotACalculator 
{
    public int Add(int a, int b) { return a + b; }
}

En este caso tenemos dos clases iguales ValidCalculator y NotACalculator que tienen los mismos métodos, pero una de ellas implementa el interfaces ICalculator y la otra no. En C# sólo podríamos usar como ICalculator instancias de ValidCalculator. Si C# soportase duck typing, aunque NotACalculator no haya declarado que implementa el interface ICalculator sí que se podría usar como tal.

Si el código anterior fuese nuestro, la solución sería muy sencilla: hacer que NotACalculator implementase el interface ICalculator y dejar de complicarnos la vida. El problema es que a veces necesitamos que una clase que no controlamos nosotros implemente un interfaz determinado.

La solución «de libro» a esto es aplicar el patrón adapter, que nos permite adaptar el interfaz de una clase para adecuarlo a nuestros requisitos:

public class CalculatorAdapter : ICalculator
{ 
    private readonly NotACalculator inner;
    public CalculatorAdapter(NotACalculator inner)
    {
        this.inner = inner;
    }

    public int Add(int a, int b)
    {
        return inner.Add(a, b);
    }
}

Lo bueno del patrón adapter es que además nos permite ajustar pequeñas diferencias en el interfaz, como pueden ser nombres de métodos, tipos de argumentos, etc.

Lo malo es que cuando la clase ya implementaba exactamente el interfaz, estamos escribiendo un código repetitivo y que no aporta mucho a la aplicación

Duck Typing con Castle.DynamicProxy

Para evitar escribir estos adapters podemos utilizar LCG y para eso una buena opción es aprovechar Castle DynamicProxy, del que ya hemos hablado por aquí para aplicar AOP con interceptores.

Mi objetivo es conseguir pasar este test:

[TestFixture]
public class DuckInterceptorTest
{
  public interface ICalculator
  {
    int AddNumbers(int a, int b);
  }

  public class NotACalculator
  {
    public int AddNumbers(int a, int b) { return a + b; }
  }

  [Test]
  public void DuckTyping_of_methods()
  {
    ICalculator calculator = DuckType.As<ICalculator>(new NotACalculator());
    Assert.That(calculator.AddNumbers(2, 3), Is.EqualTo(6));
  }
}

Quiero implementar un método DuckType.As<T>(target) para generar dinámicamente un adapter que implemente el interfaz especificado y redirija las llamadas a los métodos adecuados del objeto indicado. Vamos, lo mismo que antes habíamos hecho manualmente en el ejemplo, pero de forma genérica y reutilizable.

Pues resulta que hacer eso con DynamicProxy no es demasiado complicado (aunque depende un poco de cómo de detallistas queramos ser):

public static class DuckType
{
  public static T As<T>(object target) where T : class
  {
    var interceptor = new DuckTypingInterceptor(target);
    return new ProxyGenerator().CreateInterfaceProxyWithoutTarget<T>(interceptor);
  }

  private class DuckTypingInterceptor : IInterceptor
  {
    private readonly object target;

    public DuckTypingInterceptor(object target)
    {
      this.target = target;
    }

    public void Intercept(IInvocation invocation)
    {
      var methods = target.GetType()
                          .GetMethods(BindingFlags.Instance | BindingFlags.Public);

      // This should (probably) be cached
      var method = methods.FirstOrDefault(x => IsCompatible(x, invocation.Method));

      if (invocation.GenericArguments != null && 
          invocation.GenericArguments.Length > 0)
      {
        if (!method.IsGenericMethod)
          throw MissingMethodException(invocation);

        method = method.MakeGenericMethod(invocation.GenericArguments);
      }

      if (method == null)
        throw MissingMethodException(invocation);

      invocation.ReturnValue = method.Invoke(target, invocation.Arguments);
    }

    private MissingMethodException MissingMethodException(IInvocation invocation)
    {
      return new MissingMethodException(
                   string.Format("Cannot found compatible method {0} on type {1}", 
                     invocation.Method.Name, target.GetType().Name));
    }

    private bool IsCompatible(MethodInfo m1, MethodInfo m2)
    {
      // FIXME: handle ref/out parameters and generic arguments restrictions

      if (m1.Name != m2.Name || m1.ReturnType != m2.ReturnType)
        return false;

      if (!m1.IsGenericMethod)
      {
        var parameterTypes1 = m1.GetParameters().Select(p => p.ParameterType);
        var parameterTypes2 = m2.GetParameters().Select(p => p.ParameterType);

        return parameterTypes1.SequenceEqual(parameterTypes2);
      }

      return true;
    }
  }
}

Para generar el proxy usamos el método ProxyGenerator.CreateInterfaceProxyWithoutTarget, que crea un proxy que implementa el interfaz especificado y no tiene un objeto detrás, por lo que toda la implementación que queramos hacer tendremos que hacerla mediante interceptores.

Ahí entra en juego la clase DuckTypingInterceptor, que es el interceptor que se va a encargar de redigir las llamadas al objeto sobre el que estamos aplicando el duck typing. Cuando se intercepta una llamada, se busca un método en el objeto que estamos adaptando que tenga la misma signatura que el método invocado, y se redirige a ese método la llamada.

Conclusiones

Como decía al principio del post, el duck typing es una característica bastante interesante que a veces se echa de menos en lenguajes estáticos. Es verdad que al aplicarlo con técnicas como las descritas en este post se pierden partes de las ventajas de un lenguaje estático, fundamentalmente la detección de errores en tiempo de compilación, pero a cambio ganamos una flexibilidad que puede ser útil en determinados casos.

Uno podría pensar que el uso de dynamic y ExpandoObject en C#4 permite hacer esto de forma más sencilla, pero lo cierto es que no he visto una forma de sacarle partido porque aunque podemos pasar el objeto declarado como dynamic como si fuese cualquier interface, lo único que obtendremos será un RuntimeBinderException.

La implementación es muy mejorable y tiene varios fallos, como que no gestiona parámetros ref/out o fallaría si hay sobrecargas de métodos genéricos, pero sirve como prueba de concepto y es fácil para trastear con ella. Podéis encontrar el código completo con algún test más demostrando cómo funciona en este gist.

Si alguien le quiere echar un ojo a librerías más completas (y complejas) para hacer esto, puede empezar por impromptu o clay, que tienen buena pinta.

3 comentarios en “Duck Typing en C# con Castle.DynamicProxy

  1. Estimado Juanma:

    He estado dándole 20.000 vueltas (incluso ahora mientras venía conduciendo para regresar a casa) a un problema que me correo desde hace ya tiempo. Como purista que creo que soy y que me gustaría seguir siendo, he tratado siempre de separar responsabilidades correctamente y crear código eficaz, lo menos repetitivo posible y con la suficiente abstracción como para poder resolver el mismo tipo de problema en la mayoría de los casos.

    Iré al grano: Programo en WPF con MVVM y en gran cantidad de situaciones me surge la necesidad de mostrar al usuario ventanas (modales casi siempre) en las que se solicita la alta/modificación de cierta entidad. Pero en el momento en que introduzco cambios de ventana y mostrar esto o aquello, la cosa hace aguas hasta hundirse por todos sitios.

    He visto muchísima maneras de tratar con ello. Casi todas me disgustan o me parecen burdas maneras de tapar el problema tras decens de capas. Cuando menos, hay algunas formas que no dejan de ser ingeniosas, aunque en todas las aproximaciones que he leído, siempre me parece que la Vista y el ViewModel están muy atadas a la tecnología subyacente, de tal manera que siempre acabo viendo un ShowDialog que aparece como si de un grano en mitad de la frente se tratase.

    Es inevitable que esté por algún lado, porque la ventana ha de mostrarse y debería acabar en una llamada al método Show/ShowDialog, pero nunca jamás en un ViewModel.

    He pensado en una especie de servicio de flujo que se encargue de la fontanería y los detalles de implementación, algo así como un servicio al que tú le dices «edítame un tipo T» y el servicio él solito sabría qué ventana mostrar, si será modal o no y qué acciones se llevarán a cabo cuando el usuario acepte o rechace la información de la vista. Pero ya te digo que sería una vista como parte del flujo y que a su vez, esa vista podría lanzar más ventanas, pero siempre totalmente «un-aware».

    Se me ocurre que este servicio llevaría una especie diccionario de vistas asociadas a cada tipo T y tal vez un verbo (o token) para discriminar vistas el caso de que hubiese varias para un mismo tipo T. En este aspecto es cuando pienso en las posibles dificultades de implementación, si debería usar inyección de dependencias y qué clases tendría que sacarme de la manga.

    Quería preguntarte sobre ello 2 cosas:
    ¿Existe ya algo que creas que me valdrá para mi propósito? No quiero reinventar la rueda :)
    Si no existe, ¿cómo ves de válida mi «idea»? ¿Voy de culo y cuesta abajo?
    Asimismo pienso que si hicieras un artículo sobre el tema sería de muchísima utilidad a tantos que como yo tratamos de abrir ventanitas sin casarnos con DialogResults y ShowDialogs.

    Muchísimas gracias.

  2. Ese problema del MVVM lo he resuelto utilizando ReactiveUI, hay formas de implementar interacciones con el usuario sin que el ViewModel sepa nada de la vista.

    El único «problema» que hay con ReactiveUI es que hay que aprender un poco de la programación Reactiva, mas específicamente sobre las Reactive Extensions.

Comentarios cerrados.