Introducción

Este documento describe por intermedio de ejemplos sencillos algunos de los patrones de diseño[GOF] más importantes, utilizando Java y ocasionalmente C++.

Revisiones

  • 0.1 2006-04-14 Template

  • 0.2 2006-04-15 Bridge, Singleton, Adapter, Factory Method

Template Pattern (Patrón Plantilla)

Primer Caso: Vectores Asociados

Supóngase que deseamos modelar vectores en el plano, los cuales esencialmente representan un par de coordenadas (x,y). Una implementación básica consiste de:

class VectorPlano {
	private double x;
	private double y;

	public VectorPlano(double x,double y) {
		this.x=x; this.y=y;
	}
	public void setX(double x) { this.x=x; }
	public void setY(double y) { this.y=y; }
	public double getX() { return this.x; }
	public double getY() { return this.y; }
	public double norma() {
		return Math.sqrt(x*x+y*y);
	}
}

Nótese que hemos definido una operación “norma()” que retorna la magnitud del vector. Así como ésta, podrían haber muchas otras operaciones.

Esto funciona bien cuando los vectores no tienen relación entre sí. Sin embargo, en una aplicación técnica o de ingeniería, es frecuente la presencia de vectores que se derivan a partir de otros vectores. Por ejemplo, podríamos tener un vector que en todo momento proporciona la suma de otros dos vectores (los cuales pueden variar continuamente):

class VectorSuma {
	private VectorPlano v1;
	private VectorPlano v2;

	public VectorSuma(VectorPlano a,VectorPlano b) {
		v1=a; v2=b;
	}
	public double getX() {
		return v1.getX()+v2.getX();
	}
	public double getY() {
		return v1.getY()+v2.getY();
	}
	public double norma() {
		double c1=getX();
		double c2=getY();
		return Math.sqrt(c1*c1+c2*c2);
	}
}

Ahora bien, ¿qué tienen en común VectorPlano y VectorSuma? Podemos considerar dos grupos de elementos comunes:

  1. Métodos con la misma "firma" (pero distinta implementación como getX() y getY(), lo cual nos da la idea de cierta interface común

  2. Métodos con la misma "firma" y la misma implementación, que pueden corresponder a una jerarquía o herencia

Si observamos con atención, el método norma() de la clase VectorSuma puede aplicarse sin ninguna variación en la clase VectorPlano (sin embargo, lo inverso no es cierto.) A fin de aprovechar esto, podríamos definir una jerarquía del siguiente modo:

public interface Vec {
	double getX();
	double getY();
}

abstract class VectorBase implements Vec {
	public double norma() {
		double c1=getX();
		double c2=getY();
		return Math.sqrt(c1*c1+c2*c2);
	}

class VectorPlano extends VectorBase {
	... // no se repite norma()
}

class VectorSuma extends VectorBase {
	private Vec v1; // Notese que se usa la interfase!
	private Vec v2;

	public VectorSuma(Vec a,Vec b) {
		v1=a; v2=b;
	}
	... // no se repite norma()
}

Gracias a esta abstracción podemos implementar !en la clase abstracta padre! algoritmos que involucran varios vectores. Por ejemplo, un sencillo producto escalar:

	public double productoEscalar(Vec v) {
		reurn getX()*v.getX()+getY()*v.getY();
	}

Los métodos norma() y productoEscalar() ilustran la aplicación del patrón de diseño "Template", el cual consiste en efectuar operaciones (implementar algoritmos) con métodos que aún no han sido definidos, y que se implementarán en las subclases (en nuestro caso, los métodos getX() y getY().)

Este patrón de diseño permite una gran reutilización de código y evita que los algoritmos dependan las implementaciones concretas, las cuales pueden crearse en el futuro conforme se necesiten sin necesidad de alterar el código ya existente (ni siquiera recompilarlo.) Por ejemplo, podríamos agregar una nueva clase que represente el producto de cierto vector por un escalar:

class VectorProducto extends VectorBase {
	private factor;
	private Vec v1;
	public VectorBase(Vec v,double f) {
		v1=v;
		factor=f;
	}
	double getX() {
		return v1.getX()*factor;
	}
	double getY() {
		return v1.getY()*factor;
	}
}

Aunque no tiene relación directa con el patrón template, es digno de resaltar el papel de flexibilidad que proporciona la interface "Vec" a la jerarquía. Gracias a ésta es posible hacer un "producto escalar" con un objeto que no necesariamente pertenece a la jerarquía, por ejemplo:

class Paralelepipedo implements Vec {
	double largo,ancho,alto;
	double ciertoCalculo(Vec t)
		{
		return t.productoEscalar(this);
		}
	// Implementacion de Vec:
	double getX() { return ancho; }
	double getY() { return largo; }
	...
}

Segundo Caso: Impuesto Municipal

Suponiendo que una municipalidad cobra impuestos a los inmuebles siguiendo el siguiente criterio: -1 Si es institución benéfica, el impuesto es cero - Si es vivienda el factor es $2.0 - Si es negocio el factor es $3.5 - El impuesto base es el factor por el área en metros cuadrados - Al impuesto base se le aplica un recargo de 5% si hay ascensores - Al impuesto base se le aplica un recargo de 10% si hay piscina - El impuesto total de las viviendas se reduce en 90% si el dueño es jubilado

Un enfoque tradicional implementaría diversas fórmulas de cálculo del impuesto dependiendo de cada clase concreta, sin embargo, esto es difícil de mantener (un cambio en las leyes municipales obligaría a alterar el código en muchos lugares.)

Una mejor solución consiste en modelar un inmueble abstracto usando el patrón "Template" en un método “calculoImpuesto()” que hace uso de diveros métodos a ser implementados en las subclases, y que implementa totalmente el algoritmo:

abstract class Inmueble {
	private double area;

	abstract public boolean esInstitucionBenefica();
	abstract public boolean esNegocio();
	abstract public boolean hayAscensor();
	abstract public boolean hayPiscina();
	abstract public boolean propietarioJubilado();

	public double calculoImpuesto()
		{
		if(esInstitucionBenefica())
			return 0.0;
		double factor=2.0;
		if(esNegocio())
			factor=3.5;
		double iBase=area()*factor;
		double iAscensor=0;
		if(hayAscensor())
			iAscensor=iBase*0.05;
		double iPiscina=0;
		if(hayPiscina())
			iPiscina=iBase*0.05;
		double iTotal=iBase+iAscensor+iPiscina;
		if(!esNegocio() && propietarioJubilado())
			{
			iTotal=iTotal*(1.00-0.90);
			}
		return iTotal;
		}
	public double getArea() {
		return area;
	}
	public void setArea(double a) {
		area=a;
	}
}

Un hotel (abstracto) podría modelarse a partir de lo anterior así:

abstract class Hotel extends Inmueble {
	boolean esInstitucionBenefica() {
		return false;
	}
	abstract boolean esNegocio() {
		return true;
	}
	boolean propietarioJubilado() {
		return false; // no relevante al no ser vivienda
	}
}

Nótese que el Hotel, si bien abstracto, implementa métodos que lo descartan como institución benéfica, y que indican que se trata de un negocio.

Finalmente, un hotel de lujo es una clase concreta:

abstract class HotelDeLujo extends Inmueble {
	boolean hayAscensor() {
		return true;
	}
	boolean hayPiscina() {
		return true;
	}
}

Conclusiones para Template Pattern

  1. El patrón de diseño "Template" permite implementar algoritmos genéricos cuya implementación concreta no es requerida aún o no está disponible

  2. Típicamente se implementa invocando a métodos abstractos, aunque también podría tratarse de métodos concretos que son posteriormente sobrecargados

  3. La centralización del algoritmo en un único método de una única clase base, facilita tremendamente el mantenimiento del código y la incorporación de cambios

Bridge Pattern (Patrón Puente)

Primer Caso: Acceso a distintas bases de datos

Supóngase que se va a modelar una jerarquía de clases que modelan las cuentas de un banco. En una fase inicial se utilizará una base de datos relacional MySQL para el almacenamiento físico, pero es un requerimiento que el sistema sea extensible para soportar otros productos con el mismo propósito.

Una implementación simple podría hacer uso de la herencia para separar las operaciones comerciales de la implementación del acceso a la base de datos:

// C++
class Cuenta {
	protected:
	string fechaApertura;
	double saldoInicial;
	public:
	string getFechaApertura() {
		return fechaApertura;
	}
	double getSaldoInicial() {
		return saldoInicial;
	}
};

Su implementación con MySQL (por simplicidad, asumieremos que sólo el constructor requiere esta interacción):

// C++
#include <mysql++.h>
class MysqlCuenta {
	public:
	MysqlCuenta(int idCuenta) {
		mysqlpp::Query query=con.query();
		query << "SELECT SALDO_INICIAL,FECHA_APERTURA FROM CUENTA"
		" WHERE IDCUENTA=" << idCuenta;
		mysqlpp::Result res = query.store();
		if(res && res.at(0)) {
			mysqlpp::Row row=res.at(0);
			saldoInicial=row.at(0);
			fechaApertura=string(row.at(1));
		}
		else {
			throw CuentaException;
		}
	}
};

A continuación, podemos tener una cuenta asociada a puntos promocionales de marketing, los cuales típicamente se traducen en obsequios para los clientes:

class CuentaPromocion : public Cuenta {
	protected:
	int puntosPromocion;
	public:
	int getPuntosPromocion() {
		return puntosPromocion;
	}
};

Y su respectiva implementación MySQL:

// C++
class MysqlCuentaPromocion : public CuentaPromocion {
	public:
	CuentaPromocion(int idCuenta) {
		mysqlpp::Query query=con.query();
		query << "SELECT PUNTOS FROM PROMOCION"
		" WHERE IDCUENTA=" << idCuenta;
		mysqlpp::Result res = query.store();
		if(res && res.at(0)) {
			mysqlpp::Row row=res.at(0);
			puntosPromocion=row.at(0);
		}
		else {
			throw CuentaException;
		}
	MysqlCuenta tmp(idCuenta);
	saldoInicial=tmp.getSaldoInicial();
	fechaApertura=tmp.getFechaApertura();
	}
};

Un cliente que accede a una "Cuenta Promocional" implementada con MySQL deberá utilizar un código tal como:

MysqlCuentaPromocion cta(450025);
cout << "Saldo inicial:"+cta.getSaldoInicial() <<
	"Puntos:"+cta.getPuntosPromocion() << endl;

Eventualmente, este diseño puede reutilizarse con una implementación en otra base de datos (por ejemplo, Oracle.) Para esto se crearían las clases derivadas OracleCuenta y OracleCuentaPromocion, que utilizarían el API de acceso a Oracle (obviaremos presentar el código correspondiente.) La jerarquía de clases será ahora:

Jerarquía problemática

Y los clientes accederían a esta nueva implementación mediante:

OracleCuentaPromocion cta(450025);
cout << "Saldo inicial:"+cta.getSaldoInicial() <<
	"Puntos:"+cta.getPuntosPromocion() << endl;

Problemas de la implementación

  1. Para N clases de negocio y B implementaciones de base de datos, el número total de clases se eleva rápidamente a N*B, lo cual se hace muy difícil de mantener y extender

  2. Los clientes no deberían estar "amarrados" a una implementación particular (sin embargo, en ciertas ocasiones podrían elegir la implementación adecuada en tiempo de ejecución.) Por ejemplo, la aplicación cliente que utiliza el objeto “MysqlCuentaPromocion” requiere la modificación de su código fuente para utilizar la implementación Oracle proporcionada por “OracleCuentaPromocion”

  3. Se hace difícil la reutilización del código. Por ejemplo, los constructores de MysqlCuentaPromocion (y de OracleCuentaPromocion) no pueden aprovechar directamente la jerarquía e invocar a los constructores de MysqlCuenta (y de OracleCuenta). La solución presentada en el ejemplo recurre a la creación de un objeto temporal de la clase MysqlCuenta, pero esto claramente es ineficiente y confuso

Implementación con Bridge

El patrón Bridge consiste en la creación de dos jerarquías interrelacionadas pero distintas. La primera jerarquía capturaría las clases de negocio, mientras que la segunda debe partir de una clase abstracta (o una interfaz) que contenga las diversas operaciones de base de datos a ser implementadas para cada producto en sus subclases.

La clase base de la primera jerarquía posee una referencia a la clase abstracta o interfaz de la segunda jerarquía, lo cual permite su codificación sin necesidad de disponer aún de una implementación concreta en la segunda jerarquía (algo similar al patrón "Template".)

Si esto último no se comprendío, no hay problema. Estúdiese el ejemplo y luego retorne a esta sección.

Nuestra primera jerarquía podría empezar con la clase “Cuenta”, pero para brindar una mejor separación de la abstracción, crearemos una superclase llamada "CuentaAbstracta" que implementa la primera parte del patrón Bridge:

// C++
class CuentaAbstracta {
	private:
	DataBase *database; // Referencia a la segunda jerarquia
	public:
	void CuentaAbstracta() {
		database=globalConfig.getDatabase();
	}
	string obtieneCampoPorIdTabla(string &campo,
		string &tabla,int idCuenta) {
		return database->obtieneCampoPorIdTabla(campo,
			tabla,idCuenta);
	}
};

class Cuenta : public CuentaAbstracta {
	protected:
	string fechaApertura;
	double saldoInicial;
	public:
	Cuenta(int idCuenta) {
		database=Configuracion.getDatabase();
		fechaApertura=obtieneCampoPorIdTabla(
			"FECHA_APERTURA","CUENTA",idCuenta);
		saldoInicial=atof(obtieneCampoPorIdTabla(
			"SALDO_INICIAL","CUENTA",idCuenta));
	}
	string getFechaApertura() {
		return fechaApertura;
	}
	double getSaldoInicial() {
		return saldoInicial;
	}
};

class CuentaPromocion : public Cuenta {
	protected:
	int puntosPromocion;
	public:
	CuentaPromocion(int idCuenta):Cuenta(idCuenta) {
		fechaApertura=obtieneCampoPorIdTabla(
			"PUNTOS","PROMOCION",idCuenta);
	}
	int getPuntosPromocion() {
		return puntosPromocion;
	}
};

Hemos considerado (para este caso particular) que el constructor obtiene un objeto de tipo DataBase a partir de la configuración global de la aplicación, lo cual es algo frecuente. En ciertas ocasiones esto podría no ser suficiente (por ejemplo, si se accede simultáneamente a distintas bases de datos) y se podría proporcionar un argumento de tipo DataBase* al constructor para hacerlo más genérico.

Como se aprecia, la jerarquía de negocios se apoya en la clase padre (típicamente abstracta) “CuentaAbstracta” para realizar las operaciones de base de datos. Obsérvese que el código del constructor de “CuentaPromocion” se apoya directamente en el código del constructor de “Cuenta” (no se requieren objetos temporales ni repetir código como en la implementación original.)

Para este problema concreto hemos logrado aislar todas las operaciones de base de datos en un único método extremadamente general llamado “obtieneCampoPorIdTabla()”, pero típicamente se requerirán más métodos de esta índole. La implementación de la jerarquía de base de datos se presenta a continuación:

// C++
class Database {
	public obtieneCampoPorIdTabla(string campo,string tabla,
		int cta)=0;
}

class MysqlDatabase {
	public obtieneCampoPorIdTabla(string campo,string tabla,
			int cta) {
		mysqlpp::Query query=con.query();
		query << "SELECT " << campo <<" FROM " << tabla <<
		" WHERE IDCUENTA=" << cta;
		mysqlpp::Result res = query.store();
		if(res && res.at(0)) {
			mysqlpp::Row row=res.at(0);
			return=row.at(0);
		}
	throw CuentaException;
	}
}

Análogamente tendríamos la clase OracleDatabase. Con todo esto, las aplicaciones clientes utilizarían simplemente:

CuentaPromocion cta(450025);
cout << "Saldo inicial:"+cta.getSaldoInicial() <<
	"Puntos:"+cta.getPuntosPromocion() << endl;

erarquía con Bridge Pattern

La decisión de qué implementación usar se puede mantener aislada en un único punto como parte de la configuración global de la aplicación:

globalConfig.setDatabase(new MysqlDatabase);
//globalConfig.setDatabase(new OracleDatabase);

Sólo por no dejar cabos sueltos, una posible implementación de globalConfig sería:

class GlobalConfig {
	DataBase *db;
	public:
	void setDataBase(d) { db=d; }
	Database *getDatabase() { return db; }
};

GlobalConfig globalConfig;

Como se aprecia, el patrón Bridge nos ha permitido además reducir el número total de clases a (N+1 + B+1) y ha logrado que el código cliente no esté amarrado a ninguna implementación.

Otro detalle no menos significativo es que todas las clases de negocio (excepto la abstracta inicial) ignoran por completo el asunto de la base de datos, centralizándose toda la interacción en la base, lo cual hace el código más fácil de extender.

Segundo Caso: Transferencia de Archivos

Supóngase que se desea crear un sistema de transferencia de archivos, el cual proporciona un servicio "plano" para archivos comunes, un servicio "encriptado" para archivos confidenciales, y un servicio "comprimido" para archivos muy grandes. Adicionalmente el sistema utiliza un protocolo de comunicaciones X25, pero se sabe que (como es costumbre) eventualmente se migrará a TCP/IP.

Utilizando el patrón bridge la implementación permitiría crear dos jerarquías para los tipos de servicio y para los protocolos de comunicaciones, respectivamente. Lo importante está en aislar las operaciones esenciales de las clases base de estas jerarquías, por ejemplo, la interface de los protocolos de comunicación (o medios de transporte, si se quiere), debería proporcionar los siguientes métodos:

// java
public interface MedioComunicacion {
	public boolean conectar(String cs);
	public boolean enviarDatos(byte[] datos);
	public byte[] recibirDatos();
	public boolean desconectar();
}

Una hipotética implementación de esto podría ser:

import com.xxx.x25Connection;

public class X25FileTransfer implements MedioComunicacion {
	private X25Connection conn;
	public boolean conectar(String connString) {
		try {
			conn=new X25Connection(connString);
		} catch(Exception e) {
			return false;
		}
		return true;
	}
	public boolean enviarDatos(byte[] datos) {
		return conn.sendData(datos);
	}
	public byte[] recibirDatos() {
		return conn.recvData(datos);
	}
	public boolean desconectar() {
		return conn.close();
	}

Análogamente se tendría una implementación en sockets TCP/IP (que no mostraremos.)

La primera jerarquía tendría como padre la siguiente clase abstracta, la cual proporcionará implementaciones por omisión para todos los métodos:

abstract class FileTransfer {
	private MedioComunicacion medio;
	public FileTransfer(MedioComunicacion m) {
		medio=m;
	}
	public boolean conectar(String cs) {
		return medio.conectar(cs);
	}
	public boolean enviarDatos(byte[] datos) {
		return medio.enviarDatos(datos);
	}
	public byte[] recibirDatos() {
		return medio.recibirDatos(datos);
	}
	public boolean desconectar() {
		return medio.desconectar();
	}
}

La primera implementación es la más sencilla: el servicio de transferencia de archivos "planos" sólo tiene que implementar el constructor (pues es lo único que no se hereda):

class SimpleFileTransfer {
	public SimpleFileTransfer(MedioComunicacion m) {
		super(m);
	}
}

Para los servicios de encriptación y compresión, asumiremos la disponibilidad de dos clases utilitarias externas (com.xxx.Encriptador y com.xxx.Compresor.) Con esto, sólo es menester reimplementar un par de métodos en cada caso.

Clase de servicio de encriptación:

import com.xxx.Encriptador;

class CryptedFileTransfer {
	private Encriptador crypter;

	public CryptedFileTransfer(MedioComunicacion m) {
		super(m);
	crypter=new Encriptador;
	}
	public void enviarEncriptado(byte[] datos,byte[] clave) {
		return enviarDatos(crypter.desCrypt(datos,clave));
	}
	public byte[] recibirEncriptado() {
		return crypter.desUnCrypt(recibirDatos(),clave);
	}
}

Clase de servicio de compresión:

import com.xxx.Compresor;

class CompressedFileTransfer {
	private Compresor compresor;

	public CompressedFileTransfer(MedioComunicacion m) {
		super(m);
	compresor=new Compresor;
	}
	public void enviarComprimido(byte[] datos) {
		return enviarDatos(compresor.comprimir(datos));
	}
	public byte[] recibirComprimido() {
		return compresor.descomprimir(recibirDatos());
	}
}

Obsérvese una vez más que las clases de servicio (excepto la clase abstracta base) no tienen ningún conocimiento de la existencia de protocolos de comunicación.

Jerarquía de clases con Bridge Pattern

Finalmente, un cliente hará uso de estos servicios del siguiente modo:

CompressedFileTransfer cft=new CompressedFileTransfer(
	new X25FileTransfer());
cft.conectar("74412314422");
cft.enviarComprimido(muchosDatos);
cft.desconectar();

No sobra recordar que es preferible utilizar un objeto auxiliar que centralice el medio de comunicación deseado, a fin de evitar introducir nombres de clases concretas en el código:

CompressedFileTransfer cft=new CompressedFileTransfer(
	globalConfiguration.getProtocol());

(una idea de cómo implementar esto se proporcionó para el caso anterior de este mismo patrón.)

Singleton Pattern

Caso Único: Calculor de impuestos

Tenemos una clase cuya función es proporcionar el cálculo de impuestos a las ventas:

public class Impuesto {
        private double tasa=0;
        public void setTasa(double t) {
                tasa=t;
        }
        public double calcula(double monto) {
                return tasa*monto;
        }
}

Una aplicación cliente hace uso de ésta así:

class Prueba {
        public static void main(String[] args) {
                Impuesto i=new Impuesto();
                i.setTasa(0.19);
                double monto=120.0;
                System.out.println("Impuesto de "+monto+
			"="+i.calcula(monto));
        }
}

Sin embargo, es probable que la aplicación se torne más compleja eventualmente otro desarrollador podría crear una nueva instancia de Impuesto. Esto puede ser riesgoso e ineficiente:

  1. El nuevo desarrollador podría olvidar inicializar correctamente la tasa o en general, el estado del objeto

  2. En tanto ya se dispone de una instancia de Impuesto correctamente inicializada, ésta se debería reutilizar en cualquier otro lugar de la aplicación (asumiéndose que no tiene sentido que el mismo impuesto se utilice con dos tasas distintas)

En conclusión, la aplicación sólo requiere de una instancia de la clase, y cualquier otra es un indicador de un potencial error. El patrón "singleton" corresponde precísamente a lograr la creación de un único objeto de una clase, y no más de uno.

Alternativas

La clase Impuesto se podría reescribir a fin de que todos sus métodos sean estáticos, a fin de que no haya necesidar de instanciarla. La aplicación cliente sería:

class Prueba {
        public static void main(String[] args) {
                Impuesto.setTasa(0.19);
                double monto=120.0;
                System.out.println("Impuesto de "+monto+
			"="+Impuesto.calcula(monto));
        }
}

Además, el constructor se podría declarar privado a fin de que no se permita ninguna instanciación:

public class Impuesto {
        private static double tasa=0;
        public static void setTasa(double t) {
                tasa=t;
        }
        public static double calcula(double monto) {
                return tasa*monto;
        }
        private Impuesto(){}
}

Esto es una solución rápida que puede ser útil para ciertos casos. Sin embargo, se pierde mucha flexibilidad propia de los objetos.

Una implementación más adecuada consiste en utilizar un método estático encargado de proporcionar la (unica) instancia de la clase singleton. El constructor tendrá ámbito "private" a fin de que los clientes no creen instancias directamente:

public class Impuesto {
        private static Impuesto inst=null;
        private double tasa=0;
        public void setTasa(double t) {
                tasa=t;
        }
        public double calcula(double monto) {
                return tasa*monto;
        }
        public static Impuesto instancia() {
                if(inst==null) {
                        inst=new Impuesto();
                }
        return inst;
        }
        private Impuesto() {}
}

Esto es razonablemente sencillo de agregar a cualquier clase que se requiere hacer "singleton". Una de las extensiones típicas a estas implementaciones consisten en permitir obtener objetos "singleton" correspondientes a las subclases de la clase base. Por ejemplo, la siguiente clase especializa el cálculo del impuesto:

public class ImpuestoExonerado extends Impuesto {
        public double calcula(double monto) {
                if(monto<700.0)
                        return 0.0;
                return super.calcula(monto);
        }
}

La clase Impuesto podría proporcionar ahora una (única) instancia de esta nueva clase:

public class Impuesto {
        private static Impuesto inst=new ImpuestoExonerado();
        private double tasa=0;
        public void setTasa(double t) {
                tasa=t;
        }
        public double calcula(double monto) {
                return tasa*monto;
        }
        public static Impuesto instancia() {
                return inst;
        }
        protected Impuesto() {
                if(inst!=null)
                        throw new RuntimeException("No usar constructor!");
                }
}

Nótese que hemos modificado el constructor a "protected" a fin de permitir la creación de la clase hija (que implícitamente lo invoca.) Esto tiene el penoso efecto de permitir a los clientes del mismo paquete de Impuesto la instanciación directa con new. Una forma de contrarrestar este efecto consiste en proporcionar un paquete propio a Impuesto, aunque eventualmente esto puede trasgredirse si el cliente se declara en dicho paquete. A fin de cerrar definitivamente el acceso a este constructor, hemos forzado a la creación de la instancia en el momento de inicialización de la aplicación al declarar la variable inst de tipo static. Con esto, cualquier cliente que logre invocar al constructor, recibirá una excepción. Esta solución no es perfecta, pero es un compromiso razonable.

De otro lado, el Impuesto ahora tiene en "hardcode" la creación de ImpuestoExonerado. Claramente esto es poco flexible, siendo de esperar que Impuesto proporcione un mecanismo auxiliar para especificar qué subclase se va a instanciar.

Véase [SINGLETON] para una implementación bastante general (pero no muy sencilla) de este requerimiento.

Patrón Adapter

Este patrón se utiliza para reutilizar código que presenta una interface incompatible con la que el cliente necesita.

Primer Caso: Vectores 3D y 2D

Supóngase que se dispone de una librería de vectores tridimensionales implementados con una clase Vector3D:

public class Vector3D {
	private double x,y,z;
	public double getX() { return x; }
	public double getY() { return y; }
	public double getZ() { return z; }
	public double productoEscalar(Vector3D v) {
		return x*v.getX()+y*v.getY()+z*v.getZ();
	}
	public double norma() {
		return Math.sqrt(x*x+y*y+z*z);
	}
	public Vector3D(double a,double b,double c) {
		x=a; y=b; z=c;
	}
}

Por otro lado, estamos construyendo una aplicación que únicamente requiere de vectores en el plano (Vector2D.) La idea es implementar una interface [1] tal como:

public interface Vector2D {
	public double getAbsisa(); // eje x
	public double getOrdenada(); // eje y
	public double prod(Vector2D v); // producto escalar
	public double magnitud(); // norma
}

Un cliente de esta nueva aplicación deberá crear un objeto que implementa dicha interface tal como:

// Nueva aplicacion
VectorPlano v=new VectorPlano(3,4);
System.out.println("Magnitud del vector = "+v.magnitud());

Como se aprecia, la implementación de la (antigua) librería tridimensional podría reutilizarse, siempre y cuando se haga una adecuada adaptación. El patrón Adapter consiste precísamente en esto. Una ventaja adicional de este patrón es que no se requiere disponer del código fuente de la librería que se va a reutilizar.

Concretamente, los vectores en el plano se pueden considerar un caso particular de los vectores 3D en el que su coordenada "z" es nula.

Una primera implementación de la clase "Adapter" VectorPlano consiste de:

public class VectorPlano extends Vector3D implements Vector2D {
	VectorPlano(double a,double b)
		{
		super(a,b,0);
		}
	public double getAbsisa() {
		return getX();
	}
	public double getOrdenada() {
		return getY();
	}
	public double prod(Vector2D v) {
		return productoEscalar(
			new Vector3D(v.getAbsisa(),
				v.getOrdenada(),0));
	}
	public double magnitud() {
		return norma();
	}
}

Esto se suele denominar "Class Adapter" en la medida que la clase "Adapter" hereda de la clase a reutilizar. Otra manera de implementar esto es mediante una composición:

public class VectorPlano implements Vector2D {
	Vector3D v3;
	VectorPlano(double a,double b)
		{
		v3=new Vector3D(a,b,0);
		}
	public double getAbsisa() {
		return v3.getX();
	}
	public double getOrdenada() {
		return v3.getY();
	}
	public double prod(Vector2D v) {
		return v3.productoEscalar(
			new Vector3D(v.getAbsisa(),
				v.getOrdenada(),0));
	}
	public double magnitud() {
		return v3.norma();
	}
}

adapter

La composición tiene diversas ventajas sobre la herencia tal como se discute en [DIAMANTE].

Segundo Caso: Adaptación para cambio de base de datos

Este ejemplo utiliza lenguaje C, pero la idea es exactamente la misma que en caso de los objetos: adaptar dos interfases.

Supóngase que se dispone de muchos programas clientes que utilizan el API nativo de una base de datos MySQL. Por simplicidad consideraremos un programa que sólo se conecta e inmediatamente desconecta:

#include <stdio.h>
#include <mysql.h>

MYSQL  *conn; /* connection handler */

int
main (int argc, char *argv[])
{
  conn = mysql_init (NULL);
  mysql_real_connect (
        conn,
        "localhost", /* server host */
        "sargon", /* user */
        "ninive", /* password */
        "babilon", /* database */
        0, NULL, 0);
  mysql_close (conn);
  exit (0);
}

Ahora pretendemos que este programa se ejecute (¡sin cambios!) en una base de datos PostgreSQL. Para esto debemos adaptar las interfases respectivas, y nuestra clase "Adapter" corresponderá simplemente a un módulo conteniendo un conjunto de funciones de adaptación. Como es usual en C, crearemos un archivo cabecera con las declaraciones y un archivo de código fuente con las implementaciones:

/* Nuevo mysql.h: Interfaz de Mysql esperada por el cliente */

#include "libpq-fe.h" /* header de postgresql */
typedef PGconn MYSQL;

MYSQL *mysql_init(MYSQL *ptr);
MYSQL *mysql_real_connect(MYSQL *mysql, const char *host,
	const char *user, const char *passwd, const char *db,
	unsigned int port, const char *unix_socket,
	unsigned long client_flag);
void mysql_close(MYSQL *mysql);

La implementación del adaptador:

/* mysql_adapter.c */

#include <mysql.h> /* el nuevo ! */
MYSQL *mysql_init(MYSQL *ptr) {
	if(ptr)
		{
		memset(ptr,0,sizeof(MYSQL));
		return r;
		}
	MYSQL *r=calloc(1,sizeof(MYSQL));
	return r;
}
MYSQL *mysql_real_connect(MYSQL *mysql, const char *host,
	const char *user, const char *passwd, const char *db,
	unsigned int port, const char *unix_socket,
	unsigned long client_flag) {
	if(mysql==NULL)
		return NULL;
	/* PostgreSQL utiliza una 'cadena de conexion' */
	char buffer[1024]="";
	/* falta verificar posible desborde del arreglo */
	if(host) {
		strcat(buffer," host="); strcat(buffer,host);
	}
	if(user) {
		strcat(buffer," user="); strcat(buffer,user);
	}
	if(passwd) {
		strcat(buffer," password="); strcat(buffer,passwd);
	}
	if(db) {
		strcat(buffer," dbname="); strcat(buffer,db);
	}
	/* ignoraremos el puerto y otros flags */
	PGconn *conn=PQconnectdb(buffer);
	if(conn) {
		if(PQstatus(conn)==CONNECTION_OK)
			{
			/* copiamos la nueva conexion en la
			estructura suministrada */
			memcpy(mysql,conn,sizeof(PGconn));
			return mysql;
			}
	}
	PQfinish(conn);
}
void mysql_close(MYSQL *mysql) {
	PQfinish(mysql);
}

Con esta adaptación el programa cliente seguirá "creyendo" que utiliza MySQL, pero en realidad ya no es así. Nótese que el programa cliente deberá ser recompilado (pero no modificado) debido a que los ejecutables de C podrían estar enlazados estáticamente a la librería de MySQL, y además las estructuras asociadas tendrán distintos tamaños y "alignments" que muy probablemente generarán errores difíciles de depurar. Asimismo se debe tener cuidado de apuntar hacia nuestro reemplazo de "mysql.h" (tal vez sería mejor eliminar la instalación de Mysql por completo.)

Factory Méthod Pattern (Patrón Método Fábrica)

Primer Caso: Sistema de mensajería financiera

Cierto sistema de mensajería es utilizado para intercambiar información con una entidad financiera. Este sistema cuenta con toda la información de índole comercial y posee los mecanismos para codificarla adecuadamente en mensajes a ser enviados y recibidos (por ejemplo, transacciones financieras.)

Sin embargo, a fin de dotar de mayor flexibilidad a las instalaciones, los diseñadores de este sistema decidieron dejar indefinido el medio de transporte (protocolos de comunicaciones) a utilizarse. Como es de esperarse, sólo se especificó la interface que ofrecería dicho medio de transporte:

public interfase TransporteMensaje {
	public boolean conectar();
	public boolean enviarDatos(byte[] datos);
	public byte[] recibirDatos();
	public boolean desconectar();
}

El Sistema de Mensajería como tal, se implementa mediante una clase abstracta que debe ser derivada en cada instalación:

public abstract class SistemaMensajeria {
	public boolean iniciarOperaciones() {
		TransporteMensaje transporte=crearTransporte();
		if(!transporte.conectar())
			return false;
		// enviar y recibir mensajes por intermedio
		// de 'transporte'
		...
	}
	public abstract TransporteMensaje crearTransporte();
}

Tal como se aprecia, esta clase requiere que sus derivadas implementen el método crearTransporte(), el cual retornará un objeto de tipo TransporteMensaje, el cual será utilizado para la transferencia de todos los mensajes financieros. El sistema se inicializa invocando al método “iniciarOperaciones”, el cual como primera acción realiza la obtención del medio de transporte.

Como se indicó, un implementador de dicho sistema deberá proporcionar un medio de transporte que satisface la inteface TransporteMensaje. Por ejemplo, si la instalación utiliza redes SNA, el medio de transporte podría definirse así:

import com.ibm.products.*;

public class TransporteSNA implements TransporteMensaje {
	SNAconnection conn;
	public boolean conectar() {
		try {
			conn=new SNAconnectionLU6("remoteSystem");
		} catch(SNAException e)
			{
			return false;
			}
		return true;
	}
	public boolean enviarDatos(byte[] datos) {
		conn.sendSyncData(datos);
	}
	public byte[] recibirDatos() {
		return conn.getSyncData();
	}
	public boolean desconectar() {
		return conn.closeConnection();
	}
}

Con esto, el implementador derivará el sistema del mensajería así:

public class MensajeriaSNA extends SistemaMensajeria {
	public TransporteMensaje crearTransporte() {
		return new TransporteSNA();
	}
}

Para ser usado de este modo:

MensajeriaSNA sistema=new MensajeriaSNA();
sistema.iniciarOperaciones();
// Retorna cuando ya no hay mas operaciones financieras que realizar
// o si ocurre un error...

Gracias al "Factory Method" crearTransporte(), el sistema de mensajería es tremendamente genérico al no estar atado a ningún protocolo de comunicación: las subclases se encargan de proporcionar los objetos con dicho fin.

Esta técnica es utilizada con frecuencia por los "frameworks de desarrollo" que deben prestar servicios diversos a clases y objetos que aún no han sido escritos por los programadores.

Decorator Pattern (Patrón Decorador)

Primer Caso: Transferencia de información

Se cuenta con dos servicios de transferencia de información: uno de ellos envía la infomación hacia archivos de disco y el otro hacia un proceso remoto vía red. En el futuro podrían agregarse más servicios.

Estos servicios se implementan en clases que satisfacen la siguiente interface:

public interface Flujo {
	public boolean abrir(String cs);
	public boolean enviarDatos(byte[] datos);
	public byte[] recibirDatos();
	public boolean desconectar();
}

Las clases de servicio actualmente son:

public class FlujoDisco implements Flujo {
	...
}
public class FlujoRed implements Flujo {
	...
}

Se desea agregar servicios de encriptación y/o compresión a estos flujos.

Decoración de objetos

El problema se presenta con las condiciones para aplicar el patrón Decorador debido a que se desea extender el comportamiento exterior de ciertos objetos que satisfacen cierta interface. Esto se puede implementar también mediante herencias, pero conlleva a una explosión en el número total de clases.

El patrón decorador consiste en "envolver" los objetos cuyo comportamiento se desea extender (decorar), conservando su interface. Esta envoltura se encarga de ejecutar el comportamiento adicional y finalmente invocará a los métodos del objeto decorado.

La aplicación del decorador demanda por lo general crear una nueva clase (abstracta) que implementa la interface Flujo. Esta clase tendrá como miembro una referencia a un objeto también de tipo Flujo (el objeto decorado.) Los decoradores concretos son subclases de dicha clase abstracta. Como siempre, el ejemplo lo ilustra mejor:

Para nuestro caso, crearemos una clase abstracta "FlujoDecorado" que implementa la interface Flujo redirigiendo las solicitudes hacia el objeto decorado:

abstract class FlujoDecorado implements Flujo {
	private Flujo objetoDecorado;
	public FlujoDecorado(Flujo f) {
		objetoDecorado=f;
	}
	public boolean abrir(String cs) {
		return objetoDecorado.abrir(cs);
	}
	public boolean enviarDatos(byte[] datos) {
		return objetoDecorado.enviarDatos(datos);
	}
	public byte[] recibirDatos() {
		return objetoDecorado.recibirDatos();
	}
	public boolean desconectar() {
		return objetoDecorado.desconectar();
	}
}

A partir de dicha clase se heredan los decoradores reales. Por ejemplo, el decorador de compresión:

import com.xxx.Compresor;

class FlujoComprimido extends FlujoDecorado {
	public FlujoComprimido(Flujo f) {
		super(f);
	}
	public boolean enviarDatos(byte[] datos) {
		byte[] cdata=Compresor.comprimir(datos);
		return ((FlujoDecorado)this).enviarDatos(cdata);
	}
	public byte[] recibirDatos() {
		byte[] bigdata=((FlujoDecorado)this).recibirDatos();
		return Compresor.descomprimir(bigdata);
	}
}

Nótese que este decorador sólo redirige los requerimientos a la clase abstracta y altera los la información convenientemente.

Anidamiento

Un detalle interesante corresponde a la posibilidad de anidar decoradores. Por ejemplo, se podría definir un flujo con compresión y encriptación así:

Flujo f=new FlujoComprimido(
	new FlujoEncriptado(new FlujoRed("192.168.1.5")));

Para terminar, el lector deberá comparar este caso con el segundo del patrón Bridge y apreciar los compromisos que representan cada una de las soluciones.

Composite Pattern (Patrón de Composición)

Este patrón se para modelar estructuras de componentes que pertenecen a contenedores, y estos a su vez pertenecen a otro contenedor más grande, y así sucesivamente.

Primer Caso: Máquina compuesta de partes

En cierto proceso de fabricación se construyen máquinas en las que intervienen diversos materiales. Se requiere conocer el total del aporte de cada material al costo y al peso de la máquina. La máquina está compuesta de partes, las cuales a su vez están compuestas de otras partes. Cada parte tiene un nombre que la identifica.

Aplicación del patrón Composite

La clave está en reconocer la relación parte-todo recursiva. Implementamos una interface abstracta "Parte":

public interface Parte {
        public double getPrecio();
        public double getPrecioMaterial(String material);
        public void agregarParte(Parte p);
        public String getNombre();
}

Ahora consideraremos dos tipos de parte: aquellas que están compuestas de otras partes y aquellas que no lo están (partes atómicas.) Partimos creando una clase abstracta base que proporciona algunas implementaciones:

public abstract class ParteAbstracta implements Parte {
        private String nombre;
        public ParteAbstracta(String s) {
                nombre=s;
        }
        public void agregarParte(Parte p) {
        }
        public String getNombre() {
                return nombre;
        }
}

Y crearemos dos clases para modelar los dos tipos de parte. A continuación una parte "atómica":

import java.util.*;

public class ParteAtomica extends ParteAbstracta {
        private Hashtable <String,Double>precio;
        public ParteAtomica(String name) {
                super(name);
                precio=new Hashtable<String,Double>();
        }
        public void setMaterial(String material, double precio) {
                this.precio.put(material,precio);
        }
        public double getPrecio() {
                double p=0;
                for(Enumeration <String> e=precio.keys();e.hasMoreElements();)
                        p+=precio.get(e.nextElement());
                return p;
        }
        public double getPrecioMaterial(String material) {
                if(precio.containsKey(material))
                        return precio.get(material);
                return 0;
        }
        public String toString() {
                return getNombre();
        }
}

Parte compuesta:

import java.util.*;

public class ParteCompuesta extends ParteAbstracta {
        private List<Parte> subparte;
        public ParteCompuesta(String name) {
                super(name);
                subparte=new ArrayList<Parte>();
        }
        public void agregarParte(Parte p) {
                subparte.add(p);
        }
        public double getPrecio() {
                double p=0;
                for(Iterator <Parte> it=subparte.iterator();it.hasNext();)
                        p+=it.next().getPrecio();
                return p;
        }
        public double getPrecioMaterial(String material) {
                double p=0;
                for(Iterator <Parte> it=subparte.iterator();it.hasNext();)
                        p+=it.next().getPrecioMaterial(material);
                return p;
        }
        public String toString() {
                String rpta=getNombre()+"(";
                for(Iterator <Parte> it=subparte.iterator();it.hasNext();)
                        rpta=rpta+it.next().toString()+",";
                return rpta+")";
        }
}

En ciertos casos, no bastará crear una "parte atómica" sino también un conjunto de sub clases propias del dominio del caso, por ejemplo, ParteMotor, ParteRueda, etc. No lo haremos aquí.

El programa que se muestra a continuación ilustra el uso de todo lo anterior:

public class Prueba {

        public static void main(String[] args) {
                ParteCompuesta computador=new ParteCompuesta("PC");
                ParteAtomica cpu=new ParteAtomica("CPU");
                cpu.setMaterial("oro",100);
                cpu.setMaterial("plastico",20);
                cpu.setMaterial("cobre",5);
                computador.agregarParte(cpu);
                ParteAtomica disco=new ParteAtomica("Disco Duro");
                disco.setMaterial("acero",60);
                disco.setMaterial("plastico",30);
                computador.agregarParte(disco);
                ParteCompuesta casex=new ParteCompuesta("Case");
                ParteAtomica power=new ParteAtomica("PowerSupply");
                power.setMaterial("acero",5);
                power.setMaterial("plastico",5);
                casex.agregarParte(power);
                ParteAtomica box=new ParteAtomica("Box Structure");
                box.setMaterial("acero",5);
                box.setMaterial("plastico",2);
                casex.agregarParte(box);
                computador.agregarParte(casex);

                System.out.println("Computador cuesta (USD): "+
                        computador.getPrecio());
                System.out.println("Partes del computador: "+computador);
                System.out.println("Precio en acero: "+
                        computador.getPrecioMaterial("acero"));
                System.out.println("Precio en plastico: "+
                        computador.getPrecioMaterial("plastico"));
        }
}

Su resultado es:

$ java Prueba
Computador cuesta (USD): 232.0
Partes del computador: PC(CPU,Disco Duro,Case(PowerSupply,Box Structure,),)
Precio en acero: 70.0
Precio en plastico: 57.0

Referencias


1. No es imprescindible una interfase, podría ser una clase abstracta.