martes, 11 de diciembre de 2012

Polimorfismo

En la programación orientada a objetos resulta muy frecuente escuchar o leer el término 'polimorfismo'. Voy a intentar explicarlo de una forma sencilla. Porque aunque la palabra puede resultar algo complicada de pronunciar, es mucho más fácil de entender.

El polimorfismo podríamos definirlo como la posibilidad que tiene una variable objeto definida en tiempo de compilación de un tipo determinado, de poder instanciarse en tiempo de ejecución como un objeto de distinto tipo pero que tiene el mismo interfaz que el tipo definido en compilación. (Entendiendo por interfaz el conjunto completo de peticiones (métodos) que se pueden enviar al objeto)

Suena un poco a trabalenguas, pero no os asustéis. Seguro que con este ejemplo queda más claro.

Supongamos la siguiente jerarquía de clases



Podemos ver como las clases 'Triangulo', 'Rectangulo' y 'Romboide' aun siendo clases diferentes con sus características peculiares, obviamente un triangulo no es igual que un rectángulo, todas ellas son figuras geométricas y por lo tanto extienden de la clase 'Figura', de la que heredan aquellos elementos comunes a todas ellas, en este caso los atributos 'base' y 'altura', y el métodos 'calcularArea', que nos devolverá el área de la figura geométrica en cuestión. Y son estos elementos los que definen el interfaz común.

Esto traducido en código Java quedaría de la siguiente manera (Aunque quiero dejar claro que lo menos importante es el código con su sintaxis, porque esto mismo nos lo encontraremos en otros lenguajes orientados a objetos con distinta sintaxis, lo más importante es el concepto)

package test;

public abstract class Figura {
    
    protected int base;
    protected int altura;
    
    abstract int calcularArea();
    
    public Figura(int base, int altura){
         this.base = base;
         this.altura = altura;
    }

}

El hecho de que la clase 'Figura' que define el interfaz común sea un clase abstracta no es relevante, podría ser igualmente una clase "normal" completamente implementada o incluso un interfaz (propiamente dicho) con los métodos declarados pero no implementados. Lo importante es que las distintas clases hijas compartan este mismo interfaz.

Las clases hijas quedarían implementadas de la siguiente manera

package test;

public class Rectangulo extends Figura {

    public Rectangulo(int base, int altura) {
         super(base, altura);
    }

    @Override
    int calcularArea() {
        return base*altura;
    }

}

package test;

public class Romboide extends Figura {

    public Romboide(int base, int altura) {
         super(base, altura);
    }

    @Override
    int calcularArea() {
        return base*altura;
    }

}

package test;

public class Triangulo extends Figura {

    public Triangulo(int base, int altura) {
         super(base, altura);
    }

    @Override
    int calcularArea() {
        return (base*altura)/2;
    }

}

Podemos observar como aún teniendo todas ellas el mismo interfaz, cada una lo implementa de forma distinta, puesto que para cada tipo de figura el área se calcula de manera diferente.

Ahora supongamos que queremos mostrar por pantalla el área de un rectángulo, la implementación sería sencilla.

public void escribirAreaRectangulo(Rectangulo rectangulo){
     int area = rectangulo.calcularArea();
     System.out.println("El area del triangulo es " + area);
}

Ahora supongamos que queremos hacer lo mismo para un triangulo, pues siguiendo con este planteamiento implementamos lo mismo pero para un triangulo (copy-paste y un par de cambios)

public void escribirAreaTriangulo(Triangulo triangulo){
     int area = triangulo.calcularArea();
     System.out.println("El area del triangulo es " + area);
}

Y si ahora, siguiendo esta lógica queremos hacer lo mismo para un romboide, pues más de lo mismo, otro copy-paste y listo. Y así podríamos seguir indefinidamente añadiendo más y más código sin ningún problema.

¿Compila? Sí. ¿Funciona? También. Pero desde el punto de vista técnico de la ingeniería del software esto es una chapuza descomunal. Cuando se desarrolla software si es cierto que en última instancia lo más importante es que éste funcione y cumpla con unos requisitos establecidos, pero no es lo único importante, también es muy importante cómo se hace, como se implementa, el fin no justifica los medios, no todo vale con tal de que la aplicación funcione. Esto es algo que tristemente no se suele tener en cuenta y al final, más tarde o más temprano, suele pasar facturar con resultados lamentables.

Y es en este punto donde surge el polimorfismo como solución a nuestro problema. Nos aprovechamos de que todas las clases comparten el mismo interfaz, para declarar una variable del tipo común ('Figura') y ejecutar sobre ella cualquiera de los métodos definidos en su interfaz, de manera que en tiempo de ejecución esta variable "tomara la forma" de un objeto instanciado de cualquiera de las clases y sobre él se ejecutará la implementación correspondiente a su clase.

public void escribirArea(Figura figura){
     int area = figura.calcularArea();
     System.out.println("El area del triangulo es " + area);
}

De esta forma la variable 'figura' puede ser en tiempo de ejecución tanto un rectángulo, como un romboide o un triangulo, puesto que todas ellas son figuras e implementan el mismo interfaz, en este caso al método 'calcularArea()', aunque cada una de distinta manera dependiendo de su naturaleza.

Este es un ejemplo sencillo para entender qué es el polimorfismo. Su uso puede llegar a ser más complejo, como en los patrones de diseño, pero el concepto es el mismo.

domingo, 9 de diciembre de 2012

Agregación y asociación

Estas son dos formas de relacionar objetos que muchas veces se confunden pero que son bien diferentes.
  • Agregación 
Este tipo de relación entre objetos implica que un objeto posee a otro o que es responsable de él. Normalmente decimos que un objeto tiene a otro o que un objeto es parte de otro.

Las relaciones de agregación tienden a ser menos y más permanentes que las de asociación.

En este tipo de relación podemos distinguir dos modalidades:
  1. Agregación simple
  2. Composición
En el caso de la agregación simple aunque hay una relación de pertenencia, del todo y la parte, la existencia de los distintos elementos que forman parte o componen el todo no está ligada a la existencia de éste. Es decir, los elementos agregados pueden existir sin el objeto del que forman parte.

La forma de representar está relación con UML es por medio de una línea que une el todo con la parte, añadiendo al extremo del todo un rombo vacío.

Sin embargo en el caso de la composición la existencia de las partes dependen directamente de la existencia del todo que las contiene. De forma que si el todo deja de existir también lo hacen sus partes.

La forma de modelar este tipo de agregación con UML es de la misma forma que la agregación simple, pero con el rombo oscuro.

En el siguiente diagrama podemos ver un ejemplo de ambas relaciones:


En este caso podemos ver como un 'Alumno' forma parte tanto de una 'Universidad' como de una 'Asignatura'. Siendo la primera una relación de composición y la segunda de agregación simple. La diferencia conceptual esta en la relación de dependencia existencial que existe entre ellas. Si la 'Universidad' deja de existir también dejarían de existir las entidades de tipo 'Alumno', porque los alumnos necesariamente tienen que forma parte de una 'Universidad', mientras que si deja de existir una 'Asignatura' no tienen porque dejar de existir los elementos 'Alumno' vinculados a ella, porque aunque dejen de forma parte de esa 'Asignatura' formarán parte de otras, o incluso en el peor de los casos de ninguna pero sí seguirán formando parte de la 'Universidad'.
  • Asociación
En este tipo de relación un objeto simplemente conoce a otro, es decir, es una relación de uso. Los objetos relacionados de esta manera pueden pedirse operaciones entre sí, pero no son responsables unos de otros. Es una relación más débil que la agregación y representa mucho menor acoplamiento entre objetos.

Las relaciones de asociación se hacen y deshacen con mucha más frecuencia que las relaciones de agregación, y algunas veces sólo existen mientras dura una operación.

En el caso de la asociación la forma de modelar este tipo de relación con UML es simplemente con una línea uniendo los objetos asociados, aunque a veces esta linea es una flecha dirigida desde la clase que usa a la clase usada por éste.


En este ejemplo vemos como el 'Pizzero' usa la 'Pizza', pero es sólo una relación de uso no de composición. La existencia de uno no está ligada a la existencia del otro. Si el 'Pizzero' desaparece la 'Pizza' sigue existiendo, y viceversa.

sábado, 8 de diciembre de 2012

Herencia vs. Composición

Cuando se desarrolla software, entendiendo por desarrollo no sólo la implementación sino también las fases previas de análisis y diseño, uno de los objetivos perseguidos, o por lo menos así debería ser, es conseguir la máxima capacidad de reutilización de las distintas funcionalidades implementadas, y por lo tanto también del código.

En los entornos de programación orientados a objetos (p.ej. Java) existen básicamente dos formas de conseguir esto:

Mediante herencia de clases
Esta manera de desarrollar enfocada a la reutilización también es conocida como 'reutilización de caja blanca'. Este término se refiere a la visibilidad, porque en la herencia las interioridades de las clases padres suelen hacerse visibles a las subclases para poder definir una implementación en términos de otra.

La forma de representar la herencia en UML sería mediante una flecha (con la punta triangular vacia) dirigida desde la clase hija (subclase) a la clase padre (superclase).


En este caso, las subclases de la clase 'Mamifero' serían las clases: 'Perro', 'Tigre' y 'Oveja'. De forma que las clases hijo heredarían la interfaz e implementación de la clase padre 'Mamifero'
  • Ventajas
Se define estáticamente en tiempo de compilación, y no en tiempo de ejecución. Por otro lado al estar permitido por el lenguaje de programación resulta sencilla de usar e implementar. Y también hace que sea más fácil modificar la implementación que esta siendo reutilizada
  • Inconvenientes
Sin embargo, precisamente porque se define en tiempo de compilación, no se pueden cambiar las implementaciones heredadas de las clases padre en tiempo de ejecución. Así mismo también rompe la encapsulación al exponer a una subclase los detalles de implementación de su padre, de forma que cualquier cambio en la implementación del padre obligará a cambiar la subclase. Estas dependencias de implementación también pueden causar problemas al tratar de reutilizar una subclase. Cuando algún aspecto de la implementación heredada no sea apropiada para nuevos dominios, la clase padre deberá ser escrita de nuevo o reemplazada por otra más adecuada. Esto limita la flexibilidad y la reutilización. Una solución a esto es heredar sólo de clases abstractas, ya que éstas normalmente tienen poca o ninguna implementación.

Mediante composición  
Esta forma de desarrollar enfocada a la reutilización también es conocida como 'reutilización de caja negra', porque los detalles internos de los objetos no son visibles y la nueva funcionalidad se obtiene ensamblando o componiendo objetos para obtener una funcionalidad más compleja.

La forma de representar esta relación entre clases con UML es mediante una línea con un rombo relleno en el extremo de la clase que representa el "todo" y por lo tanto contiene a las "partes" de las que esta compuesto.


En este ejemplo la clase 'Cama' esta compuesta por las clases: 'Patas', 'Colchon' y 'Somier'. De forma que la existencia de las partes está necesariamente ligada a la existencia del todo que las contiene, es decir, que si la clase 'Cama' deja de existir también lo harán las clases que la componen.
  • Ventajas
Al contrario que la herencia la composición se define en tiempo de ejecución, y no en tiempo de compilación, a través de objetos que tienen referencias a otros objetos. Así mismo puesto que a los objetos se accede sólo a través de sus interfaces no se rompe la encapsulación. Cualquier objeto puede ser reemplazado por otro en tiempo de ejecución siempre que sea del mismo tipo. También ayuda a mantener cada clase encapsulada y centrada en una sola tarea. De esta forma nuestras clases y jerarquías de clases permanecerán pequeñas y más manejables. Y al haber mas objetos por tener menos clases, el comportamiento del sistema dependerá de las relaciones entre estos objetos en vez de estar definido en una clase.

Con esto podemos concluir que hay que favorecer la composición de objetos frente a la herencia de clases

Idealmente sólo crearíamos nuevos componentes para lograr la reutilización. Deberíamos ser capaces de conseguir toda la funcionalidad que necesitásemos simplemente ensamblando componentes existentes a través de la composición de objetos. Sin embargo, rara vez es éste el caso, puesto que el conjunto de componentes disponibles nunca es, en la práctica, lo suficientemente rico. Reutilizar mediante la herencia hace más fácil construir nuevos componentes que puedan ser combinados con los antiguos. Y de esta forma la herencia y la composición trabajan por lo tanto juntas.