Muchos os podéis preguntar que qué sentido tiene esto, si precisamente la herencia nos da la opción de añadir funcionalidad a casos bases para extenderlos. La respuesta es sencilla: La herencia hace nuestro código extensible pero a la vez introduce complejidad y en ocasiones viola el principio Abierto/Cerrado de los fundamentos SOLID de diseño orientado a objetos.
En la práctica esto se traduce a que la herencia nos hace crear clases estáticas en nuestro código, y para cada nueva funcionalidad que deseemos añadir estamos obligados a crear una nueva clase que cargue con todo el código de su padre. El patrón Decorator viene a solucionar este problema de manera que podamos añadir funcionalidad a nuestro código en tiempo de ejecución y extender dicha funcionalidad sin necesidad de crear clases más complejas.
Vamos a revisar la plantilla y a ver un ejemplo para poder entender mejor su funcionamiento:
Decorator
Nombre: Decorator
Problema y Contexto:
Nuestro sistema requiere que la funcionalidad de ciertos componentes pueda extenderse y modificarse en tiempo de ejecución. Además pretendemos que esta característica no se implemente mediante herencia para poder aprovechar al máximo las clases existentes sin introducir jerarquías complejas y extensas.
Se aplica cuando:
- Necesitamos añadir responsabilidades a objetos individuales de forma dinámica y transparente
- Se pueden revocar responsabilidades antes asignadas a nuestros objetos.
- La extensión mediante herencia viola los principios SOLID.
- Necesitamos extender la funcionalidad de una clase pero la herencia no es una solución viable.
- Necesitamos extender la funcionalidad de un objeto en tiempo de ejecución e incluso eliminarla si fuera necesario.
Solución y Estructura:
Crearemos un nuevo peldaño en la jerarquía de clases llamado Decorator que encapsulará las nuevas responsabilidades. Esta nueva clase se encarga de redirigir las peticiones al componente original admás de permitir la modificación de ciertos aspectos antes y después de dicha redirección. Con esta estructura, podremos añadir nuevas funcionalidades con forma de decoradores de manera recursiva.
La estructura es la siguiente:
Donde:
Component: Clase abstracta o interfaz que deben poseer los objetos que necesiten ser 'decorados'.
ConcreteComponent: Componente real al cual se la pueden añadir responsabilidades adicionales.
Decorator: Se trata de una clase abstracta que contiene una referencia al componente asociado. Debe implementar a la interfaz Componente, en la que delegará la funcionalidad real.
ConcreteDecorator: Elemento real que añadirá las funcionalidades y/o responsabilidades al componente objetivo.
Consecuencias
- POSITIVAS:
- Es más flexible que la herencia.
- Permite añadir y eliminar responsabilidades en tiempo de ejecución.
- Evita la herencia con muchas clases y la herencia múltiple.
- Limita la responsabilidad de los componentes para evitar clases con excesivas responsabilidades en los niveles superiores de la jerarquía.
- NEGATIVAS:
- Genera gran cantidad de objetos pequeños.
- Puede haber problemas con la identidad de los objetos, ya que los decoradores son 'envoltorios' que se comportan como el objeto original, pero la referencia no es la misma; no deberíamo apoyarnos en la identidad a la hora de trabajar con este patrón.
Patrones Relacionados: Adapter, Composite y Strategy.
Ejemplo:
Para digerir esto sin que nos ocasione una gastroenteritis, vamos a ver un ejemplo práctico de como funciona. Supongamos que nos encargan construir un buscador de hoteles en el que debemos ofrecer un presupuesto según las características que se seleccionen. Además de hoteles, se preveé que en el futuro se oferten diferentes tipos de alojamientos, como apartamentos u apartahoteles.
Comenzaremos definiendo nuestra interfaz Component que representará a los hoteles y a los futuros tipos de alojamiento que se ofertarán en el futuro, la llamaremos alquilable:
public interface Alquilable {
public String getDescripcion();
public String getTipo();
public float obtenerPresupuesto();
}
A continuación definiremos una clase Hotel que sea Alquilable y que, evidentemente, representará los hoteles que ofertamos en nuestro buscador. Nuestro hotel tendrá un precio base de 100€ por noche:
public class Hotel implements Alquilable{
private double coste_base = 100;
private String tipo = "Hotel";
private String descripcion;
public Hotel(String descripcion){
this.descripcion = descripcion;
}
public String getDescripcion(){
return this.descripcion;
}
public String getTipo(){
return this.tipo;
}
public float obtenerPresupuesto(){
return this.coste_base;
}
}
Ahora crearemos un Decorator que nos permitirá modificar el comportamiento de nuestro elemento Alquilable en tiempo de ejecución:
public abstract class AlquilableDecorator implements Alquilable{
private Alquilable alquilable;
public AlquilableDecorator(Alquilable alquilable){
this.alquilable = alquilable;
}
public Alquilable getAlquilable(){
return this.alquilable;
}
public void setAlquilable(Alquilable alquilable){
this.alquilable = alquilable;
}
public String getDescripcion(){
return getAlquilable().getDescripcion();
}
public String obtenerTipo(){
return getAlquilable().getTipo();
}
public float obtenerPresupuesto(){
return getAlquilable().obtenerPresupuesto();
}
}
Bien, ahora definiremos los complementos con los que extenderemos nuestra clase hotel los cuales podremos asignar en tiempo de ejecución (decoradores concretos):
public class PrimeraLineaDePlaya extends AlquilableDecorator{
public PrimeraLineaDePlaya(Alquilable alquilable){
super(alquilable);
}
public String getDescripcion(){
return getAlquilable().getDescripcion().concat(" (vistas al mar)");
}
public float obtenerPresupuesto(){
return getAlquilable().obtenerPresupuesto() + 100;
}
}
public class PensionCompleta extends AlquilableDecorator{
public PensionCompleta(Alquilable alquilable){
super(alquilable);
}
public String getDescripcion(){
return getAlquilable().getDescripcion().concat(" (pension completa)");
}
public float obtenerPresupuesto(){
return getAlquilable().obtenerPresupuesto() + 65;
}
}
public class DescuentoClienteVIP extends AlquilableDecorator{
public DescuentoClienteVIP(Alquilable alquilable){
super(alquilable);
}
public String getDescripcion(){
return getAlquilable().getDescripcion().concat(" (descuento cliente VIP)");
}
public float obtenerPresupuesto(){
return getAlquilable().obtenerPresupuesto() * 0.85;
}
}
Y por último vamos a ver como utilizar todos nuestros ingredientes desde nuestro buscador, que será el cliente:
public static void main(String[] args){
// Buscaremos un hotel en Torremolinos con pensión completa
Alquilable hotel_torremolinos = new Hotel("Hotel en Torremolinos (Málaga)");
hotel_torremolinos = new PensionCompleta(hotel_torremolinos);
// Visualizamos el resultado
System.out.println(hotel_torremolinos.getDescripcion());
// Que mostrará: "Hotel en Torremolinos (Málaga) (pension completa)"
// Obtenemos el presupuesto
System.out.println(hotel_torremolinos.obtenerPresupuesto()+" €");
// Que mostrará: "165 €"
// Ahora buscaremos un hotel en Denia en primera linea de playa,
// con pensión completa y le aplicaremos el descuento VIP
Alquilable hotel_denia = new Hotel("Hotel en Denia (Alicante)");
hotel_denia = new PrimeraLineaDePlaya(hotel_denia);
hotel_denia = new PensionCompleta(hotel_denia);
hotel_denia = new DescuentoClienteVIP(hotel_denia);
// Visualizamos el resultado
System.out.println(hotel_torremolinos.getDescripcion());
// Que mostrará: "Hotel en Denia (Alicante) (vistas al mar) (pension completa) (descuento cliente VIP)"
// Obtenemos el presupuesto
System.out.println(hotel_torremolinos.obtenerPresupuesto()+" €");
// Que mostrará: "225.25 €"
}
Con esto hemos ilustrado cómo funciona el patrón Decorator, pero si por ejemplo crearíamos una clase Apartamento que implementase Alquilable, también podría ser decorada con las características que hemos definido en el ejemplo.