Contexto: Supongamos que un sistema de comunicación contiene las clases TCPChannel
(protocolo de comunación TCP) y UDPChannel
(protocolo de comunicación UDP) y ambas implementan una interfaz Channel
:
Problema: Los clientes de estas clases necesitan agregar funcionalidades adicionales a los canales, como buffers, compresión de mensajes, registro de los mensajes enviados, etc. Sin embargo, estas funcionalidades son opcionales: dependiendo del cliente, se requieren solo algunas funcionalidades o, tal vez, ninguna. Una primera solución consiste en el uso de herencia para crear subclases con cada posible selección de funcionalidades. En el cuadro siguiente, mostramos algunas de las subclases que tendríamos que crear (donde “extends” significa relación de herencia):
En esta solución, usamos la herencia para implementar subclases para cada conjunto de funcionalidades. Supongamos que el usuario necesita un canal UDP con buffer y compresión. Para ello, tuvimos que implementar UDPBufferedZipChannel
como subclase de UDPZipChannel
, que a su vez es subclase de UDPChannel
. Como habrá notado, una solución basada en herencia es casi inviable, ya que genera una explosión combinatoria del número de clases relacionadas con los canales de comunicación.
Solución: El Patrón Decorador representa una alternativa a la herencia cuando se necesita agregar nuevas funcionalidades a una clase base. En lugar de usar herencia, se utiliza la composición para añadir dichas funcionalidades dinámicamente en las clases base. Por lo tanto, el Decorador es un ejemplo de aplicación del principio de diseño “Prefiera Composición sobre Herencia”, que se estudio anteriormente.
En nuestro problema, al optar por decoradores, el cliente podrá configurar un Channel
de la siguiente manera:
channel = new ZipChannel(new TCPChannel()); // TCPChannel que comprime/descomprime datos channel = new BufferChannel(new TCPChannel()); // TCPChannel con un buffer asociado channel = new BufferChannel(new UDPChannel()); // UDPChannel con un buffer asociado channel= new BufferChannel(new ZipChannel(new TCPChannel())); // TCPChannel con compresión y un buffer asociado
Por lo tanto, en una solución con decoradores, la configuración de un Channel se realiza en el momento de su instanciación, mediante una secuencia anidada de operadores new. El new más interno siempre crea una clase base, en nuestro ejemplo TCPChannel
o UDPChannel
. Después de esto, los operadores más externos se utilizan para decorar el objeto creado con nuevas funcionalidades.
Falta entonces explicar las clases que son los decoradores propiamente dichos, como ZipChannel
y BufferChannel
. Primero, estas son subclases de la siguiente clase que no aparece en el ejemplo, pero que es fundamental para el funcionamiento del patrón Decorador:
class ChannelDecorator implements Channel { private Channel channel; public ChannelDecorator(Channel channel) { this.channel = channel; } public void send(String msg) { channel.send(msg); } public String receive() { return channel.receive(); } }
Esta clase tiene dos características importantes:
Channel
, es decir, implementa esa interfaz y, por lo tanto, sus dos métodos. Así, siempre que se espere un objeto del tipo Channel
, podemos pasar un objeto del tipo ChannelDecorator
en su lugar.Channel
al cual delega las llamadas a los métodos send
y receive
. En otras palabras, un decorador, en nuestro caso, siempre va a hacer referencia a otro decorador. Después de implementar la funcionalidad que le corresponde — un buffer, compresión, etc. — transfiere la llamada a ese decorador.
Por último, llegamos a los decoradores reales. Estos son subclases de ChannelDecorator
, como en el siguiente código, que implementa un decorador que comprime y descomprime los mensajes enviados a través del canal:
class ZipChannel extends ChannelDecorator { public ZipChannel(Channel c) { super(c); } public void send(String msg) { "comprime el mensaje msg" super.send(msg); } public String receive() { String msg = super.receive(); "descomprime el mensaje msg" return msg; } }
Para entender el funcionamiento de ZipChannel
, supongamos el siguiente código:
Channel c = new ZipChannel(new TCPChannel()); c.send("Hello, world")
La llamada a send en la última línea del ejemplo desencadena las siguientes ejecuciones de métodos: