Interfaces y delegados

Hablaba esta semana en twitter con Nicolás Herrera y Ernesto Cárdenas sobre este artículo de la MSDN acerca del uso de delegados o interfaces.

En su favor hay que decir que el artículo tiene pinta de tener unos cuantos años (yo diría que alrededor de 10), pero independientemente de eso, la argumentación que da sobre cuándo usar interfaces y cuando usar delegados es muy discutible.

Por poner sólo un par de ejemplos:

Según el artículo, es mejor usar un interface cuando la clase que utiliza el interfaz quiera hacer un cast de ese interfaz a otro interfaz u otra clase. Eso se llama violar el principio de sustitución de Liskov y anula la primera utilidad de un interfaz, que es aislar al consumidor del interfaz de la implementación concreta que está utilizando.

También sugiere utilizar IComparable como base de un algoritmo de ordenación, puesto que la habilidad de comparar pertenece a la clase comparada. Bueno, puede que la clase tenga un “orden natural” que tenga sentido que pertenezca a ella, pero excepto casos muy concretos (números, strings y poco más), es habitual querer ordenar de distintas formas en función de la situación y acopar la clase a la condición de comparación a la clase comparada no parece una buena idea.

En cualquier caso, el objetivo del post no es tanto criticar un artículo de hace 10 años sino, como decía Nicolás, escribir sobre un tema del que no hay demasiada documentación en castellano.

Inyección dinámica de comportamiento

Los interfaces y los delegados tienen un objetivo fundamental dentro de un lenguaje como C# (o Java, C++ y demás amigos): nos permiten inyectar comportamiento de forma dinámica dentro de un algoritmo (en el sentido más amplio de la palabra algoritmo).

En un lenguaje orientado a objetos es habitual tener varios objetos colaborando entre si para implementar un algoritmo concreto. En su forma más básica, todos estos objetos que colaboran entre si conocen los tipos concretos de sus colaboradores, por lo que el comportamiento está fijado de antemano (salvando la herencia entre clases). No podemos modificarlo sin modificar los tipos que estamos usando.

Cuando cambiamos el tipo de las dependencias y, en lugar de usar un tipo concreto, usamos un interfaz o un delegado, estamos relajando los requisitos sobre la dependencia. Ya no fijamos que la dependencia sea un objeto de tipo FileWriter, sino que nos vale con cualquier cosa que se parezca a un IWriter, o incluso cualquier cosa que nos permita invocar un void Write(string message).

Nota sobre herencia entre clases: aunque en el post hable de interfaces, casi todo es aplicable a herencia entre clases. En lenguajes como C# y Java se suelen usar interfaces porque no existe herencia múltiple (entre otros motivos), pero en C++ estaríamos hablando siempre de clases (ya que no existen interfaces como un concepto independiente).

Cohesión y acoplamiento

Teniendo en cuenta lo que acabamos de ver, la decisión entre utilizar un interfaz o un delegado está más relacionada con los conceptos de cohesión y acoplamiento que con otra cosa.

Existen algunos motivos puramente técnicos que nos pueden llevar a utilizar una cosa u otra. Por ejempo los delegados son necesarios si queremos (y ojo, que no siempre queremos) utilizar los eventos de C#, o podemos necesitar interfaces si estamos usando un contenedor de inversión de control que no permite trabajar con delegados, pero si nos centramos en el aspecto conceptual, la decisión, como decía, está más ligada a la cohesión y acoplamiento.

Un interfaz nos permite “empaquetar” un conjunto de operaciones. Las estamos acoplando entre ellas, pero también estamos aumentando su cohesión. Si tengo una clase que necesita “algo” capaz de escribir string, int y float, podríamos inyectarle un interfaz con tres métodos o tres delegados. En este caso, probablemente tenga más sentido inyectar un único interfaz porque las implementaciones estarán relacionadas (es raro que queramos escribir el string a un sitio y el int a otro distinto). Además si en el futuro necesitamos escribir un decimal, es hasta probable que ese interfaz ya nos ofrezca la posibilidad.

En el otro extremo, tenemos el problema de depender de interfaces muy grandes cuyas implementaciones tienen a su vez muchas dependencias, haciendo que el acoplamiento aumente mucho, acabemos no respetando el principio de segregación de interfaces y paguemos sus consecuencias por otro lado.

Los delegados, en cambio, ofrecen mucha más flexibilidad. En el fondo, si queremos centrarnos en el paradigma orientado a objetos más puro, podemos ver un delegado como un interface con un sólo método (de hecho en Java los llaman interfaces funcionales). En realidad en .NET tienen alguna característica más, como que se pueden capturar varias implementaciones en un único delegado (multicast delegates), pero para nuestra discusión no son demasiado importantes.

Al ser “interfaces de un sólo método”, los delegados están mucho más focalizados y depender de ellos hace que dependas sólo de lo que realmente necesitas, con lo que el acoplamiento entre el usuario del delegado y la implementación es mínimo, y además no se produce un acoplamiento artificial a la hora de implementar varios delegados sólo porque alguien decidió que formaban parte de un único interfaz. Representan el principio de segregación de interfaces llevado al extremo.

Una ventaja adicional, y para mi muy importante, es que podemos definir “implementaciones” de un delegado a posteriori. Imagina que tienes este interfaz:

public interface ILog
{
  void Write(string msg, params object[] args);
}

Si queremos que una clase implemente ILog, tenemos que decidirlo en el momento de crear esa clase. Si no tenemos control sobre esa clase, habra que recurrir a algún patrón aburrido (adapter, brigde, decorator). Si en lugar de un interfaz hubiésemos usando un delegado:

public delegate void WriteLog(string msg, params object[] args);

Podríamos hacer que cualquier método con esa signatura actuase como “implementación”, aunque no estuviera previsto inicialmente. Podríamos usar Console.WriteLine, o log4net.ILog.InfoFormat o lo que queramos. Tenemos mucha más flexilibidad.

Cuando usamos delegados podemos incluso recurrir a los delegados que ya existen en el framework, los Action<T>, Func<T>, que nos ahorra definir nuestros propios tipos y hace que tengamos un código aún más genérico.

Una limitación de los delegados es que si una clase (o método) necesita usar varios, resulta más confuso (e incómodo) pasárselos que usar un interface que los agrupe. Una opción para mitigar este problema es el uso de parameter objects. Tenéis un buen ejemplo de esto en este post sobre estrategias dinámicas en C#.

Nombres y herramientas

Hasta ahora, parece que los delegados van ganando a los interfaces, y lo cierto es que últimamente tiendo a inclinarme más por el uso de delegados, especialmente en sus versiones más genéricas (Action y Func) que por el uso de interfaces, pero aparte de la cohesión y acoplamiento, hay otros parámetros en los que los interfaces aportan cosas interesantes.

Los interfaces nos ayudan a dotar de valor semántico a las operaciones, sobre todo si los comparamos con delegados genéricos. Este valor semántico se refleja en algo tan básico como tener que ponerle un nombre a una operación o un conjunto de operaciones.

Al poner ese nombre, estamos incrementando la información que comunicamos sobre ellas. Por ejemplo, no es lo mismo decir que un método recibe un Action<string> que un ILogger. Aunque en ambos casos sea más o menos lo mismo (un método que nos permite pasarle un string), con ILogger podemos ir más allá de la estructura y comunicar intención: le vamos a pasar un string para que lo añada a un log, no para que lo envíe por email, valide que tiene más de 12 caracteres o verifique que es un palíndromo.

Esto también hace que no podamos usar cualquier método con esa signatura, sino que tiene que ser un método que originalmente esté diseñado para ello. Sí, eso es lo mismo que antes veíamos como una ventaja, pero a veces también puede ser un inconveniente porque puede generar confusión con respecto a si el método que usamos como delegado se ajusta mínimamente a lo que necesitamos o no. Es cierto que el hecho de implementar un interfaz tampoco es una garantía demasiado fuerte, pero al menos podemos asumir que si alguien se molestó en implementar el interfaz era porque creía que esa clase era apta para cubrir esa necesidad.

Desde el punto de vista de herramientas, el uso de interfaces también tiene algunas ventajas con respecto a los delegados. Esto se manifiesta cuando usas un IDE potente, como suele ser el caso cuando se trabaja con lenguajes tan pesados como C# o Java.

Con un interfaz es más fácil localizar todas las implementaciones del interfaz, navegar hasta una implementación concreta (o incluso hasta un método concreto de una implementación concreta) y, en general, el IDE tiene más metainformación para facilitarnos ciertas cosas.

En el caso de delegados esto es más complicado porque al definir un método no indicamos “este método puede funcionar como los siguientes delegados”, sino que es al definir un delegado cuando, “de repente”, hay un montón de métodos cuya signatura se ajusta a él (sin contar posibles lambdas). Navegar entre posibles implementaciones es más complicado, el análisis estático que se puede realizar es más limitado y perdemos parte de las herramientas más potentes del IDE.

Conclusión

Tanto interfaces como delegados (o funciones) resuelven problemas similares. Nos permiten inyectar comportamiento de forma dinámica en un algoritmo. Eso implica que su uso puede solaparse en muchos escenarios, pero cada uno presenta ciertas ventajas sobre el otro.

Los interfaces pueden ayudar a aumentar la cohesión y hacer el código más fácil de navegar y descubrir. También introducen un mayor acoplamiento y reducen la flexibilidad del código porque nos obligan a decidir de antemano si una clase implementará un interfaz o no.

Con los delegados ganamos en flexibilidad y dinamismo. Podemos componer delegados al vuelo usando métodos de clases que originalmente no estaban diseñadas para ello. A cambio, abusar de esta flexibilidad puede hacer que nuestro código resulte algo más complejo de comprender.

La solución, como siempre, es comprender las ventajas de cada técnica y aplicarlas en función de las necesidades que tengamos en cada momento.


Un comentario en “Interfaces y delegados

  1. Pingback: [ASP.NET] Simplificando los controladores | Blog de Nicoloco

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *

*

Puedes usar las siguientes etiquetas y atributos HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>