Programación Avanzada

Patrones de diseño

Patrón de diseño Decorador (Decorator)

Introducción

La herencia extiende el comportamiento de una nueva clase hija ligándola fuertemente con su clase madre.

A veces, necesitamos un mecanismo más flexible que la herencia para ampliar el comportamiento de una clase. De hecho, se recomienda utilizar la composición frente a la herencia.

El patrón de diseño Decorador nos ofrece una solución para añadir nueva funcionalidad a una clase, más flexible que la herencia, y de manera dinámica.

Bibliografía

Contenidos

  1. Tengo que renovar mi vehículo.
  2. Programemos la solución.
  3. Definición del patrón de diseño Decorador.
  4. El patrón Decorador en el paquete estándar de Java.
  5. Resumen.
  6. Recursos en Internet.

Tengo que renovar mi vehículo

Mi coche pasa más tiempo en el taller que en la carretera. Consume más combustible que un avión y en verano me achicharro cuando voy a la playa

Es hora de cambiar de coche.

Me acerco a un concesionario y empiezan las dudas.

Tengo que renovar mi vehículo

  • Quiero comprar un coche.
  • Un utilitario, familiar, monovolumen,...
  • Bueno... un utilitario.
  • ¿Qué color?
  • Rojo.
  • ¿Lo quiere con pintura metalizada?
  • Si
  • ¿Aire acondicionada?
  • Si, si.
  • ¿Techo solar?
  • No.

Programemos la solución

Armado de lápiz y papel el vendedor calcula:

  • El precio se le queda en: 15.000 del vehículo más 600 del aire acondicionado y 600 de la pintura metalizada... 16.200 euros precio final.
  • ¿Y cuanto vale un familiar?
  • 19.000 euros.
  • Si le ponemos aire acondicionado y techo solar, pero sin pintura metalizada.
  • Veamos, en ese caso tenemos los 19.000 del familiar más los 600 del aire acondicionado más 1.000 del techo solar, en total 20.600 euros.

Programemos la solución

  • ¿Me permite una pregunta?
  • Cómo no.
  • ¿Siempre calcula el precio a mano?
  • ¿!!?
  • Es que soy informático y me parece que se puede mejorar.
  • Si me hace el programa le hago un descuento del 50%
  • ... ok, hablemos

Programemos la solución

50% es mucha pasta, pongámonos manos a la obra.

Analizando el problema tenemos:

  1. Hay varios tipos de vehículos: Utilitario, Familiar, Monovolumen,...
  2. Cada uno de ellos puede tener uno o más accesorios.
  3. El resultado final es independiente del orden en el que se añadan los accesorios: aire acondicionado, pintura metalizada, techo solar, etcétera.
  4. El precio final se ha de calcular de modo automático.
  5. Tendré una descripción del vehículo final dependiendo de los accesorios que haya añadido.

Programemos la solución

Primera idea.

Escribo una clase para cada vehículo y luego creo clases hijas de cada uno de los vehículos añadiendo los extras.

Por ejemplo, tendré: Utilitario como clase madre, y después una clase hija que sea UtilitarioAireAcondicionado, otra que sea UtilitarioPinturaMetalizada,...

Y si quiero un Utilitario con aire acondicionado y pintura metalizada tendré UtilitarioAireAcondicionadoPinturaMetalizada,... uy!, esto no va por buen camino.

Tengo una explosión de clases.

Programemos la solución

Vale, pues puedo tener una clase para cada vehículo, y dentro de cada uno de ellos una array con los extras que le he añadido.

Así calcularé el precio final sumando el precio de los extras y añadiéndoselo al precio del vehículo.

Y para la descripción tengo algo parecido.

Parece mejor solución.

Pero también podría añadir esos extras a cualquier otro tipo de objeto, y podría tener una maceta con aire acondicionado, porque no hay una relación semántica entre el extra y el objeto sobre el que se aplica.

Programemos la solución

Parece que nos acercamos.

Lo que quiero es que cada extra, de alguna manera, recubra al vehículo original añadiéndole una nueva característica, de manera que un vehículo con aire acondicionado siga siendo un vehículo, y no sólo un vehículo con un agregado de extras.

Programemos la solución

Empecemos a programar, a ver si se nos aclaran las ideas:

public class Utilitario {
    private float precio;

    public Utilitario(float precio) {
        this.precio = precio;
    }
    public float getPrecio() {
        return precio;
    }
    public String descripcion() {
        return "Vehículo utilitario";
    }
}

En esta clase hemos hecho una importante decisión de diseño. ¿La ves?

Programemos la solución

Vamos con el código del Familiar:

public class Familiar {
    private float precio;

    public Familiar(float precio) {
        this.precio = precio;
    }
    public float getPrecio() {
        return precio;
    }
    public String descripcion() {
        return "Vehículo familiar";
    }
}

Debes tener todas las alarmas del sabueso programador encendidas.

Programemos la solución

Sí, efectivamente, mucho código duplicado. Extraigámoslo a una clase madre.

public abstract class Vehiculo {
    private float precio;

    public Vehiculo(float precio) {
        this.precio = precio;
    }

    public float getPrecio() {
        return precio;
    }

    public abstract String descripcion();
}

Y los vehículos Utilitario y el Familiar nos quedan como:

Programemos la solución

public class Utilitario extends Vehiculo {
    public Utilitario(float precio) {
        super(precio);
    }
    @Override
    public String descripcion() {
        return "Vehículo utilitario";
    }
}
public class Familiar extends Vehiculo {
    public Familiar(float precio) {
        super(precio);
    }
    @Override
    public String descripcion() {
        return "Vehículo familiar";
    }
}

Programemos la solución

Viene la magia: queremos que aire acondicionado recubra a Vehículo sin dejar de ser un Vehículo.

public class ConAireAcondicionado extends Vehiculo {
    private Vehiculo vehiculo;

    public ConAireAcondicionado(Vehiculo vehiculo, float precioExtra) {
        super(precioExtra);
        this.vehiculo = vehiculo;
    }
    @Override
    public String descripcion() {
        return vehiculo.descripcion() + ", con aire acondicionado";
    }
    @Override
    public float getPrecio() {
        return vehiculo.getPrecio() + super.getPrecio();
    }
}

Programemos la solución

Veámoslo con un poco más de detalle:

public class ConAireAcondicionado extends Vehiculo { //Sigue siendo
                                                     // un Vehiculo
    private Vehiculo vehiculo; //Aquí se recubre al vehículo original

    public ConAireAcondicionado(Vehiculo vehiculo, float precioExtra) {
        super(precioExtra); //La clase madre guarda el precio del extra
        this.vehiculo = vehiculo; //Nos quedamos con una referencia
                                  // al vehículo que decoramos.
    }

Programemos la solución

Sumamos a la descripción la característica que añade el extra.

@Override
public String descripcion() {
    return vehiculo.descripcion() +    // Descripción del vehículo que recubro
           ", con aire acondicionado"; // Mi descripción como extra
}

Añadimos al precio del vehículo, el precio del extra que está guardado en la clase madre.

@Override
public float getPrecio() {
    return vehiculo.getPrecio() + // Precio de vehículo que recubro.
           super.getPrecio();     // Mi precio como extra, lo guarda la clase madre.
}

Programemos la solución

No puedo esperar más necesito un test que valide la idea:

@Test
public void testUtilitario() {
    Vehiculo vehiculo = new Utilitario(15000); //Precio del vehiculo
    vehiculo = new ConAireAcondicionado(vehiculo, 600); //AC
    assertThat(vehiculo.getPrecio(), is(15600.0f)); //PASA!!!
}

Bien!, la idea es buena. Definamos otro extra.

Programemos la solución

public class ConPinturaMetalizada extends Vehiculo {
    private Vehiculo vehiculo;

    public ConPinturaMetalizada(Vehiculo vehiculo, float precioExtra) {
        super(precioExtra);
        this.vehiculo = vehiculo;
    }
    @Override
    public String descripcion() {
        return vehiculo.descripcion() + ", con pintura metalizada";
    }
    @Override
    public float getPrecio() {
        return vehiculo.getPrecio() + super.getPrecio();
    }
}

Para!, mucho código duplicado con ConAireAcondicionado.

Programemos la solución

Conocemos la técnica para eliminarlo, ahí va la superclase de los extras.

public abstract class Extra extends Vehiculo {
    private Vehiculo vehiculo;

    public Extra(Vehiculo vehiculo, float precioExtra) {
        super(precioExtra);
        this.vehiculo = vehiculo;
    }
    @Override
    public float getPrecio() {
        return vehiculo.getPrecio() + super.getPrecio();
    }
    @Override
    public String descripcion() {
        return vehiculo.descripcion();
    }
}

Programemos la solución

public class ConAireAcondicionado extends Extra {
    public ConAireAcondicionado(Vehiculo vehiculo, float precioExtra) {
        super(vehiculo, precioExtra);
    }
    @Override
    public String descripcion() {
        return super.descripcion() + ", con aire acondicionado";
    }
}
public class ConPinturaMetalizada extends Extra {
    public ConPinturaMetalizada(Vehiculo vehiculo, float precioExtra) {
        super(vehiculo, precioExtra);
    }
    @Override
    public String descripcion() {
        return super.descripcion() + ", con pintura metalizada";
    }
}

Programemos la solución

Maravilloso, el test sigue pasando, y si no, toca arreglar el problema hasta que vuelva a pasar. ¿Te das cuenta lo útiles que son?

Probemos un nuevo test:

@Test
public void testUtilitario() {
    Vehiculo vehiculo = new Utilitario(15000);
    vehiculo = new ConAireAcondicionado(vehiculo, 600);
    assertThat(vehiculo.getPrecio(), is(15600.0f));
    vehiculo = new ConPinturaMetalizada(vehiculo, 600);
    assertThat(vehiculo.getPrecio(), is(16200.0f));
}

BIEN!, PASA!!!

Programemos la solución

Estamos lanzados, probemos con un Familiar con AireAcondicionado y TechoSolar:

@Test
public void testFamiliar() {
    Vehiculo vehiculo = new Familiar(19000);
    vehiculo = new ConAireAcondicionado(vehiculo, 600);
    assertThat(vehiculo.getPrecio(), is(19600.0f));
    vehiculo = new ConTechoSolar(vehiculo, 1000);
    assertThat(vehiculo.getPrecio(), is(20600.0f));
}

También pasa!!!

Programemos la solución

Y si escribimos la clase para el Monovolumen, le podremos aplicar cualquiera de los extras sin problemas.

Creo que nos hemos ganado ese 50% de descuento.

Definición del patrón de diseño Decorador

Asigna responsabilidades adicionales a un objeto dinámicamente, proporcionando una alternativa flexible a la herencia para extender la funcionalidad.
Patrones de Diseño
Erich Gamma et al.

Definición del patrón de diseño Decorador

Este sería el modelo UML general del Patrón Decorador:

Definición del patrón de diseño Decorador

En nuestro caso concreto:

Definición del patrón de diseño Decorador

Ventajas:

  1. Diseño más flexible que utilizando herencia.
  2. La funcionalidad se va añadiendo según la vamos necesitando.

Desventajas:

  1. Desde el punto de vista de la igualdad de objetos, un objeto recubierto y el objeto original no se pueden comparar.
  2. Si creamos muchos recubridores, puede dar lugar a una explosión de clases.

Definición del patrón de diseño Decorador

El patrón de diseño decorador pertenece a la familia de los patrones Estructurales.

El patrón de diseño Decorador nos permite añadir nuevas características a los objetos en tiempo de ejecución.

Lo que hemos encapsulado dentro de nuevas clases (los decoradores), es la sobrecarga de los métodos de la clase madre.

El patrón Decorador en el paquete estándar de Java

El ejemplo arquetípico en Java es el paquete de entrada/salida.

Java define un gran número de clases en java.io para hacer tareas de entrada salida.

En la jerarquía de clases definidas, unas recubren a otras siguiendo el patrón de diseño Decorador de tal modo que se van añadiendo nuevas funcionalidad a la clase original con cada nuevo recubrimiento.

El patrón Decorador en el paquete estándar de Java

En la primera versión de Java, el teclado era una fuente de bytes de datos, no de caracteres.

Normalmente, lo que queremos leer desde el teclado son líneas de caracteres, no caracteres individuales.

Luego de algún modo tenemos que convertir los bytes a caracteres y de estos a líneas de caracteres.

Un posible modo de conseguirlo es utilizar las clases del paquete java.io

El patrón Decorador en el paquete estándar de Java

InputStream is = System.in; //System.in es el teclado
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
String texto = br.readLine();

El patrón Decorador en el paquete estándar de Java

Otro ejemplo es la serialización de objetos a fichero que hemos visto en prácticas.

FileOutputStream fos = new FileOutputStream("fichero.bin");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(new Particular());
oos.close();

FileOutputStream me permite escribir flujos de bytes a un fichero.

ObjectOutputStream recubre a un FileOutputStream y, además de bytes me permite enviar objetos, que se pueden serializar, a fichero.

El patrón Decorador en el paquete estándar de Java

Para el caso de lectura de objetos desde fichero, deserialización:

FileInputStream fis = new FileInputStream("fichero.bin");
ObjectInputStream ois = new ObjectInputStream(fis);
Particular particular = (Particular)ois.readObject();
ois.close();

FileInputStream me permite leer flujos de bytes desde un fichero.

ObjectInputStream recubre a un FileInputStream y, además de bytes me permite leer objetos serializados desde fichero.

Resumen

El patrón de diseño Decorador pertenece a la familia de patrones estructurales.

El patrón de diseño Decorador nos permite componer nuevos objetos a partir de objetos ya existentes sin utilizar directamente la herencia.

La solución es muy flexible ya que se pueden decorar familias de objetos.

Es una alternativa más flexible que la herencia.

De alguna manera, lo que hemos encapsulado es la sobrecarga de métodos, para que resulte más flexible.

Recursos en Internet