Una vez vistos y entendidos los patrones de creación pasaremos a ver los patrones estructurales, cuyo objetivo será gestionar cómo se combinan clases y objetos para dar lugar a estructuras más complejas.
Al igual que en los patrones de creación los patrones estructurales se dividen en patrones orientados a clases y patrones orientados a objetos:
- De clase: Emplea interfaces para combinar clases incompatibles.
- Patrones: Adaptador (de clases).
- De objeto: Utiliza objetos para la combinación de estructuras.
- Patrones: Adaptador (de objetos), Puente, Composición, Decorador, Fachada, Flyweight y Proxy.
Como podemos apreciar existe un patrón Adapter orientados a clases y otro orientado a objetos. Se explicarán conjuntamente ya que su objetivo es el mismo, pero como ejemplo sólo veremos el Adapter orientado a los objetos ya que el orientado a clases requiere de herencia múltiple, característica que de la que prescinden los principales lenguajes de programación.
Dejemos la teoría de lado y pasemos a ver la plantilla que venimos usando en las anteriores entregas:
Adaptador
Nombre: Adapter (o Wrapper)
Problema y Contexto:
Necesitamos que un conjunto de clases con interfaces incompatibles sean capaces de cooperar entre si.
Se aplica cuando:
- Pretendemos usar una clase existente cuya interfaz no es compatible con la requerida.
- Tenemos que implementar una clase que pueda ser reusable y que coopere con clases no relacionadas, es decir, las clases que no tienen necesariamente interfaces compatibles.
Solución y Estructura:
Class Adapter:
Esta clase de adaptadores es menos utilizada poque utiliza herencia múltiple. Crearemos una clase adaptador que herede de la clase o clases a adaptar, cuyos métodos pueden ser sobreescritos. Además el adaptador deberá implementar la nueva interfaz que deseamos utilizar. La adatación se realiza llamando a los métodos del padre. No permite la adaptación de las subclases de la clase adaptada.
Object Adapter:
Creamos una clase adaptadora que contiene una instancia de la clase o clases a adaptar. Para compatibilizar las clases, el adaptador hace llamadas a la instancia del objeto u objetos a adaptar. Este adaptador funciona con la clase adaptada y todas sus subclases.
La estructura es la siguiente:
Class Adapter:
Object Adapter:
Donde:
Adaptee: Se trata de la clase adaptada (o a adaptar). Se trata de una clase existente la cual pretendemos adaptar para que funcione bajo una nueva interfaz. Puede haber varios Adaptees.
Target: Nueva interfaz utilizada por Client a la que debemos adaptar a Adaptee.
Adaptor: Clase que implementará la interfaz Target y se encargará de que la antigua funcionalidad obedezca a la misma.
Client: Utiliza la funcionalidad de Adaptee de acuerdo a la interfaz Target implementada por Adaptor.
Consecuencias:
- POSITIVAS:
- Permite adaptar clases en dominios totalmente diferentes.
- Un único adaptador puede adaptar la funcionalidad de múltiples clases.
- NEGATIVAS:
- Dependiendo de la implementación, el adaptador puede contener múltiples punteros que incrementen la complejidad del sistema.
Patrones Relacionados: Bridge, Decorator, Facade y Proxy
Ejemplo:
Utilizaremos un ejemplo sencillo para entender bien la funcionalidad: Queremos adaptar una interfaz HDMI para que se vea en televisiones con RCA. En primer lugar definiremos la clase adaptada (Adaptee):
public class HDMI{
public HDMI(){
System.out.println("Nuevo conector HDMI creado.");
}
public void obtenerVideoHD(){
System.out.println("Video obtenido desde fuente HD.");
}
public void obtenerAudioHD(){
System.out.println("Audio obtenido desde fuente HD.");
}
}
Continuaremos definiendo la interfaz que necesitamos en nuestro sistema actual (Target):
public interface RCA{
public void obtenerCanalVideo();
public void obtenerCanalAudioDcho();
public void obtenerCanalAudioIzdo();
}
A continuación definiremos el adaptador (Adapter). Crearemos 2 adaptadores para ilustrar los 2 tipos de adaptadores (clases y objetos):
// Adaptador de Clases
public class HDMItoRCAClassAdapter extends HDMI implements RCA{
public void obtenerCanalVideo(){
extraerVideo();
}
public void obtenerCanalAudioDcho(){
extraerAudioDcho();
}
public void obtenerCanalAudioIzdo(){
extraerAudioIzdo();
}
private extraerVideo(){
obtenerVideoHD();
System.out.println("Convertir fuente de video HD a Video analógico");
}
private extraerAudioDcho(){
obtenerAudioHD();
System.out.println("Extrayendo canal Derecho de Audio");
System.out.println("Convertir fuente de audio HD a Audio analógico Derecho");
}
private extraerAudioIzdo(){
obtenerAudioHD();
System.out.println("Extrayendo canal Izquierdo de Audio");
System.out.println("Convertir fuente de audio HD a Audio analógico Izquierdo");
}
}
// Adaptador de Objetos
public class HDMItoRCAObjectAdapter implements RCA{
private HDMI fuente;
public HDMItoRCAObjectAdapter(){
fuente = new HDMI();
}
public void obtenerCanalVideo(){
extraerVideo();
}
public void obtenerCanalAudioDcho(){
extraerAudioDcho();
}
public void obtenerCanalAudioIzdo(){
extraerAudioIzdo();
}
private extraerVideo(){
fuente.obtenerVideoHD();
System.out.println("Convertir fuente de video HD a Video analógico");
}
private extraerAudioDcho(){
fuente.obtenerAudioHD();
System.out.println("Extrayendo canal Derecho de Audio");
System.out.println("Convertir fuente de audio HD a Audio analógico Derecho");
}
private extraerAudioIzdo(){
fuente.obtenerAudioHD();
System.out.println("Extrayendo canal Izquierdo de Audio");
System.out.println("Convertir fuente de audio HD a Audio analógico Izquierdo");
}
}
Por último queda definir un cliente que utilice el adaptador, que lo simplificaremos mostrando únicamente el metodo main():
public static void main(String args){
RCA conector;
if(args[0].equals("ClassAdapter")){
conector = new HDMItoRCAClassAdapter();
} else if(args[1].equals("ObjectAdapter")){
conector = new HDMItoRCAObjectAdapter();
}
// De esta manera conseguimos trabajar con video HDMI como si fuese analógico
conector.obtenerCanalVideo();
conector.obtenerCanalAudioDcho();
conector.obtenerCanalAudioIzdo();
}