24 de febrero de 2010

¡¡Polimorfismo, polimorfismo, polimorfismo!!

Quizás la característica más importante de los entornos orientados a objetos sea el poco comprendido y aún menos usado polimorfismo.

No me malinterpreten: la encapsulación es importante, pero ya COBOL y sus módulos podían esconder los datos mediante interfaces "bien definidas".

En un lenguaje orientado a objetos, la encapsulación no es sino aplicar los principios de modularidad hasta sus últimas consecuencias

Lo que es más, es probable —y muchas veces aún deseable— romper la encapsulación de un objeto cuando su único propósito es el trasporte de datos. Esto es tan común que incluso hay un patrón que lo documenta: DTO (Data Transfer Objects)

Sin embargo, el tema que quiero tocar el día de hoy es uno que consistentemente encuentro que los sistemas legacy pasan por alto: el polimorfismo.

Como una definición simple diremos que el polimorfismo es la capacidad para mandar un mismo mensaje a varios objetos de —potencialmente— distintos tipos y que estos procesen dicho mensaje de la forma más adecuada posible.

Por ejemplo, en Java o .NET podemos llamar al método ToString de cualquier objeto sin importar su tipo, y obtener una respuesta adecuada. Así pues, si el objeto es el Int32 13, obtendremos la cadena "13"; si el objeto es un registro de un sistema de crédito, tal vez obtengamos una cadena con el nombre del acreditado y su saldo al corte; si el objeto es un String, entonces obtendremos el mismo objeto como respuesta.

Como ven, cada objeto responde de manera distinta a la invocación de ToString, pero siempre de manera consistente con:

  1. Lo que se espera de dicho método (una representación de cadena del objeto en cuestión), y
  2. La naturaleza del objeto mismo (sus reglas de negocio, etc.)

Si el polimorfismo es tan maravilloso, ¿Porqué nadie lo usa?

Aquí quisiera hacer una aclaración. Como lo demuestra el ejemplo anterior, en realidad usamos el polimorfismo todo el tiempo. Sin embargo, como desarrolladores no hemos asimilado el pensamiento en objetos y seguimos anclados a las mismas técnicas y los mismos paradigmas de antaño. Nadie extraña lo que no ha tenido. Si nunca hemos aprendido a utilizar correctamente una herramienta y a dominarla, seguiremos naturalmente a la tendencia de usar aquellas que mejor conocemos y que nos han funcionado en el pasado.

Ahora, no quiero aquí discutir si las técnicas tradicionales funcionan o no. Es claro que funcionan puesto que se siguen usando. El punto es qué tan adecuadas son para la tarea que les hemos asignado.

Como comenté al principio del blog, algo que consistentemente veo en sistemas legacy (¡¡incluso algunos que se desarrollaron hace 2 meses!!) es la completa falta de polimorfismo en su diseño e implementación. ¿Cual es la alternativa escogida en su lugar? Series interminables de IFs y SELECT CASE/SWITCH regadas prácticamente por la totalidad de la aplicación. Algo parecido a esto:

public Money GetSaldo() {
  ...
  if (Cuenta.Tipo == C_CORRIENTE) {
    Interes = [calcular el interés de cuentas corrientes]
  } else if (Cuenta.Tipo == C_CHEQUES) {
    Interes = [calcular el interés de cuentas de cheques]
  } else {
    Interes = [calcular el interés de cuentas "comodity"]
  } 

  Saldo += Interes;

Al final, vemos listas interminables de condiciones que "checan" el tipo de cuenta para la aplicación de muy diversas reglas de negocio, diseminadas por todo el código sin un lugar bien definido para su definición y su aplicación.

Lo que un código como el anterior nos está diciendo prácticamente a gritos es que existen (por lo menos) tres tipos de cuentas y cada uno tiene comportamientos bien diferentes en contextos específicos.

Algunos podrán pensar ¡Ah! entonces hay que heredar tres subclases de Cuenta. Pues no, o por lo menos no necesariamente. Efectivamente, en lenguajes que la soportan, podemos usar herencia como nuestro vehículo para crear objetos polimórficos, sin embargo, algo que en su momento me costó trabajo comprender (probablemente por mis raizes de lenguajes fuertemente tipados) es que el polimorfismo puede operar de forma independiente a la herencia.

Si estamos trabajando en Java o .NET, es posible utilizar una interface. En C++ y Visual Basic 5 o 6, usamos una clase abstracta, es decir, una que solo contiene métodos virtuales sin código. En Smalltalk, Ruby, Python, Javascript, Objective-C y —sorpresa, sorpresa— de nuevo, Visual Basic 5, 6 o VBscript, etc., podemos enviar mensaje y dejar que el sistema de mensajería del lenguaje resuelva la invocación del método apropiado.

Regresando al ejemplo anterior, suponiendo que tenemos tres clases que implementan el cálculo del interés de sendos tipos de cuenta y que responden adecuadamente al mensaje GetInteres, podemos reescribirlo de la siguiente forma:

interface Rendimientos {
  Money GetInteres(Cuenta c);
}

class CuentaCorriente : Rendimientos {
  public Money GetInteres(Cuenta c) {
    return [calcular el interés de cuentas corrientes];
  }

class CuentaCheques : Rendimientos {
  public Money GetInteres(Cuenta c) {
    return [calcular el interés de cuentas de cheques];
  }

class CuentaSimple : Rendimientos {
  public Money GetInteres(Cuenta c) {
    return [calcular el interés de cuentas "comodity"];

public Money GetSaldo() {
  ...
  Saldo += rendimiento.GetInteres(this);

En este caso he optado por definir una interfaz llamada Rendimientos. Nótese que no he usado la nomenclatura usual agregando una "I" al principio, ya que si más adelante decido que hay código en común en las clases que la implementan, mismo que me gustaría consolidar, puedo convertir a Rendimientos en una clase abstracta sin demasiados problemas y sin preocuparme de tener ahora una nomenclatura inconsistente por un lado, o tener que cambiar todas las menciones de la misma por el otro.

Analicemos las ventajas y desventajas que tiene el enfoque alternativo:

  • Ventajas
    1. El código que hace uso del método polimórfico se simplifica considerablemente.
    2. Las clases y métodos resultantes de la descomposición son altamente cohesivos, virtud de lo cual
    3. Existe un lugar claro en el código para la definición de reglas de negocio relacionadas con las variantes identificadas.
    4. El código de los métodos es sencillo y por lo tanto más fácil de comprender.
    5. El código es más sencillo de probar, ya que solo maneja una variante a la vez.
  • Desventajas
    1. Es necesario crear un mayor número de clases.
    2. No se puede saber con solo observar el código cual método se ejecutará, ya que esto se resuelve en tiempo de ejecución.
    3. Requiere de un diseño más concienzudo que la inclusión ad hoc de sentencias IF "según se vaya ocupando".

Vuelvo a hacer énfasis en algo que he comentado varias veces. Algunas personas opinan que aparentemente, el uso de la programación orientada a objetos aumenta la complejidad del código. Esto es una falacia. Hay que distinguir entre "código simple" y "código simplón":

Código simple
Es código que expresa su intención de forma clara, aún si no se conocen todos los detalles que lo hacen funcionar.
Código simplón
Es código escrito con las mismas cinco instrucciones que aprendí en mi curso de Introducción a la Programación, sin importar cuan rebuscado pueda ser el resultado final.

Nadie se lo piensa dos veces al llamar ToString el su código o al concatenar una cadena con un número (usando implícitamente ToString). ¿Porqué entonces el crear mi propio código polimórfico es más complejo?

Teclear no es un sustituto para pensar.

Al descomponer esas largas y complejas rutinas en sub-rutinas, estamos aplicando "divide y vencerás". Al mover esas sub-rutinas a clases polimórficas, aplicamos "a cada quien lo suyo".

Con el tiempo, en lugar de tener unas pocas clases grandes y complejas, terminaremos con un montón de clases pequeñas y simples. De eso se trata la programación y muy en especial la programación orientada a objetos: De manejar la complejidad inherente de los sistemas. De ocultarla si es posible, no de incrementarla con programación descuidada y por debajo de lo que los usuarios esperan de nosotros.

Saludos.

2 comentarios:

  1. BUENO, PARA LUEGO HACER MANTENIBLE EN EL TIEMPO CON REDUCCIÓN DE TIEMPO EN EL PROCESO.

    ResponderEliminar
  2. Excelente explicación, mejor no se puede encontrar de lo que es y en lo que ayuda el polimorfismo, gracias!

    ResponderEliminar