Interfaces + Extension Methods = Protocolos de Clojure

En el post anterior vimos cómo los multimétodos de clojure permitían resolver problemas típicos de una forma diferente a cómo lo haríamos en un lenguaje orientado a objetos como Java y C#. En este post vamos a otra herramienta de clojure para aplicar polimorfismo, los protocolos.

El problema de ejemplo

Nuevamente vamos a empezar por definir un problema de ejemplo que nos permita tener un poco de contexto a la hora de analizar las distintas soluciones ofrecidas por un lenguaje y otro. El problema en cuestión forma parte de lo que se conoce como el problema de expresión y consiste en tener que añadir operaciones a una jerarquía de tipos ya existente.

Supongamos que tenemos una estructura de clases para representar controles en pantalla, similar a la que encontramos en Windows Forms o WPF:

public class Control
{
  public string Text { get; set; }
  public Color Color { get; set; }
}

public class TextBox : Control
{
  public int MaxLength { get; set; }
}

public class Button : Control
{
  public ButtonStyle Style { get; set; }
}

Sobre esta jerarquía de clases, queremos implementar nuevas operaciones que nos permitan guardar el estado de un control para poder recuperarlo posteriormente (algo así como implementar un Undo/Redo).

Para ello necesitamos implementar los métodos void PersistState(IWriter writer) y void RestoreState(IReader reader) para poder tratar las distintas clases de forma homogénea dentro de nuestra aplicación, pero no podemos modificar las clases ya que no las controlamos nosotros, sino que forman parte del framework.

La solución OOP: Adapters

Una solución típica en un lenguaje orientado a objetos pasa por utilizar el patrón adapter, y tener algo así:

public interface IPersistentControl
{
  void PersistState(IWriter writer);
  void RestoreState(IReader reader);
}

public class PersistentTextBox : IPersistentControl
{
  private readonly TextBox textBox;
  
  public PersistentTextBox(TextBox textBox)
  {
    this.textBox = textBox;
  }
  
  public void PersistState(IWriter writer)
  {
    // guardar el estado sobre writer
  }

  public void RestoreState(IReader reader)
  {
    // recuperar el estado leyéndolo del reader
  }
}

// Implementaciones similares para el resto de controles

El patrón adapter nos permite dotar a los controles de un nuevo interface con las operaciones que necesitamos, y a la hora de utilizarlos dentro de nuestra aplicación, podemos encapsular los controles dentro de sus adapters correspondientes y tratarlos de manera uniforme como IPersistentControls.

Esta solución nos obliga a escribir bastante código, aunque es verdad que con un IDE adecuado gran parte de este código “se escribe solo”. El mayor problema radica en que cuando encapsulamos una instancia de TextBox dentro de un IPersistentControl, conseguimos que cumpla el contrato de IPersistentControl pero perdemos el contrato de Control, por lo que no podemos tener a la vez un objeto que cumpla ambos contratos, lo que puede ser bastante molesto.

La solución clojure: Protocolos

En clojure, un protocolo es muy similar a un interface en cuanto a que define un contrato, es decir, un conjunto de operaciones que debe cumplir un tipo determinado. La diferencia fundamental con un interface de C# o Java, es que podemos implementar protocolos sobre tipos que ya existen. Mientras que en C# una clase sólo puede indicar qué interfaces implementa en el momento de su declaración, con los protocolos de clojure podemos añadir a posteriori una implementación del protocolo para tipos que ya existen.

En el ejemplo anterior, tendríamos algo así:

(deftype Control )
(deftype TextBox )
(deftype Button )

(defprotocol PersistentControl
  (persist-state [this writer])
  (restore-state [this reader]))

(extend-type TextBox
  PersistentControl 
  (persist-state [this writer]
    (println "guardando estado"))
  (restore-state [this reader]
    (println "recuperando estado")))

El protocolo se define con defprotocol, indicando las funciones que forman parte de él, y se implementa con extend o extend-type (como en el caso del ejemplo) indicando el tipo y protocolo que queremos extender, y la implementación de las operaciones del protocolo.

La principal ventaja de esta forma de trabajar es que podemos decidir implementar protocolos sobre tipos que ya han sido definidos anteriormente sin necesidad de modificar la declaración de los tipos.

A cambio, vemos en el ejemplo una limitación de clojure (o de mi conocimiento de clojure) y es que no podemos hacer que un tipo herede de otro si usamos deftype. Eso hace que exista cierta duplicidad entre los constructores de Control, Button y TextBox. Se puede solventar utilizando clases de java, pero si vamos por esa vía empezamos a perder parte de la claridad y concisión de clojure.

Interfaces y extension methods

Si has llegado hasta aquí y no has abandonado al ver los paréntesis de clojure, seguramente te hayas dado cuenta de que los protocolos de clojure podríamos verlos como una mezcla entre interfaces (la definición del protocolo) y extension methods (la implementación del protocolo).

Es una buena forma de verlo, ya que la implementación del protocolo, al igual que los extension methods, sólo tiene acceso a las operaciones públicas del tipo sobre el que está implementando el protocolo.

La diferencia es que en C#, aunque podamos añadir los extension methods necesarios para implementar un interface, eso no va a hacer que la clase implemente el interface. Sería genial si pudiésemos hacer algo así (OJO, sintaxis inventada):

public static class PersistentTextBoxExtensions
{
  public static void IPersistentControl.PersistState(this TextBox textBox, IWriter writer) 
  {
    // Guardar estado
  }
  
  public static void IPersistentControl.RestoreState(this TextBox textBox, IReader reader) 
  {
    // Recuperar estado
  }
}

Desgraciadamente eso no es posible hoy en día, y debemos conformarnos con soluciones como la descrita anteriormente con los adapters o cruzar la línea de la generación dinámica de código y empezar a jugar con interceptores como en este tipo de soluciones.

Conclusiones

Los protocolos en clojure son una manera interesante de extender el comportamiento de tipos ya existentes para poder implementar algoritmos polimórficos. En C# (de momento) no existe una construcción parecida, pero eso no quiere decir que el problema no se pueda resolver, aunque las soluciones pueden no ser tan cómodas.

Nuevamente quiero dejar claro que esto no se trata de decidir qué lenguaje es mejor ya que para eso hay que tener en cuenta muchos factores (y algunos mucho más importantes que este tipo de cosas), sino de conocer otras formas de resolver los mismos problemas.

2 comentarios en “Interfaces + Extension Methods = Protocolos de Clojure

  1. Aunque se deduce de tu articulo me gustaria resaltar que puedes extender incluso los tipos predefinidos, como String u Object, con lo que consigues algo parecido al monkey patching de lenguajes como ruby o javascript. La diferencia es que la extension del protocolo no es global sino cualificada por el modulo donde se hace.
    Lo de la duplicacion de codigo, ¿es por repetir los nombres de los campos? No me parece un precio muy alto a cambio de mandar a paseo la herencia ;-)
    Como se declara en http://clojure.org/datatypes :
    «You should always program to protocols or interfaces»

  2. La duplicación de código iba por ahí, sí.

    Parte de la «esencia» de un control sería tener text y color, por lo que al definir nuevos controles te obliga a repetirlo si quieres hacerlo explícito.

    Una opción sería incluir esas «propiedades» en su propio protocolo. Otra, pasar por completo de definir explícitamente esas propiedades, usar records, y tratarlos como maps, pero me gusta la idea de tener un sitio al que acudir para ver qué cosas forman un (todos) los controles.

Comentarios cerrados.