Patrones de Diseño
Patrones de Diseño
Patrones de Diseño
E INYECCIÓN
DE DEPENDENCIAS
• Los conceptos de Inversión de Control e Inyección de Dependencias no son nuevos, sino que se remontan
hasta finales de la década de los 80. Sin embargo, estos conceptos han comenzado a popularizarse debido
a la estrecha relación que mantienen con la aparición de Frameworks como Spring en Java o Unity en .NET.
• Inversión de Control
• El concepto de Inversión de Control fue acuñado originalmente por Martin Fowler, diseñador del patrón
MVVM (Model View View-Model). Fowler definió el concepto de forma informal denominándolo como
el Principio de Hollywood, en el que, tras una audición, se le decía al actor la famosa frase de No nos
llames, nosotros te llamaremos.
• El principio establece una diferenciación entre el concepto de biblioteca y framework, definiendo el
primero como un simple conjunto de clases, métodos y funciones que son invocadas por el flujo del
programa y que posteriormente devuelven el control a éste (control normal) y el segundo como un diseño
más abstracto y elaborado que se encargará, en algún momento, de invocar el código que el programador
se encargue de codificar (inversión de control).
• El ejemplo expuesto por Fowler no puede ser más sencillo: un control normal sería un simple programa
secuencial de consola en el que el programa va solicitando datos al usuario y realizando operaciones
(cálculos, visualización por pantalla) al recibir las respuestas. Un programa que aplica una inversión de
control, sin embargo, se podría representar como una ventana compuesta por cajas de texto, etiquetas y
botones. El Framework, en este caso, expondría un bucle de espera que detectaría la emisión de eventos,
como la pulsación de un botón, momento en el cual se ejecutaría el código de usuario. El Framework
invocará nuestro código en lugar de realizar la operación contraria.
• El Principio de Inversión de Dependencias
• El segundo concepto se lo debemos a otro de los gurús de la ingeniería del Software,
Robert C. Martin, creador del desarrollo ágil y de los principios básicos de la programación orientada
a objetos, denominados SOLID.
• Dos de estos principios, de hecho, sirvieron como base para otro de ellos: el concepto de Inversión
de Dependencias:
• Principio Abierto/Cerrado: una entidad software debe ser abierta para su extensión, pero cerrada
para su modificación.
• Principio de Sustitución de Liskov: un objeto siempre debe poder ser reemplazado por una instancia
de una clase derivada sin alterar el funcionamiento del programa, es decir, todos los métodos de una
clase padre deben estar presentes en una clase hija, y esta debe poder asumir el papel de su padre.
• Partiendo de estos principios, Martin escribió un artículo en el que desarrollaría el concepto
de Principio de Inversión de Dependencias. En este artículo establece, a grandes rasgos, que uno de
los grandes problemas del software es el acoplamiento, es decir, la dependencia de clases entre sí.
• El concepto de inversión proviene, por lo tanto, por el giro de 180 grados que se produce respecto al
paradigma habitual en el que los módulos superiores tienden a construirse sobre los inferiores (por
ejemplo, una clase de interfaz de usuario invocando un método de una clase de negocio) y en el que
las abstracciones tienden a depender de los detalles (por ejemplo, una implementación de una
interfaz).
• Inyección de Dependencias
• Llegamos así al tercer concepto en discordia, relacionado con los dos conceptos anteriores: la
inyección de dependencias. Este concepto se basa en hacer que una clase A inyecte objetos en una
clase B en lugar de dejar que sea la propia clase B la que se encargue de crear el objeto (éste último
caso se suele realizar mediante un simple new()).
• La forma más común de realizar la inyección de dependencias es a través de lo que se conoce
como Contenedor DI, que se encarga de realizar la inyección en objetos simples (POJOs o POCOs). Es
probable que, después de esta escueta explicación, haya quien se plantee la pregunta: ¿qué tiene que
ver la inyección de dependencias con el rollo que nos has soltado en los dos apartados anteriores?
Principalmente se basa en que la inyección de dependencias se suele realizar a través de algún tipo de
Framework (Spring, Unity, Castle Windsor…), por lo que se realizará mediante una inversión de
control, haciendo que sea el Framework (concretamente el contendor DI) el que invoque nuestro
código.
• El siguiente ejemplo ilustrará el concepto de inyección de dependencias. Imaginemos que tenemos el
siguiente código, que ilustra el funcionamiento “habitual” de una clase que depende de otra:
Como podemos ver, existe una clase “Vehiculo” que contiene un objeto de la clase “Motor”. La clase
public class Motor
{ “Vehiculo” quiere obtener las revoluciones del motor, por lo que invoca el método GetRevoluciones del
public void Acelerar() objeto Motor y devuelve su resultado. Este caso se corresponde con una dependencia (el módulo
{ superior -vehículo- depende del módulo inferior -motor-).
// ...
} Como primer paso para desacoplar el motor del vehículo podríamos hacer que la clase “Vehiculo” deje
public int GetRevoluciones()
de encargarse de instanciar el objeto “Motor”, pasándoselo como parámetro al constructor. De este
{ modo, la clase “Vehiculo” quedaría de la siguiente manera:
int currentRPM = 0;
public class Vehiculo
// ... {
private Motor m;
return currentRPM;
} public Vehiculo(Motor motorVehiculo)
} {
// El módulo superior ya no instancia directamente el objeto Motor,
public class Vehiculo // sino que éste es pasado como parámetro en el constructor
{ m = motorVehiculo;
private Motor m; }
public Vehiculo() public int GetRevolucionesMotor()
{ {
m = new Motor(); return m.GetRevoluciones();
} }
}
public int GetRevolucionesMotor()
{ Simple, ¿verdad? El constructor del vehículo se encarga de
return m.GetRevoluciones();
}
inyectar la dependencia dentro del objeto, evitando que esta
} responsabilidad recaiga sobre la propia clase. De este modo,
estamos desacoplando ambos objetos. Si hacemos uso de
interfaces, el acoplamiento será incluso menor.
public class MotorDiesel : IMotor
{
public interface IMotor public void Acelerar()
{ {
// Métodos comunes a todos los motores RealizarAdmision();
void Acelerar(); RealizarCompresion();
int GetRevoluciones(); RealizarCombustion(); // Propia del motor diesel
} RealizarEscape();
}
public class MotorGasolina : IMotor
{ public int GetRevoluciones()
public void Acelerar() {
{ int currentRPM = 0;
RealizarAdmision();
RealizarCompresion(); return currentRPM;
RealizarExplosion(); // Propia del motor de gasolina }
RealizarEscape();
} private void RealizarAdmision() { /* ... */ }
private void RealizarCompresion() { /* ... */ }
public int GetRevoluciones() private void RealizarCombustion() { /* ... */ }
{ private void RealizarEscape() { /* ... */ }
int currentRPM = 0; }
// ... public class Vehiculo
{
return currentRPM; private IMotor m;
}
public Vehiculo(IMotor motorVehiculo)
private void RealizarAdmision() { /* ... */ } {
private void RealizarCompresion() { /* ... */ } // El módulo superior ya no instancia directamente el objeto
private void RealizarExplosion() { /* ... */ } Motor,
private void RealizarEscape() { /* ... */ } // sino que éste es pasado como parámetro en el constructor
m = motorVehiculo;
} }
public int GetRevolucionesMotor()
{
return m.GetRevoluciones();
}
}
• Como vemos, el objeto public class Vehiculo
{
Vehiculo ya ni siquiera está private IMotor m;
acoplado a un objeto de la // Se añade una propiedad para poder acceder al motor
public IMotor M
clase Motor, sino que bastará {
un objeto que implemente la get { return m; }
set { m = value; }
interfaz IMotor, }
como MotorGasolina y MotorDi public Vehiculo(IMotor motorVehiculo)
{
esel. A continuación podremos // El módulo superior ya no instancia directamente el objeto Motor,
modificar el vehículo // sino que éste es pasado como parámetro en el constructor
m = motorVehiculo;
añadiéndole una propiedad }
para poder acceder public int GetRevolucionesMotor()
directamente al motor e {
// Se comprueba que el motor existe antes de invocar uno de sus métodos
invocar sus métodos. if (m != null)
return m.GetRevoluciones();
else
return -1;
}
}
• Por último, realizaremos la instanciación public enum TipoMotor
del objeto a través de una clase que {
MOTOR_GASOLINA = 0,
implemente el patrón Factoría. MOTOR_DIESEL = 1
Definiremos una enumeración que }
public class VehiculoFactory
establecerá el tipo de motor que {
deseamos que tenga nuestro vehículo. public static Vehiculo Create(TipoMotor tipo)
{
Vehiculo v = null;
Por lo tanto, hemos conseguido inyectar la
dependencia motor dentro del objeto vehículo switch(tipo)
{
haciendo que el vehículo no dependa del motor, sino case TipoMotor.MOTOR_DIESEL:
que sea la factoría (clase externa a ambos elementos) v = new Vehiculo(new MotorDiesel());
la que se encargue de este trabajo. Para el break;
case TipoMotor.MOTOR_GASOLINA:
programador, la instanciación del objeto no tendrá v = new Vehiculo(new MotorGasolina());
mucha más complicación, ya que pasará de instanciar break;
el objeto de esta forma: default:
break;
}
return v;
}
}
• Nótese que la diferencia es estructural: el resultado será el mismo (un
vehículo con un motor), pero si el día de mañana fuese necesario
extender la aplicación (por ejemplo, si el cliente necesita un vehículo
que funcione con un motor de hidrógeno), será suficiente con crear
una nueva clase que implemente la interfaz IMotor y añadir el
proceso de instanciación en la factoría. De este modo, no sería
necesario modificar nada de lo codificado anteriormente. En caso de
haberlo codificado por la vía “habitual”, sería necesario realizar
cambios en la clase vehículo para implementar esta nueva
funcionalidad.
INTRODUCCIÓN A LA
INYECCIÓN DE DEPENDENCIAS
MEDIANTE UNITY
• Tras una pequeña introducción a la inversión de control y a la
inyección de dependencias, veremos cómo se comportan los
contenedores DI en una aplicación práctica. Para ello hablaremos de
Unity, desarrollado por Microsoft y perteneciente al paquete
Enterprise Library.
• Unity es, por tanto, un Framework de inyección de dependencias o DI
Container. Puede ser descargado desde Codeplex o utilizando NuGet
dentro de Visual Studio, método este último que utilizaremos en
nuestro ejemplo.
• Comenzaremos creando un nuevo proyecto de consola al que
llamaremos, por ejemplo, UnityExample.
A continuación haremos uso de NuGet para añadir
nuestra referencia a Unity. Será tan sencillo como
hacer click derecho sobre las referencias del
proyecto y seleccionar la opción Manage NuGet
Packages…
• Seleccionaremos la opción Online en el árbol de la derecha y
escribiremos Unity en la caja de búsqueda para obtener el paquete.
Una vez encontrado, pulsaremos sobre el botón Install.
• Creando las clases para el ejemplo
• Realizaremos un ejemplo similar al del artículo anterior, salvo que en
lugar de utilizar motores y coches, imaginaremos una ludoteca con
mesas, en cada una de las cuales puede jugarse a un juego de mesa. La
clase Table será la engargada de modelar la mesa, mientras que la
interfaz IGame simbolizará las operaciones comunes a todos los juegos:
añadir jugadores, eliminarlos y mostrar el estado actual del juego.
• Crearemos un par de juegos que implementarán la
interfaz: TrivialPursuit y TicTacToe (el típico tres en raya).
• Comenzaremos codificando la interfaz. {
public interface IGame
Finalmente,
{
private IGame game;
public Table(IGame game)
{
this.game = game;
}
codificaremos la
clase Table que contendrá
public string GameStatus()
{
una interfaz IGame que
return game.result();
}
public void AddPlayer()
{
game.addPlayer();
}
será inyectada a través de
su constructor.
public void RemovePlayer()
{
game.removePlayer();
}
public void Play()
{
game.play();
}
}
• Registro de tipos Lo primero que deberemos hacer será instanciar un
contenedor de Unity y registrar los tipos que queramos que resuelva
por nosotros. Como primer ejemplo, registraremos la
interfaz IGame con la clase TrivialPursuit. Esto significará que, cuando
resolvamos las dependencias, Unity instanciará un objeto de la
clase TrivialPursuit en el momento en el que resolvamos un objeto
que posea una dependencia de tipo IGame.
static void Main(string[] args)
{
// Declaramos un contenedor Unity
var unityContainer = new UnityContainer();
// Registramos IGame para que cuando se detecte la dependencia
// proporcione una instancia de TrivialPursuit
unityContainer.RegisterType<IGame, TrivialPursuit>();
}
• Resolución de tipos Por tanto, cada vez que le pidamos a Unity que
resuelva una interfaz IGame, el Framework buscará si tiene registrado
el tipo y, en caso afirmativo, proporcionará una instancia de la clase
que se haya mapeado (en este caso, TrivialPursuit:
Si le indicamos a Unity que resuelva una instancia de la clase Table, lo que lograremos será que se inyecten
todas aquellas dependencias que Unity tiene registradas, es decir, nos proporcionará una instancia de la
clase Table inyectando un objeto de la clase TrivialPursuit en el momento en el que se encuentre con una
interfaz IGame:
namespace Patterns.Factory.SimpleFactory.Interfaces
{
public interface IMotor
{
public int Estabilidad { get; set; }
public decimal ParMotor { get; set; }
public int Potencia { get; set; }
public decimal Rendimiento { get; set; }
public int VelocidadNominal { get; set; }
string ConsumirCombustible();
string InyectarCombustible();
string RealizarEscape();
string RealizarExpansion();
}
}
public class MotorDiesel : IMotor
{
#region IMotor Members
int Estabilidad { get; set; }
decimal ParMotor { get; set; }
int Potencia { get; set; }
decimal Rendimiento { get; set; }
int VelocidadNominal { get; set; }
public string ConsumirCombustible()
{
return RealizarCombustion();
}
public string InyectarCombustible(int cantidad)
{
return string.Format("MotorDiesel: Inyectados {0} ml. de Gasoil.", cantidad);
}
public string RealizarEscape()
{
return "MotorDiesel: Realizado escape de gases";
}
public string RealizarExpansion()
{
return "MotorDiesel: Realizada expansion";
}
#endregion
private string RealizarCombustion()
{
return "MotorDiesel: Realizada combustion del Gasoil";
}
}
public class MotorGasolina : IMotor
{
#region IMotor Members
public int Estabilidad { get; set; }
public decimal ParMotor { get; set; }
public int Potencia { get; set; }
public decimal Rendimiento { get; set; }
public int VelocidadNominal { get; set; }
public string ConsumirCombustible()
{
return RealizarExplosion();
}
public string InyectarCombustible(int cantidad)
{
return string.Format("MotorGasolina: Inyectados {0} ml. de Gasolina.", cantidad);
}
public string RealizarEscape()
{
return "MotorGasolina: Realizado escape de gases";
}
public string RealizarExpansion()
{
return "MotorGasolina: Realizada expansion";
}
#endregion
private string RealizarExplosion()
{
return "MotorGasolina: Realizada explosion de la Gasolina";
}
}
• Finalmente, crearemos una clase MotorFactory que exhiba un
método público CreateInstance(nombreClase) para instanciar, por
ejemplo, un motor Diesel.
public class MotorFactory
{
public IMotor CreateInstance(string tipoMotor)
{
IMotor resultado;
switch (tipoMotor)
{
case "MotorDiesel":
resultado = new MotorDiesel() { Estabilidad = 100, Potencia = 40, Rendimiento = 800, VelocidadNominal = 0 };
break;
default:
resultado = null;
break;
}
return resultado;
}
}
• Lo que estamos haciendo aquí no es otra cosa que delegar
en MotorFactory la tarea de instanciar el motor. Ya tenemos el motor
Diesel. Nos falta ahora el Gasolina. Para ello deberemos modificar la
clase MotorFactory y añadir un nuevo case a la sentencia switch:
public class MotorFactory
{
public IMotor CreateInstance(string tipoMotor)
{
IMotor resultado;
switch (tipoMotor)
{
case "MotorDiesel":
resultado = new MotorDiesel();
break;
case "MotorGasolina":
resultado = new MotorGasolina();
break;
default:
resultado = null;
break;
}
return resultado;
}
}
• Si quisiéramos obtener un motor diesel, por lo tanto, bastaría con el siguiente
código:
static void Main(string[] args)
{
MotorFactory factory = new MotorFactory();
IMotor motorDiesel = factory.CreateInstance("MotorDiesel");
if (motorDiesel == null)
return;
Console.WriteLine(motorDiesel.InyectarCombustible(20));
Console.WriteLine(motorDiesel.ConsumirCombustible());
Console.WriteLine(motorDiesel.RealizarExpansion());
Console.WriteLine(motorDiesel.RealizarEscape());
Console.ReadLine();
}
• Principios SOLID Lo que acabamos de hacer es delegar la decisión de instanciar
un motor u otro hasta el momento en el que la variable tipoMotor llega a
nuestro método CreateInstance, lo cual flexibiliza el código permitiendo que no
nos atemos a un tipo de motor concreto en tiempo de compilación. Sin
embargo, al aparecer un nuevo tipo de motor hemos tenido que modificar una
clase para incluir el nuevo elemento a instanciar.
• Si recordamos el artículo sobre la inyección de dependencias, comentamos por
encima los Principios SOLID propuestos por Robert C. Martin, que eran los
siguientes:
• Responsabilidad única: un objeto sólo debe tener una única responsabilidad.
• Abierto/cerrado: una clase debe estar abierta para su extensión, pero cerrada para su
modificación.
• Principio de sustitución de Liskov: una clase padre siempre debe poder ser sustituida por
una clase hija sin alterar el comportamiento del programa.
• Segregación de la Interfaz: es preferible contar con muchas interfaces específicas que
con una de propósito general.
• Inversión de dependencia: se debe depender de abstracciones, no de concreciones.
• Si cada vez que aparezca un nuevo motor tenemos que modificar nuestra
factoría para darle cabida en nuestro proceso de instanciado, estaremos
violando directamente el principio abierto/cerrado, ya que las clases
deben extenderse, no modificarse.
• Por lo tanto, lo que estamos afirmando es que es necesario encontrar un
modo de generalizar nuestra factoría de tal modo que no sea necesario
modificarla en el caso de que un nuevo tipo se añada al ensamblado. El
problema, a simple vista, parece irresoluble por métodos ajenos a la
brujería. Sin embargo, recordemos que la razón de ser de los patrones de
diseño era precisamente esta: proporcionar soluciones generales a
problemas concretos. Y este problema concreto tiene una solución más
sencilla de lo que parece: bucear en las tripas del ensamblado y generar
un listado de pares clave-valor con los tipos que se ajusten a lo que
necesitamos. Entraremos, por lo tanto, en el campo de Reflection.
• Configurando la factoría mediante un fichero XML Un primer acercamiento
puede ser el siguiente: crear una sección en nuestro fichero de configuración (o
cualquier otro fichero XML, base de datos…) que contenga el nombre de las
clases que nuestra factoría puede generar. El constructor de la factoría iterará
sobre estos elementos y creará un diccionario con pares clave-valor cuya clave
será el nombre del tipo indicado en el fichero y cuyo valor correspondiente será
el tipo en sí. Por tanto, añadiremos una nueva sección a nuestro fichero
app.config, del siguiente modo:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="AllowedMotorTypes" type="System.Configuration.NameValueSectionHandler" />
</configSections>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
<AllowedMotorTypes>
<add key="Patterns.Factory.SimpleFactory.Motores.MotorDiesel" value="true" />
<add key="Patterns.Factory.SimpleFactory.Motores.MotorGasolina" value="true" />
</AllowedMotorTypes>
</configuration>
• A continuación añadiremos una referencia a la biblioteca System.Configuration, y
haremos uso del siguiente código para lograr nuestro objetivo: instanciaremos un
objeto de la clase NameValueCollection que simbolizará la sección que hemos
creado, y usaremos una sentencia LINQ para iterar sobre todas las claves de la
sección. Sobre esta consulta usaremos el método de
extensión ToDictionary(campoClave, campoValor) en los que indicaremos el
contenido de keypara la clave y, a través de Reflection, recuperaremos el tipo
correspondiente a esa clave para almacenarlo como valor.
private void ConfiguracionDesdeXml()
{
// Extraemos los pares clave-valor del fichero de configuracion, concretamente de
// la seccion AllowedMotorTypes
NameValueCollection settings = ConfigurationManager.GetSection("AllowedMotorTypes") as NameValueCollection;
if (settings != null)
{
// Instanciamos el diccionario<nombreTipo, tipo>
tiposInstanciables = (from clave in settings.AllKeys // Recorremos todas las claves
where bool.Parse(settings[clave]) // ...en las que value sea "true"
select clave).ToDictionary(key => key, // La clave del diccionario sera "key"
key => Assembly.GetExecutingAssembly().GetType(key)); // El valor sera el tipo
}
}
• Configurando la factoría mediante las clases del ensamblado
• Una segunda posibilidad consiste en recorrer todas las clases existentes en el
ensamblado, comprobar si implementan la interfaz IMotor y, en caso
afirmativo, añadirla a un diccionario en el que se relacione el nombre de la
clase con el tipo que representa. Una vez que se invoque el
método CreateInstance, se consultará el diccionario usando el parámetro del
método como clave y se instanciará el tipo asociado mediante la
clase Activator.
• Resumiendo, el proceso será el siguiente:
• Al instanciar la factoría, se recorren todas las clases del ensamblado.
• Por cada clase del ensamblado
• Si la clase implementa la interfaz que nuestra factoría se encarga de instanciar, se añade al
diccionario(nombreClase, tipo)
• Al invocar el método CreateInstance(nombreClase), se consulta el diccionario
y se recupera el tipo asociado, instanciándolo mediante Activator.
private void ConfiguracionAutomatica()
{
// Usamos LINQ para obtener un diccionario (nombreClase, tipoClase) a partir de todos
// aquellos tipos del ensamblado actual que implementen la interfaz IMotor.
tiposInstanciables = (from tipo in Assembly.GetExecutingAssembly().GetTypes()
where tipo.GetInterface(typeof(IMotor).ToString()) != null
select tipo).ToDictionary(t => t.ToString(), // Clave: nombre del tipo
t => t); // Valor: tipo
}
Como vemos, el proceso de recorrer el ensamblado no es más que una forma de configurar la factoría. Este proceso no
tiene por qué ser como el que aquí se expone: puede recorrerse un fichero XML, una base de datos o cualquier otra forma
de proporcionar a nuestra factoría un método de saber qué clases puede instanciar.
El siguiente paso será obtener el tipo a partir de la cadena de texto que recibirá nuestro método CreateInstance(). Esto será
tan sencillo como consultar el diccionario y devolver el resultado.
En caso de que el constructor que queremos invocar requiriese por ejemplo un valor entero, le pasaríamos un
array con un único elemento con el tipo entero a GetConstructor y un array con un único elemento entero al
método Invoke.
// Si tuviesemos un constructor del estilo a MotorFactory(int dato), esta ultima invocacion
// equivaldria a "new MotorFactory(34)":
IMotor resultado = (IMotor)ObtenerTipo(tipoMotor).GetConstructor(new[] {typeof(int) }).Invoke(new object[] {34} );
Patrón Factory Method
• Hasta ahora hemos explicado el concepto de factoría: una clase especializada en crear objetos. A partir de
aquí, podemos especializar aún más estas factorías para que generen tipos concretos. El patrón Factory
Method es similar a lo que hemos visto hasta ahora, con una pequeña variación: el
método CreateInstance() ya no generará una instancia, sino que o bien se convertirá en abstracto o bien
pertenecerá a una interfaz y dejará a las clases que la implementen la tarea de codificar su comportamiento.
Por lo tanto, nuestra clase MotorFactory ya no podrá utilizarse directamente para generar objetos, sino que
habrá que crear una nueva clase que herede de MotorFactory que implemente el método CreateInstance() o
bien transformar MotorFactory en una interfaz IMotorFactory y dejar a las clases que la implementen el
trabajo de instanciar las clases.
• ¿Qué conseguimos con esto? Básicamente, especializar las factorías. En lugar de tener una única factoría que
centralice todo el proceso de creación de objetos, tendremos varias clases factoría que heredan
de MotorFactory (o implementan IMotorFactory) que estarán especializadas en crear variantes concretas.
• En nuestro ejemplo sólo exponemos el caso de la instanciación de dos clases: MotorDiesel y MotorGasolina.
Además, las diferencias entre ambas clases son mínimas, por lo que una factoría simple nos bastaría para
nuestro diseño. Pero, ¿qué ocurriría si estas clases se especializaran cada vez más y más? ¿Y si se añadiesen
muchos más tipos de motores? Quizás la carga funcional sobre nuestra factoría sería demasiada, por lo que
sería aconsejable pasar al siguiente paso: utilizar Factory Method para especializar las factorías.
• Vamos a realizar, por lo tanto, el siguiente proceso:
• 1) Eliminar la clase MotorFactory y sustituirla por la
interfaz IMotorFactory. Esta nueva interfaz expondrá el
método CreateInstance(), que deberá ser implementado por otras
clases.
• 2) Crear dos nuevas
clases, MotorDieselFactory y MotorGasolinaFactory, que
implementen la interfaz IMotorFactory y su método CreateInstance(),
especializando el proceso de instanciado. El nombre del
patrón Factory Method viene precisamente de aquí.
• Fijémonos en que el método CreateInstance sigue devolviendo una
interfaz IMotor. Podríamos pensar en que sería mejor
que MotorGasolinaFactory.CreateInstance() devolviera una instancia
de MotorGasolina, ¿verdad? Bien, en realidad lo hará, pero
recordemos que MotorGasolina implementa la interfaz IMotor, por lo
que puede usar esta referencia para devolver una instancia
de MotorGasolina tal y como lo ha venido haciendo hasta ahora.
Dejemos el nivel de abstracción lo más alto posible: la concreción no
es buen amigo del diseñador.
• Por tanto, el código de la interfaz IMotorFactory se simplificará hasta
el punto de exponer únicamente la firma del
método CreateInstance(): public interface IMotorFactory
{
IMotor CreateInstance();
}
• Configurando la factoría por defecto
• Anteriormente vimos cómo podíamos configurar qué clases estaban
disponibles para su instanciado: mediante el recorrido de todas las
clases del ensamblado que implementaban la interfaz IMotor o
mediante su inclusión en el fichero de configuración .config.
• Sin embargo, en lugar de usar un fichero .config para estas tareas, es
más aconsejable usar para ello un fichero .settings. Este fichero
consiste en una especie de diccionario al que se puede acceder
mediante la clase Settings (o Properties.Settings, dependiendo del
Framework). Para añadir un fichero de este tipo, agregaremos un
nuevo elemento a nuestro proyecto de tipo Settings File.
A continuación, añadiremos un nuevo par clave-valor. El valor consistirá en el nombre
de la clase de la factoría que queremos utilizar. Por supuesto, podríamos utilizar varios
pares clave-valor (en la práctica es lo que se hace) dependiendo del contexto. Esto nos
puede servir, entre otras cosas, para instanciar distintos elementos dependiendo de si
estamos desarrollando en producción, pruebas unitarias, en desarrollo…
Hecho esto, creamos un método ObtenerFactoria() que devuelva una interfaz IMotorFactory que alojará
la instancia de la factoría concreta que queremos utilizar y que vendrá determinada por el contenido del
valor que hemos guardado en la configuración.
Como observamos, las interfaces DbConnection y DbDataAdapter efectúan las mismas operaciones sobre una base de
datos Oracle y SQL Server. Las factorías proporcionan estos elementos, junto a algunos más. Por lo tanto, la diferencia
fundamental entre Factory Method y Abstract Factory radica en que el segundo patrón se centra en familias
completas de objetos en lugar de hacerlo con una única clase.
Usando este tipo de factorías es posible ampliar la funcionalidad de nuestras clases respetando el Principio
Abierto/Cerrado. El nombre de la clase de la nueva factoría (en el ejemplo
anterior Oracle.DataAccess.Client y System.Data.SqlClient) permitirá a la factoría abstracta instanciar nuevas factorías a
medida que las nuevas familias de objetos se vayan añadiendo.
• ¿Cuándo utilizar este patron? Ejemplos reales
• Ya hemos visto un ejemplo real de factoría abstracta: la
clase DbProviderFactory, que proporciona factorías para diversas
familias de bases de datos y permiten crear desde conexiones hasta
adaptadores de datos (DataAdapter).
• Este patrón se utiliza también a la hora de definir elementos de
interfaces gráficas, tales como botones, paneles, etc. Dependiendo del
motor gráfico que se vaya utilizar. Por ejemplo, se utilizaría una factoría
para elementos gráficos de Gtk, otra para elementos gráficos de KDE,
otra para elementos gráficos de xfce…
• Si se cuenta con varios entornos (producción, desarrollo, pruebas)
también puede ser útil para, mediante el fichero de configuración,
instanciar determinados objetos dependiendo de éste (por ejemplo,
para realizar el mocking de las pruebas unitarias).
PATRON BUILDER
• Objetivo: “Separar la construcción de un objeto complejo de su
representación, de modo que el mismo proceso de construcción
pueda crear representaciones diferentes.”
• Hablando en plata, el patrón Builder es un patrón creacional cuyo objetivo es instanciar objetos
complejos que generalmente están compuestos por varios elementos y que admiten diversas
configuraciones. Cuando hablamos de “construcción” nos referimos al proceso, mientras que
cuando hablamos de “representación” nos estaremos refiriendo a los datos que componen el
objeto. Se encargará, por tanto, de encapsular todo el proceso de generación de modo que
únicamente necesite los detalles necesarios para “personalizar” el objeto, devolviendo como
resultado una instancia del objeto complejo que deseamos construir. Es un patrón fuertemente
ligado a otro, el patrón estructural Composite.
Vehiculo v = new Vehiculo();
v.NumPuertas = 5; Como vemos, la lógica de nuestro programa construye un
v.Matricula = "1034 CAA"; vehículo compuesto de muchas partes (de hecho, podríamos
v.Faros = new Faro[4];
v.Faros[0] = new Faro(TipoFaro.Xenon); seguir añadiendo detalles) de forma secuencial. El
v.Faros[1] = new Faro(TipoFaro.Xenon); patrón Builder se encargará de encapsular todo este proceso
v.Faros[2] = new Faro(TipoFaro.Lexus);
v.Faros[3] = new Faro(TipoFaro.Lexus); haciendo que, mediante la obtención de estos detalles, el
v.Color = "Rojo"; proceso de generación de este objeto sea transparente para
v.Motor = new Motor(TipoMotor.Gasolina);
v.Motor.Capacidad = 2200; quien lo solicita.
v.Motor.Valvulas = 12;
DirectorConstruccion director = new DirectorConstruccion (new ConstructorFordFiestaSportEdition());
Vehiculo v = director.ConstruirVehiculo();
• El resultado a nivel computacional será más o menos el mismo, ya
que el proceso de “ensamblado” debe realizarse de todos modos. La
diferencia radica en que la lógica del programa no necesita
saber cómo ensamblar el objeto, sino únicamente aquellas piezas que
quiere que éste disponga. Si vas a comprar un vehículo, no le indicarás
al vendedor el proceso de fabricación de éste paso a paso: él
únicamente debe proporcionarte un vehículo que se ajuste a las
características que tú le solicitas, dejándole a él los detalles de cómo
el vehículo es construido.
• Elementos del patrón
• Utilizaremos el ejemplo de los vehículos para entender cómo funciona este patrón.
Comenzaremos por el final: el producto que queremos obtener. Se trata del objeto complejo que
queremos obtener, que suponemos que estará compuesto por un buen número de elementos.
Imaginemos que queremos obtener un vehículo y que su estructura es la siguiente:
// Tipo de rueda: diámetro, llanta y neumático
public class Rueda // Motor diesel, que implementará la interfaz IMotor
{ public class MotorDiesel : IMotor
public int Diametro { get; set; } {
public string Llanta { get; set; } #region IMotor Members
public string Neumatico { get; set; }
public string ConsumirCombustible()
}
{
// Tipo de carrocería
return RealizarCombustion();
public class Carroceria
}
{
public bool HabitaculoReforzado { get; set; }
public string InyectarCombustible(int cantidad)
public string Material { get; set; }
{
public string TipoCarroceria { get; set; }
return string.Format("MotorDiesel: Inyectados {0} ml. de Gasoil.", cantidad);
}
}
// Interfaz que expone las propiedades del motor
public interface IMotor public string RealizarEscape()
{ {
string ConsumirCombustible(); return "MotorDiesel: Realizado escape de gases";
string InyectarCombustible(int cantidad); }
string RealizarEscape();
string RealizarExpansion(); public string RealizarExpansion()
} {
return "MotorDiesel: Realizada expansion";
}
#endregion
private string RealizarCombustion()
{
return "MotorDiesel: Realizada combustion del Gasoil";
}
}
• Finalmente, la clase que simboliza el vehículo estará formada por los elementos anteriores más algún atributo más. El
producto, como podemos imaginar, será el elemento final una vez haya sido correctamente configurado. Marquémonos
como objetivo la construcción del siguiente vehículo: Audi A3 Sportback. Este vehículo constará de llantas de aluminio de
17cm y neumáticos Michelín, color plata cromado, cierre centralizado y dirección asistida, con una carrocería reforzada de
fibra de carbono. Ya tenemos el objetivo. Ahora necesitamos un constructor (builder) que lo construya por nosotros.
public class Vehiculo
{
public bool CierreCentralizado { get; set; }
public string Color { get; set; }
public bool DireccionAsistida { get; set; }
public string Marca { get; set; }
public string Modelo { get; set; }
public IMotor Motor { get; set; }
public Carroceria TipoCarroceria { get; set; }
public Rueda TipoRuedas { get; set; }
public string GetPrestaciones()
{
StringBuilder sb = new StringBuilder();
string nl = Environment.NewLine;
sb.Append("El presente vehiculo es un ").Append(Marca).Append(" ").Append(Modelo);
sb.Append(" estilo ").Append(TipoCarroceria.TipoCarroceria).Append(nl);
sb.Append("Color: ").Append(Color).Append(nl);
sb.Append(DireccionAsistida ? "Con " : "Sin ").Append("direccion asistida").Append(nl);
sb.Append(CierreCentralizado ? "Con " : "Sin ").Append("cierre centralizado").Append(nl);
sb.Append("Carroceria de ").Append(TipoCarroceria.Material);
sb.Append(TipoCarroceria.HabitaculoReforzado ? " con " : " sin ").Append("el habitaculo reforzado").Append(nl);
sb.Append("Ruedas con llantas ").Append(TipoRuedas.Llanta).Append(" de ").Append(TipoRuedas.Diametro).Append(" cm").Append(nl);
sb.Append("Neumaticos ").Append(TipoRuedas.Neumatico);
sb.Append("Respuesta del motor: ").Append(Motor.InyectarCombustible(100));
return sb.ToString();
}
}
• La clase Builder y los constructores concretos
• La clase constructora será la encargada de construir nuestro objeto. Sin embargo, esta clase no
debe preocuparse de cómo debe construir el producto, sino únicamente de qué partes han de
construirse. Al igual que en una fábrica de ensamblaje, el peón debe centrarse en una tarea
concreta de construcción, dejando el proceso del ensamblaje ordenado para otros
trabajadores.
• Sin embargo, antes de ponernos a codificar el contenido de nuestro constructor, es preciso
pensar un poco en la (más que probable) posibilidad de que otro cliente decida, en algún
momento dado, un vehículo diferente. Por ello, definiremos una interfaz a la que todo
constructor deberá ceñirse para asegurar la compatibilidad del proceso. Todos los constructores
de coches deberán saber añadir ruedas, carrocería o motor, por lo que crearemos una
interfaz IVehiculoBuilder que implemente todas estas operaciones. Recordemos que las
operaciones deben ceñirse al proceso de construir las partes, nunca de cómo combinarlas (ni
en qué orden.
• Otra posibilidad, en lugar de utilizar una interfaz, será la de hacer uso de una clase que defina la
funcionalidad común y declare como abstractos los métodos que deberá implementar el
constructor concreto para cada coche en particular. Ciñámonos a esta segunda opción y
creemos la clase VehiculoBuilder, que será como sigue:
• La interfaz tendrá un aspecto como el siguiente:
public class A3SportbackBuilder : VehiculoBuilder
public abstract class VehiculoBuilder {
{
public override void DefinirVehiculo()
// Declaramos la referencia del producto a
{
construir
v = new Vehiculo();
protected Vehiculo v; v.Marca = "Audi";
v.Modelo = "A3 Sportback";
// Declaramos el método que recuperará el objeto }
Estos patrones están pensados más en como crear clases nuevas sin
modificar las existentes O/C
PATRÓN ADAPTER (WRAPPER)
• Objetivo: Convertir la interfaz de una clase en otra interfaz que el cliente
espera. Adapter consigue que trabajen juntas clases que de otro modo no
podrían.
• El patrón Adapter nos abre el camino hacia el segundo grupo de patrones
propuestos por el Gang of Four: los patrones estructurales. Si bien los
patrones de creación definían la forma en la que los objetos son
instanciados, los patrones estructurales se basan en la forma en la que un
conjunto de clases se relacionan entre sí para proporcionar una funcionalidad
compleja, proporcionando una estructura para conseguir lograr ese objetivo.
• La filosofía de Adapter, al igual que vimos con Prototype, es tan simple como
autoexplicativa: establecer una capa intermedia que permita comunicarse a
dos clases que de otro modo no podrían hacerlo, realizando
una adaptación de la interfaz de la clase que proporciona el servicio a la que
la solicita.
• Representándolo de una forma visual, imaginemos que nuestro objeto
pertenece a la clase Taladro y requiere hacer uso del método Flujo110V() de
una clase que implemente la interfaz IEnchufeIngles. Por tanto, nuestro
taladro está preparado para hacer uso de esa interfaz y espera recibir como
valor de retorno un flujo con una diferencia de potencial de 110V
• Sin embargo, resulta que la clase de la que disponemos en el otro
extremo no implementa esta interfaz, sino otra
llamada IEnchufeEuropeo() que dispone de un método capaz de
devolver a nuestro taladro un flujo de 220V, cuyo nombre
es Flujo220V(). La funcionalidad es parecida, pero no la esperada, y la
interfaz que el taladro espera utilizar no es compatible con el
elemento del subsistema. Nos encontramos, por tanto, con un claro
problema de compatibilidad.
• Para que nuestro taladro pueda comunicarse con IEnchufeEuropeo, será
necesario modificar uno de los dos extremos para que la interfaz de
comunicación coincida, pero si hiciéramos esto, nuevamente estaríamos
rompiendo el principio abierto/cerrado del que hablamos con anterioridad,
amén de que cambiando esta interfaz haríamos que de repente dejasen de
compilar todas aquellas clases que la estuvieran utilizando hasta el momento.
• La solución adecuada consistirá en adaptar los requerimientos de la interfaz de
la clase cliente (taladro) a los servicios que la clase servidora (sistema eléctrico
europeo) puede ofrecer. Hablando en plata, y tal como indica el propio nombre
del patrón, necesitaremos un adaptador que haga coincidir la entrada de un
elemento con la salida del otro. Este será el objetivo del patrón Adapter
• Por lo tanto, cuando nos encontremos con un problema similar a este,
lo más adecuado será codificar una nueva clase que implemente la
interfaz original (IEnchufeIngles) y que posea una propiedad cuyo tipo
será el de la clase que se pretende adaptar. En este caso, nuestra
clase adaptador contendrá una referencia a una instancia de una clase
que implemente IEnchufeEuropeo. De este modo, nuestro taladro
hará una llamada a la clase que espera (IEnchufeIngles.Flujo110V()) y
de forma interna se invocará el método Flujo220V() mediante la
instancia de EnchufeEuropeo.
• Codificando un Adaptador
• Un adaptador no sólo se encarga de transformar una interfaz en otra:
también puede realizar otro tipo de operaciones, principalmente de
transformación. En el ejemplo de los enchufes, además de transformar
una interfaz en otra (los polos del enchufe), nuestro adaptador podía
también realizar otras operaciones, como por ejemplo transformar la
diferencia de potencial del flujo de salida de 220 a 110, ya que de lo
contrario correríamos el riesgo de quemar nuestro dispositivo.
• Comencemos proporcionando las
interfaces IEnchufeIngles e IEnchufeEuropeo y dos clases que las
implementen:
public interface IEnchufeIngles
{
// Devuelve un array de enteros con un voltaje de unos 110V
int[] Flujo110V();
// Devuelve el numero de bornes del enchufe
int getNumeroBornes(); public class EnchufeBritanico : IEnchufeIngles
} {
#region IEnchufeIngles Members
// Devuelve un array con voltajes en distintos momentos
public int[] Flujo110V()
{
int[] arrayFlujo = new int[100];
Random r = new Random();
for (int i = 0; i < arrayFlujo.Length; i++)
{
// Calculamos la fluctuacion del voltaje
int fluctuacion = r.Next(100) > 50 ? 1 : -1; // Positiva o negativa
fluctuacion = fluctuacion * (r.Next(7) + 1); // El valor fluctuara entre -7 y +7
// Valor total entre 103 y 117V
arrayFlujo[i] = fluctuacion + 110;
}
return arrayFlujo;
}
// Devuelve el numero de bornes del enchufe
public int getNumeroBornes()
{
return 3;
}
#endregion
}
public interface IEnchufeEuropeo
{
// Devuelve un array de enteros con un voltaje de unos 220V
int[] Flujo220V();
// Devuelve el numero de bornes del enchufe
int getNumeroBornes(); public class EnchufeEspanol : IEnchufeEuropeo
} {
#region IEnchufeEuropeo Members
// Devuelve un array con voltajes en distintos momentos
public int[] Flujo220V()
{
int[] arrayFlujo = new int[100];
Random r = new Random();
for (int i = 0; i < arrayFlujo.Length; i++)
{
// Calculamos la fluctuacion del voltaje
int fluctuacion = r.Next(100) > 50 ? 1 : -1; // Positiva o negativa
fluctuacion = fluctuacion * (r.Next(10) + 1); // El valor fluctuara entre -0 y +10
// Valor total entre 210 y 230V
arrayFlujo[i] = fluctuacion + 220;
}
return arrayFlujo;
}
// Devuelve el numero de bornes del enchufe
public int getNumeroBornes()
{
return 2;
}
#endregion
Ahora codificaremos nuestra clase Taladro, que posee un
enchufe inglés (es decir, posee una referencia a una
interfaz IEnchufeIngles) para recibir la alimentación:
Todos los robots con los que trabajemos, implementarán dicha interface, ya que es la que mejor se adapta a
nuestras necesidades. Ahora solo tenemos que desarrollar el adaptador en si, que en este caso tendrá el
siguiente código:
Como se puede ver, nuestro CoolCorpRobot implementa
class CoolCorpRobot:CoolCorpInterface
{
la interface que hemos definido antes. En el
private USRobot robot; constructor, creamos un objeto USRobot, que al
public CoolCorpRobot() final es el que realiza las operaciones. La clase
{ no implementa el método EnablePartialFirstLawMode
this.robot = new USRobot(); porque no lo necesitamos.
} Y ya está. Para usar el adaptador, bastará con
public float CurrentSpeedInKilometersPerHour utilizar algo similar a esto:
{
get class Program
{ {
return this.robot.CurrentSpeedInMilesPerHour * 1.6093f; static void Main(string[] args)
{
}
CoolCorpInterface robot = new CoolCorpRobot();
set robot.CurrentSpeedInKilometersPerHour = 3;
{ robot.Jump(12);
this.robot.CurrentSpeedInMilesPerHour = value * 0.62137f; Console.ReadKey();
} }
}
}
public void Jump(float meters) ¿Y si USRobots modifica su interface? Pues nosotros solo tendremos
{ que modificar el adaptador. Mientras que respetemos nuestra
this.robot.Jump(meters * 0.3048f);
interface, no serán necesarios cambios en otras partes del código.
}
}
¿Y si USRobots saca a la venta un nuevo robot, pero hay que
mantener en uso las versiones antiguas? En este caso lo que
tendremos que hacer es crear un nuevo adaptador, que respete
nuestra CoolCorpInterface. Así los clientes no se verán afectados.
Y más si en lugar de utilizar new inyectamos la dependencia con el
adaptador adecuado.
Otras cosas a tener en cuenta
• El patrón Adapter se puede implementar de dos maneras: con
objetos o con clases. Este artículo utiliza la primera opción. Para
implementar el patrón con clases necesitaríamos un lenguaje que
soportase multiherencia. No es el caso de C#.
• Existe otra versión de este patrón denominada Two-Way Adapter. La
idea es similar, pero en este caso, el adaptador implementa las
interface original y la interface adaptada. Algo útil si necesitamos
ambas funcionalidades.
PATRÓN FACADE
• Objetivo: Proporcionar una interfaz unificada a otro conjunto de
interfaces de un subsistema. Façade define una interfaz de más alto
nivel que hace que el subsistema más fácil de usar.
Cuando vimos el patrón Adapter dijimos que el patrón Facade (fachada) estaba muy
relacionado con él. Si bien ambos patrones se basan en el mismo principio (encapsular
la interfaz de otros elementos), es el motivo y el uso que se le va a dar lo que las
diferencia:
• El patrón Adapterconvierte una interfaz en otra, haciendo que la clase cliente pueda
utilizar los métodos de una clase para cuya interfaz no estaba originalmente
preparado.
• El patrón Facade, sin embargo, no realiza ninguna transformación, sino que se limita
a simplificar y centralizar un conjunto de invocaciones en un único punto, a la vez
que desacopla al cliente del subsistema (siendo la clase Facade la que quedaría
acoplada). Es importante tener en cuenta que el patrón Facade no realiza operaciones
de transformación, ya que las clases del subsistema, pese a ser envueltas con este
elemento, pueden seguir siendo accesibles de forma normal en caso de que fuese
necesario, al contrario de lo que ocurre con el patrón Adapter. Un Facade es una
forma de simplificar las cosas: es decisión del diseñador si las clases del subsistema
son accedidas de forma directa o a través de la fachada.
• Resumiendo, Adapter transforma mientras que Facade simplifica y desacopla.
• Cambio manual vs. Cambio automático
• Seguiremos hablando de vehículos para ilustrar nuestros patrones. Un
ejemplo de este patrón aplicado a la vida real sería la diferencia entre
un sistema de cambio manual y un sistema automático. En el primer
caso, el conductor debe interactuar con el embrague, el acelerador y
la palanca de cambios, mientras que en el segundo caso, el conductor
únicamente tiene que preocuparse de acelerar o decelerar, puesto
que será el propio vehículo el que se encargará de realizar todo el
proceso.
• Veamos cómo funcionará el sistema antes de aplicar el patrón Facade:
Tal y como hemos explicado, el conductor debe interactuar con tres elementos del subsistema para realizar un
cambio de marcha. Codificaremos las interfaces del subsistema con las que interactúa el conductor para
comprobar cuál sería su uso normal.
public interface IPalancaCambios public class PalancaCambios : IPalancaCambios
{ {
private int _velocidadActual;
void InsertarVelocidad(int velocidad);
void PuntoMuerto();
int GetVelocidadActual(); #region IPalancaCambios Members
}
public void InsertarVelocidad(int velocidad)
{
public interface IEmbrague Console.WriteLine("Introduciendo marcha " + velocidad);
{ this._velocidadActual = velocidad;
void PresionarEmbrague(); }
void SoltarEmbrague();
} public void PuntoMuerto()
{
public interface IAcelerador Console.WriteLine("Sacando velocidad " + this._velocidadActual);
{ this._velocidadActual = 0;
void PresionarAcelerador(); }
void SoltarAcelerador();
} public int GetVelocidadActual()
{
return _velocidadActual;
}
#endregion
}
public class Embrague : IEmbrague
public class Acelerador : IAcelerador
{ {
#region IEmbrague Members #region IAcelerador Members
public void PresionarEmbrague() public void PresionarAcelerador()
{ {
Console.WriteLine("Embrague presionado"); Console.WriteLine("Acelerador presionado");
} }
public void SoltarEmbrague() public void SoltarAcelerador()
{ {
Console.WriteLine("Embrague suelto"); Console.WriteLine("Acelerador levantado");
}
}
#endregion
#endregion
}
}
El cliente, por lo tanto, tendría que realizar las siguientes acciones si quisiera realizar un cambio de marcha:
Como podemos observar en el gráfico superior, la clase Decorator hereda de la misma clase que el componente que se
quiere decorar. Así, cada decorador es capaz de encapsular una instancia de cualquier otro objeto que herede del
componente común, bien un componente concreto u otro decorador. Este comportamiento recuerda al que vimos
previamente en el patrón Adapter, con la diferencia de que la clase Decorator, a diferencia de la clase Adapter, no
transforma una interfaz, sino que añade cierta funcionalidad.
La encapsulación puede ser iterativa, de modo que un componente concreto puede ser encapsulado por un decorador,
que a su vez puede ser encapsulado por otro decorador… y así sucesivamente, añadiendo nueva funcionalidad en cada
uno de los pasos. Resumiendo: el patrón Decoratorsustituye la herencia por un proceso iterativo de composición.
El objeto con el que el objeto cliente interactuará
será aquel que se encuentre en la capa más
externa (en este caso, DecoratorC), que se
encargará de acceder a los objetos contenidos e
invocar su funcionalidad, que será devuelta a las
capas exteriores.
Para comenzar, por tanto, debemos tener claros los siguientes conceptos
sobre este patrón:
• Un decorador hereda de la misma clase que los objetos que tendrá que
decorar.
• Es posible utilizar más de un decorador para encapsular un mismo objeto.
• El objeto decorador añade su propia funcionalidad, bien antes, bien
después, de delegar el resto del trabajo en el objeto que está decorando.
• Los objetos pueden decorarse en cualquier momento, por lo que es posible
decorar objetos de forma dinámica en tiempo de ejecución.
La razón por la que la clase Decorator hereda de la misma clase que el objeto
que tendrá que decorar no es la de añadir funcionalidad, sino la de
asegurarse de que ambos comparten el mismo tipo y puedan
intercambiarse: un decorador podrá sustituir a un objeto decorado,
basándonos en el principio SOLID del Principio de sustitución de Liskov.
Declarando las clases funcionales
Como viene siendo habitual, ilustraremos nuestro patrón haciendo uso de
vehículos. En este caso, utilizaremos una case abstracta, llamada Vehiculo,
del que heredarán las clases funcionales a las que llamaremos “Berlina” y
“Monovolumen”, y los decoradores, que se limitarán a añadir
funcionalidad a estas clases funcionales. Los decoradores que diseñaremos
serán “Diesel”, “Gasolina”, “Inyeccion”, “CommonRail” y “Turbo”.
Estos decoradores se caracterizarán por:
• Disponer de una referencia a un vehículo que será inyectada en el
constructor.
• Modificar el funcionamiento original de la clase que decoran,
sobrecargando los métodos y llamando a los métodos de las clases
encapsuladas para modificar su información o funcionamiento.
• Comencemos codificando nuestra clase abstracta Vehiculo de la cual heredarán
el resto de clases.
public abstract class Vehiculo
{
// Atributo común a todos los objetos que heredarán de esta clase
protected string descripcion = "Vehículo genérico";
// Método no abstracto que devolverá el contenido de la descripción
// Se declara como virtual para que pueda sustituirse en las clases derivadas
public virtual string Descripcion()
Hecho esto, añadiremos nuestras clases funcionales: Monovolumen y Berlina.
{
return descripcion;
} public class Monovolumen : Vehiculo public class Berlina : Vehiculo
{ {
// Métodos abstractos public Monovolumen() public Berlina()
{ {
public abstract int VelocidadMaxima();
descripcion = "Monovolumen"; descripcion = "Berlina";
public abstract double Consumo(); } }
}
// Funcionalidad básica public override int VelocidadMaxima()
public override int VelocidadMaxima() {
{ return 180;
return 160; }
}
public override double Consumo()
// Funcionalidad básica {
public override double Consumo() return 6.2;
{ }
return 7.5; }
}
}
• Como vemos, ambas clases heredan un atributo común,
“descripcion”, y la funcionalidad VelocidadMaxima y Consumo.
Asumiremos que un monovolumen posee una velocidad máxima de
160km/h y un consumo de 7,5 litros/100km, mientras que una berlina
podrá alcanzar los 180km/h con un consumo de 6,2 litros/100km. Esta
funcionalidad será modificada por nuestras clases decoradoras, que
podrán aumentar o disminuir estas características.
• Con esto habríamos codificado nuestra rama “funcional”. Aún no hay
rastro del patrón Decorator, ya que únicamente hemos hecho uso de
la herencia de la manera habitual (de hecho, de una forma un tanto
escueta).
Creando las clases decoradoras
Es hora, por tanto, de añadir una nueva “rama” a nuestro árbol, añadiendo
los decoradores. Comenzaremos por crear una nueva clase abstracta que
herede de Vehículo (para que pueda ocupar su lugar) y de la cual heredarán
todos los decoradores.
A continuación añadiremos los decoradores, que incluirán una referencia a un vehículo y que se construirán mediante la
inyección de éste. Por tanto, las características de estos decoradores, además de heredar de Decorator, serán las
siguientes:
public abstract class ComponentDecorator:Computer Lo importante de esta clase, es que hereda de la clase original
{ por lo que los objetos se podrán convertir a Computer con un
public override abstract decimal CalculateCost(); simple cast.
} Vale, ¿y cómo añadimos funcionalidad? En este caso hemos
optado por crear una clase por cada componente hardware. A
su vez, cada una de estas clases hereda de
ComponentDecorator. Por ejemplo para un disco duro SSD,
tendremos el siguiente código:
public class FastSSD:ComponentDecorator {
private Computer currentComputer;
public FastSSD(Computer computer)
{
this.currentComputer = computer;
}
public override decimal CalculateCost()
{
return this.currentComputer.CalculateCost() + 255.20M;
} Se puede ver que lo que estamos cogiendo un objeto
} Mientras que para el procesador tope de gama, tendremos el
Computer y lo estamos extendiendo para que el método
siguiente código:
CalculateCost incremente en 120.00 lo que devuelve el
método original. Para poder usar el método original,
public class BigProcessor:ComponentDecorator {
pasamos el objeto Computer como parámetro del
private Computer currentComputer;
constructor.
public BigProcessor(Computer computer) {
Para que todo funcione necesitamos crear una clase base
this.currentComputer = computer; }
desde la que partir, que sería la siguiente:
public override decimal CalculateCost() {
return this.currentComputer.CalculateCost() + 120.00M;
}
}
public class BaseComputer:Computer En este caso el equipo base tiene un coste de 0, pero podría tener cualquier otro
valor. También podemos crear otras configuraciones base, por ejemplo, para
{
portátiles, barebones, netbooks, etc. Las configuraciones base las iremos
public override decimal CalculateCost() { decorando con los distintos componentes, para calcular el precio total.
Y para acabar veamos como usaríamos el patrón, en este caso creando un
return 0M; } Gammer PC
}
Ya veis que es muy sencillo. Nos aprovechamos de que nuestros objetos ComponentDecorator, es decir LotOfRAM, FastSSD o
BigProcessor, devuelven objetos Computer. Estos objetos los vamos pasando en el constructor de cada componente. Cuando hemos
acabado de decorar la clase, hacemos una llamada al método para calcular el coste. Esta llamada irá llamando a los métodos CalculateCost
de todos los objetos creados para al final calcular el precio final.
Aunque podríamos haber conseguido algo similar con una jerarquía de clases, probablemente no tendríamos la flexibilidad y dinamismo
que nos proporciona esta solución. Y todo de una manera muy sencilla.
PATRONES ESTRUCTURALES (IV):
PATRÓN BRIDGE
Objetivo:
Desacoplar una abstracción de su implementación de modo que los dos puedan ser modificados de
forma independiente.
El patrón Bridge o Puente es normalmente uno de los patrones que más cuesta
entender, especialmente si nos ceñimos únicamente a su descripción. La idea
tras este patrón, sin embargo, es sencilla: dado que cualquier cambio que se
realice sobre una abstracción afectará a todas las clases que la implementan,
Bridge propone añadir un nuevo nivel de abstracción entre ambos elementos
que permitan que puedan desarrollarse cada uno por su lado.
Si le echamos un ojo al diagrama, es posible que de base no nos aclare
demasiado. Nos centraremos en el elemento central: una clase abstracta
Abstracción que contiene una referencia a una interfaz Implementor y un
método operacion() que no hace más que invocar el método operacionOriginal()
de dicha interfaz. Lo que hace esta clase Abstracción es, por tanto, encapsular a
la interfaz Implementor exponiendo sus métodos.
Similitudes
Un momento… ¿no hemos visto ya esto antes? Nos suena, ¿verdad? ¿No es precisamente lo que realizaba también
el patrón Adapter, tal y como vimos anteriormente?
No vamos por mal camino. La estructura de este patrón se parece mucho a la del patrón Adapter, ya que nuestra
clase Abstracción hace las veces de “adaptador” entre nuestra clase cliente y la interfaz Implementor. Sin embargo,
nos movemos por la sinuosa senda de la ingeniería, por lo que afirmar que Adapter y Bridge realizan lo mismo
simplemente porque su estructura sea muy parecida es quedarnos en la superficie del problema que tratamos de
resolver.
Como recordaremos, el patrón Adapter basaba su razón de ser en la necesidad de adaptar (valga la redundancia) la
interfaz de una clase en otra, para lo cual encapsulaba dicha clase implementando la interfaz que el cliente
requería. Hasta aquí, todo muy parecido, salvando el hecho de que nuestra clase Abstracción no implementa
ninguna interfaz. ¿Por qué? Porque el objeto de este patrón no es realizar una adaptación. La clase cliente no
espera utilizar la interfaz Implementor, sino que sabe que va a hacer uso de una clase derivada de la clase
Abstracción. La razón de ser de esta estructura no es realizar una adaptación, sino separar una interfaz de su
implementación. La diferencia básica entre ambos patrones es, por tanto, que Adapter se utiliza para unificar
interfaces que ya existen, mientras que Bridge se utiliza cuando se sospecha que la implementación de una interfaz
va a cambiar con el tiempo. Adapter intenta cerrar viejas heridas, mientras que Bridge intenta que los errores que
cometieron los miembros que forman parte de Adapter no se vuelvan a repetir. Hay que aprender de la historia,
amigos.
Otro detalle que quizás no quede claro al ver el diagrama UML es que Abstracción es una clase abstracta, por lo que
el objeto que nuestra clase cliente utilizará será una instancia de RefinamientoAbstraccion o de cualquiera de sus
posibles clases hermanas, que serán las que contendrán los métodos refinados. De este modo, las clases que
implementan Implementor podrán evolucionar por un lado mientras las clases heredadas de Abstracción podrán
hacerlo por otro.
¿Por qué “Bridge”?
Patrón tras patrón, vemos que los mismos conceptos se repiten una y otra vez. Minimizar el acoplamiento, hacer que las
clases dependan de abstracciones en lugar de depender de implementaciones, preferir el uso de composición antes que
el uso de herencia… El patrón Bridge no es una excepción. Sin embargo, hasta el momento todos los patrones que hemos
visto tenían una relación entre su nombre y su funcionalidad. Un Adapter adapta. Una factoría fabrica objetos. Un Builder
construye. Pero… ¿Bridge? ¿Qué tiene que ver un puente con todo esto?
Una de las razones por las que opino que este patrón es complicado de entender a la primera es, precisamente, que no
existe una relación clara entre su nombre y su descripción. ¿Llamar puente a desligar una interfaz de la implementación?
¿Por qué? La razón no está tanto en este proceso sino en el camino que existe entre la clase que refina la abstracción y las
implementaciones de la interfaz. Parte de nuestro código estará implementado dentro de nuestra clase
AbstraccionRefinada, y parte estará fuera, por ejemplo en ImplementorConcretoA. Para acceder a ese código, se realiza un
puente de modo que cuando parte del código de nuestra clase AbstraccionRefinada realice una operación cuyo código no
dependa de sí misma, solicite la realización de esta al elemento que se encuentra al otro lado. En el ejemplo de un driver
de un dispositivo, la clase AbstraccionRefinada podría representar operaciones comunes a una plataforma, como por
ejemplo Windows 7. En un momento dado, nuestro driver necesita realizar un acceso a memoria, pero… ¡cuidado! Esta
operación no dependerá únicamente de la plataforma, sino que será dependiente de la arquitectura del sistema.
Bueno, una solución será crear una clase para Windows7 32 bits y otra para Windows 7 64 bits. Así se solucionaría el
problema, ¿verdad? Desde el punto de vista funcional, sí. Pero recordemos que siempre es preferible la composición
antes de la herencia así que… ¿por qué no implementar las operaciones comunes a la plataforma Windows 7 en nuestra
AbstraccionRefinada y dejarle a Implementor y sus implementaciones las tareas dependientes de la arquitectura concreta
que estemos utilizando? De este modo, cuando sea necesario realizar una operación ligada a la arquitectura, bastará con
solicitárselo al ImplementorConcreto que se encuentra al otro lado del puente y que está ligado a una arquitectura
concreta y (más importante) puede ser incorporado a nuestro objeto en tiempo de ejecución.
Hablando en plata, y a modo de resumen, realizaremos la transformación del siguiente árbol de herencia:
IMotor motorDiesel = new Diesel(); La invocación de Acelerar invocará, por tanto, los métodos
Vehiculo berlina = new Berlina(motorDiesel, 4); InyectarCombustible y ConsumirCombustible que implementa
berlina.MostrarCaracteristicas();
la clase Diesel y expone la interfaz IMotor.
berlina.Acelerar(2.4d);
¿Cuándo utilizar este patrón? Ejemplos reales
Un ejemplo típico de un patrón Bridge lo puede conformar cualquier familia de drivers de
un dispositivo, tal y como vimos en el primer ejemplo.
Otro ejemplo típico suele ser el de las APIs de dibujo. Los elementos genéricos, tales
como formas y figuras serían las abstracciones (por ejemplo, Forma sería el elemento
Abstraction del que derivarían abstracciones refinadas como Circulo o Cuadrado),
mientras que la parte “dependiente” del sistema sería la API concreta que se encargaría
de dibujar en pantalla las formas genéricas definidas en la abstracción. Este
funcionamiento puede observarse en los paquetes de java java.awt y java.awt.peer. (en
Button y List, por ejemplo).
Las situaciones óptimas en los que se debe utilizar este patrón serán, por tanto:
• Cuando se desea evitar un enlace permanente entre la abstracción y (toda o parte de)
su implementación.
• Cuando los cambios en la implementación de una abstracción no debe afectar a las
clases que hace uso de ella.
• Cuando se desea compartir una implementación entre múltiples objetos.
Ejemplo: Patrón Bridge
Para ilustrar todo esto vamos a ver un ejemplo que representará la abstracción del envío de un
paquete. Nuestra interfaz abstracta representará la agencia de transportes que realizará el envío:
public abstract class EmpresaMensajeria{
protected IEnvio envio;
A continuación vamos a definir la interfaz IEnvio que
representará al implementador del envío:
protected EmpresaMensajeria(IEnvio envio){
this.envio = envio;
}
public void recogerPaquete(){
System.out.println('Se ha recogido el paquete.');
envio.procesarEnvio();
}
public void enviarPaquete(){
envio.enviar();
}
public void entregarPaquete(){
envio.procesarEntrega();
System.out.println('Se ha entregado el paquete.');
}
public void setEnvio(IEnvio envio){
this.envio=envio;
}
public void getEnvio(){
return this.envio;
}
}
public interface IEnvio{
public void procesarEnvio();
public void enviar();
public void procesarEntrega();
}
super(envioPorDefecto); // Enviaremos un paquete vía aérea, que es la que esta empresa tiene pro defecto
mensajero.recogerPaquete();
this.nif=nif; mensajero.enviarPaquete();
mensajero.entregarPaquete();
}
public EuroTransport(String nif, IEnvio envio){ // Ahora le decimos a la empresa que queremos enviar por mar
System.out.println("Identificación: "+this.nif);
}
}
PATRONES ESTRUCTURALES (V):
PATRÓN COMPOSITE
Objetivo:
“Componer objetos en árboles para representar jerarquías todo-parte. Composite permite a los
clientes tratar objetos individuales y objetos compuestos de una manera uniforme”.
El patrón Composite se aleja un poco de la línea tradicional de los patrones vistos hasta ahora, ya
que rompe uno de los principios de la programación orientada a objetos: una clase, una
responsabilidad. En realidad, los más puristas pueden decidir no hacerlo, pero el precio a pagar es
demasiado alto para los ingenieros mortales: la simplicidad del modelo.
Cuando diseñamos debemos tener claro que la idea principal es alcanzar un equilibrio entre
muchos factores como por ejemplo presupuesto, usabilidad y facilidad para que nuestro código sea
reutilizable y pueda ser fácilmente mantenible en un futuro, el sine qua non de este patrón es la
facilidad de uso.
A grandes rasgos, el patrón Composite permite crear una jerarquía de elementos anidados unos
dentro de otros. Cada elemento permitirá alojar una colección de elementos del mismo tipo, hasta
llegar a los elementos “reales” que se corresponderán con los nodos “Hoja” del árbol. Un ejemplo
del concepto de la jerarquía que se pretende modelar sería el de los menús de una aplicación:
En este ejemplo tenemos un menú (Archivo) que contiene varios elementos, que
pueden ser “hojas” que ejecutan una operación (Abrir, CreatePDF en línea.., Compartir
archivos usando SendNow Online…, Adjuntar a correo electrónico…) o bien otro menú
(Guardar como) que a su vez contiene más elementos “hoja” (PDF…, Texto…, Word o
Excel Online…).
Creo que el ejemplo es lo suficientemente ilustrativo como para entender el concepto:
poder anidar menús que puedan contener o bien otros menús, o bien directamente
nodos hoja que ejecuten operaciones. Este ejemplo se aproxima bastante al concepto
de Composite, pero no se ajusta exactamente a su filosofía, ya que le falta una
funcionalidad: el submenú debería ser capaz de ejecutar una operación que se
encargaría de iterar sobre todos los subelementos que contiene, ejecutando la
operación de cada uno de ellos. Sin embargo, deja bastante claro el esquema lógico
que seguirá este patrón.
Llevando el coche al taller
Para ilustrar el patrón, acudiremos a un taller, que tuvo a bien contratar a un ingeniero de software para que
le diseñara una aplicación para realizar el inventariado de las piezas de recambio. El dueño del taller le
expuso al ingeniero la dificultad que tenía para establecer los precios de los recambios, ya que dependiendo
de la avería, a veces era necesario cambiar piezas enteras mientras que en otras ocasiones bastaba con
sustituir un pequeño componente. Nuestro cliente había almacenado en una base de datos cada uno de los
componentes con su respectivo precio, pero cuando el proveedor le aumentaba el precio de una pieza que
formaba parte de otra, tenía que preocuparse de actualizar, una a una, todas las piezas de grano más grueso
en las cuales estaba contenida esta pieza. Por poner un ejemplo, si el precio de un tornillo para una rueda
aumentaba, nuestro mecánico tenía que acceder a su catálogo y modificar el precio total en:
• Tornillo para rueda
• Llantas (en todos y cada uno de los modelos que usaran los tornillos anteriores)
• Ruedas (en todos y cada uno de los modelos que usaran las llantas anteriores)
Como podemos imaginar, esta situación era muy laboriosa. Así pues, nuestro ingeniero desarrolló un
sistema que realizara el cálculo del precio de forma automática a la vez que modelaba el almacén de
recambios con una estructura arborescente. En lugar de disponer del siguiente modelo:
Aplicando Composite
Es hora de ensuciarnos un poco las manos y plantar cara a nuestro código.
Comenzaremos creando nuestro elemento abstracto o interfaz correspondiente
al “Componente”. Esta clase o interfaz debe exponer los métodos comunes tanto
a los elementos compuestos como a los elementos “hoja”. Lo verdaderamente
importante es que este elemento sea una abstracción (depender de
abstracciones, no de concreciones).
• Si optamos por la interfaz, simplemente definiremos sus operaciones.
• Si optamos por la clase abstracta, además de definir sus operaciones,
añadiremos un comportamiento por defecto en el que lanzaremos una
excepción que indique que la operación no está soportada. De este modo, las
clases derivadas deberán encargarse de proporcionar la funcionalidad. En caso
de no proporcionarla, lanzarán una excepción (por ejemplo, el método add en
un elemento Hoja no tiene sentido, por lo que deberá tener este
comportamiento por defecto).
Nuestra clase Componente será, por tanto, el siguiente:
public abstract class ComponenteRecambio
{
\\Métodos comunes a objetos compuestos y hojas
public virtual string getNombre() {
throw new NotSupportedException(this.GetType().Name + "getNombre()");
}
public virtual void setNombre(string nombre) {
throw new NotSupportedException(this.GetType().Name + "setNombre()");
}
public virtual string getDescripcion() {
throw new NotSupportedException(this.GetType().Name + "getDescripcion()");
}
public virtual void setDescripcion(string descripcion) {
throw new NotSupportedException(this.GetType().Name + "setDescripcion()");
}
public virtual double getPrecio() {
throw new NotSupportedException(this.GetType().Name + "getPrecio()");
}
public virtual void setPrecio(double precio) {
throw new NotSupportedException(this.GetType().Name + "setPrecio()");
}
\\Métodos exclusivos de los objetos compuestos
public virtual void add(ComponenteRecambio componente) {
throw new NotSupportedException(this.GetType().Name + "add()");
}
public virtual void remove(ComponenteRecambio componente) {
throw new NotSupportedException(this.GetType().Name + "remove()");
}
public virtual ComponenteRecambio getElemento(int indice) {
throw new NotSupportedException(this.GetType().Name + "getElemento()");
}
}
public class ElementoRecambio : ComponenteRecambio
{
// Atributos propios del nodo hoja.
Sobre esta clase trabajaremos creando otras dos private string nombre;
clases que simbolizarán los dos tipos de private string descripcion;
private double precio;
elemento que puede contener nuestro patrón
// Constructor
Composite: elementos hoja (ElementoRecambio) public ElementoRecambio(string nombre, string descripcion, double precio) {
this.nombre = nombre;
y elementos compuestos (Recambio). Así, this.descripcion = descripcion;
this.precio = precio;
nuestros elementos “hoja” serán los que }
incorporen la verdadera funcionalidad, que será // Sobrecargamos únicamente los métodos propios de los nodos hoja, destinados
// a devolver la información y a asignarla
invocada por los elementos compuestos en caso // NOMBRE
public override string getNombre() {
de que fuera necesario. return nombre;
}
Hemos utilizado “nomenclatura Java” (getters y public override void setNombre(string nombre) {
this.nombre = nombre;
setters en lugar de una propiedad para ambas }
// DESCRIPCION
operaciones) porque a la hora de sobrecargar los public override string getDescripcion() {
métodos será más intuitivo si las operaciones se return descripcion;
}
encuentran separadas, como veremos más public override void setDescripcion(string descripcion) {
this.descripcion = descripcion;
adelante. Por supuesto, para quien lo desee, }
// PRECIO
también es posible usar propiedades virtuales y public override double getPrecio() {
sobrecargarlas del mismo modo que hacemos return precio;
}
con estos dos métodos. public override void setPrecio(double precio) {
this.precio = precio;
}
Volviendo a nuestro código, nuestra clase // Los métodos add, remove y getElemento no se sobrecargarán, ya que
ElementoRecambio almacenará información // el nodo hoja no estará compuesto por más elementos que él mismo.
// Por tanto, si se invocan estos métodos, se llamará el método padre
como el nombre, la descripción y el precio: // que lanzará una excepción de tipo NotSupportedException
}
public class Recambio : ComponenteRecambio
Seguramente, a estas alturas ya nos habremos dado cuenta de lo {
// Arraylist que contendrá los elementos hijo
que comentábamos al principio del artículo respecto a la ruptura private ArrayList listaRecambios;
del principio de responsabilidad única. La clase Elemento
implementa un conjunto de métodos, pero deja otro conjunto sin // Atributos
private string nombre;
implementar (los métodos add, remove y getElemento), que private string descripcion;
lanzarán una excepción si te utilizan. Esos métodos serán
// Constructor que recibirá el nombre, el precio y la descripción.
implementados por la siguiente clase: Recambio. public Recambio(string nombre, string descripcion, double precio) {
// Instanciamos el ArrayList
listaRecambios = new ArrayList();
// Asignamos el nombre, la descripción y el precio
this.nombre = nombre;
this.descripcion = descripcion;
this.precio = precio;
}
//Métodos relacionados con el árbol
// Añade un nuevo elemento al ArrayList
public override void add(ComponenteRecambio componente) {
listaRecambios.Add(componente);
}
// Elimina un elemento del ArrayList
public override void remove(ComponenteRecambio componente) {
listaRecambios.Remove(componente);
}
// Recupera un elemento del ArrayList
public override ComponenteRecambio getElemento(int indice) {
return (ComponenteRecambio)listaRecambios[indice];
}
}
En esta primera etapa, hemos codificado la parte que se encarga #region Métodos relacionados con el elemento
de añadir, eliminar y consultar otros elementos. El método es
public override string getNombre()
sencillo: a través de un ArrayList interno, los métodos add, {
remove y getElemento realizarán operaciones sobre él. string nombreCompleto = this.nombre + "\n";
A continuación codificaremos los métodos get que recuperarán foreach(ComponenteRecambio c in listaRecambios)
el contenido de los atributos de cada ElementoRecambio: nombreCompleto += c.getNombre();
nombre, descripción y precio. Para ello iteraremos sobre los
return nombreCompleto;
elementos contenidos dentro de cada Recambio. }
El precio, sin ir más lejos, se calculará a partir de los precios de
los elementos contenidos en cada ElementoRecambio, que se public override string getDescripcion()
{
sumará al precio del propio componente. string descripcionCompleta = this.descripcion + "\n";
foreach (ComponenteRecambio c in listaRecambios)
descripcionCompleta += c.getDescripcion();
return descripcionCompleta;
También sobrecargaremos los métodos setNombre, }
setDescripcion y setPrecio.
public override double getPrecio()
{
public override void setNombre(string nombre) { double precioTotal = this.precio;
this.nombre = nombre;
}
public override void setDescripcion(string descripcion) { foreach (ComponenteRecambio c in listaRecambios)
this.descripcion = descripcion; precioTotal += c.getPrecio();
}
public override void setPrecio(double precio) { return precioTotal;
this.precio = precio; }
}
#endregion
• Recorriendo todos los elementos contenidos dentro de cada recambio
podremos obtener la información almacenada tanto en un objeto
nodo (Recambio) como en un objeto hoja (ElementoRecambio).
Ambos se tratarán de la misma manera, aunque la implementación de
los métodos sea distinta. Y dado que ambos objetos son
intercambiables, estamos consiguiendo lo que declaramos en primera
instancia: componer objetos de forma arborescente respetando la
jerarquía todo-parte permitiendo que ambos elementos se traten de
forma uniforme.
// Declaramos los tornillos incluidos en la llanta, que serán nodos hoja
ComponenteRecambio tornillo1 = new ElementoRecambio("Tornillo llanta", "Tornillo llanta marca ACME", 0.21);
ComponenteRecambio tornillo2 = new ElementoRecambio("Tornillo llanta", "Tornillo llanta marca ACME", 0.21);
ComponenteRecambio tornillo3 = new ElementoRecambio("Tornillo llanta", "Tornillo llanta marca ACME", 0.21);
ComponenteRecambio tornillo4 = new ElementoRecambio("Tornillo llanta", "Tornillo llanta marca ACME", 0.21);
// Declaramos la llanta, que poseerá cuatro tornillos. Por tanto, se tratará de un elemento Composite (compuesto
// por otros elementos, que pueden ser compuestos u hojas)
ComponenteRecambio llanta = new Recambio("Llanta ACME 15'", "Llanta ACME de 15'", 42.22);
// Añadimos los tornillos a la llanta
llanta.add(tornillo1);
llanta.add(tornillo2);
llanta.add(tornillo3);
llanta.add(tornillo4);
// Declaramos ahora otro elemento: la válvula de la rueda
ComponenteRecambio valvula = new ElementoRecambio("Válvula", "Válvula de neumático genérica", 0.49);
// Realizamos lo mismo con el neumático
ComponenteRecambio neumatico = new ElementoRecambio("Neumático 15'", "Neumático Michelin de 15'", 13.42);
// Declaramos un nuevo objeto compuesto: la rueda.
// Este objeto estará compuesto por la llanta, la válvula y el neumático.
// A su vez, la llanta incluirá los tornillos.
// Establecemos el precio de la rueda a '0', ya que dependerá en exclusiva del contenido de sus elementos.
ComponenteRecambio rueda = new Recambio("Rueda 15'", "Rueda de 15' con llanta ACME y neumático Michelin", 0);
// Añadimos a la rueda los elementos hoja que instanciamos previamente
rueda.add(llanta);
rueda.add(neumatico);
rueda.add(valvula);
Purismo vs. Transparencia
Nos acabamos de encontrar con el primer patrón que viola deliberadamente uno de los principios de la
programación orientada a objetos. El motivo ha sido que el incremento en la transparencia (y usabilidad) es tan
importante que merece la pena el sacrificio de permitir que una clase adquiera más de una responsabilidad.
Recordemos que los patrones de diseño son soluciones genéricas a problemas concretos, por lo que su objetivo
es la de facilitar el desarrollo de software. Hay ocasiones, por tanto, en las que las que las ventajas de romper las
normas sobrepasan de largo seguirlas a rajatabla.
¿Cuándo utilizar este patrón? Ejemplos reales
Los escenarios en los que este patrón suele utilizarse son, principalmente:
• Como su propia definición indica, cuando se requiere representar jerarquías todo-parte que superen cierto
tamaño.
• Cuando se desea que los clientes puedan ignorar la diferencia entre colecciones de objetos y objetos
individuales, haciendo que ambos se traten de la misma manera.
• El ejemplo más sencillo de visualizar de este patrón son los controles de un formulario, por ejemplo de
WinForms. Los objetos (botones, literales, paneles…) heredan de la clase Control, que, de base, puede contener
a su vez una colección de otros objetos que hereden de la clase Control. Cada uno de estos controles pueden
tratarse bien como un control individual en sí o como una colección de controles (un panel puede contener, por
poner un ejemplo, un Label, un Button y un RadioButton).
• El sistema del pipeline de petición/respuesta de ASP.NET también sigue este esquema.
• Finalmente, la estructura de ficheros y directorios de un sistema de archivos también actúa en consonancia con
este patrón.
Patrón composite, un ejemplo:
• Vamos a ver un ejemplo para dejar todo más claro. Tenemos que
diseñar un sistema que permita dibujar planos de una zona concreta.
Las zonas pueden ser una ciudad, un barrio, una calle, una avenida, una
plaza o una travesía. En primer lugar definiremos una interfaz que
represente la zona a dibujar, que hará de Component:
• interface Zona {
public void generarPlano();
public String obtenerTipo();
public void agregarZona(Zona z);
public Zona subzona(int i);
}
El siguiente paso es definir las zonas que pueden estar compuestas por
otras zonas. Por ejemplo, una ciudad tiene varios barrios que a su vez
están compuestos por domicilios. Lo haremos mediante la
implementación de la interfaz zona:
interface Zona { El siguiente paso es definir las zonas que pueden estar
public void generarPlano(); compuestas por otras zonas. Por ejemplo, una ciudad tiene
public String obtenerTipo(); varios barrios que a su vez están compuestos por domicilios. Lo
public void agregarZona(Zona z); haremos mediante la implementación de la interfaz zona:
public Zona subzona(int i);
}
public class Ciudad implements Zona{ public class Barrio implements Zona {
private String nombre; private String nombre;
private ArrayList subzonas; private ArrayList subzonas;
public Ciudad(String nombre){ public Barrio(String nombre){
this.nombre = nombre; this.nombre = nombre;
subzonas = new ArrayList(); subzonas = new ArrayList();
} }
public void generarPlano(){ public void generarPlano() {
System.out.println("CIUDAD: "+this.nombre); System.out.println("BARRIO: "+this.nombre);
for(int i=0;i<subzonas.size();i++){ for(int i=0;i<subzonas.size();i++){
subzonas.get(i).generarPlano(); subzonas.get(i).generarPlano();
} }
} }
public void obtenerTipo(){ public void obtenerTipo() {
return "ciudad"; return "barrio";
} }
public void agregarZona(Zona z){
// Las ciudades se subdividen en barrios public void agregarZona(Zona z) {
if(z.getTipo().equals("barrio")){ // Un barrio no puede tener ni barrios ni ciudades dentro de el.
subzonas.add(z); } else { if(!(z.getTipo().equals("ciudad") || (z.getTipo().equals("barrio")){
System.out.println("No se puede añadir "+z.getTipo()+" directamente a una ciudad."); subzonas.add(z);
} } else {
public Zona subzona(int i){ System.out.println("No se puede añadir "+z.getTipo()+" directamente a un barrio.");
return subzonas.get(i); }
} }
} public Zona subzona(int i){
return subzonas.get(i);
}
}
Nos queda por implementar el resto de zonas, las que no son composiciones (zonas finales u hojas) o son composiciones sin hijos:
public class Calle implements Zona{ public class Avenida implements Zona{
private String nombre; private String nombre;
hortaleza.agregarZona(new Travesia("Biosca"));
hortaleza.agregarZona(new Avenida("América"));
madrid.agregarZona(vallecas);
madrid.agregarZona(chamberi);
madrid.agregarZona(hortaleza);
zonas.add(madrid);
zonas.add(new Ciudad("Barcelona"));
zonas.add(new Barrio("Georgetown"));
zonas.add(new Calle("Aleatoria"));
// Como veis, tenemos zonas de varios tipos y varios niveles Generaremos todos los planos independientemente del nivel
for(int i=0; i<zonas.size();i++){
zonas.get(i).generarPlano();
}
}
PATRONES ESTRUCTURALES (VII):
PATRÓN PROXY
Objetivo:
“Proporcionar un sustituto o intermediario para otro objeto de modo
que pueda controlarse el acceso que se tiene hacia él”.
Supongo que todos conocemos el concepto de proxy, al menos en su
acepción aplicada a la navegación web. Se trata de una máquina que actúa
de intermediaria a la hora de servir páginas web (u otros servicios). En la
configuración de área local podemos indicar la IP de esta máquina y será
esta máquina la que se conecte a la URL por nosotros y la envíe a nuestro
equipo. De este modo, un equipo no se conectará directamente a la
URL, sino que lo hará a través de este intermediario. ¿Por qué
hacer esto? Por múltiples motivos: podemos, por ejemplo,
restringir las URLs que nuestros clientes (ordenadores de la red
local) pueden visitar. O cachear las páginas que se visitan con
más frecuencia, haciendo innecesario el acceso a la web “real”
en los casos en los que el acceso se repita, proporcionando un
ahorro en ancho de banda. En resumen, un proxy será una
entidad en la que delegaremos la ejecución de ciertas tareas y
que decidirá, en última instancia, qué acciones realizar antes y
después de éstas. Los proxies, por tanto, podrían dedicarse
perfectamente a la política
Tipos de proxy
Dependiendo de las responsabilidades y del comportamiento del proxy,
tendremos varios tipos que realizarán unos tipos de tarea u otras. Los proxies
más comunes son los siguientes:
• Proxy remoto: un proxy remoto se comporta como un representante local de
un objeto remoto. Se encarga principalmente de abstraer la comunicación
entre nuestro cliente y el objeto remoto. Es el embajador de los proxies.
• Proxy virtual: se encarga de instanciar objetos cuyo coste computacional es
elevado. Es capaz de sustituir al objeto real durante el tiempo que el
verdadero objeto está siendo construido y proporcionar funcionalidades
como el lazy loading (realizar operaciones computacionalmente costosas
únicamente cuando el acceso a el elemento es requerido).
• Proxy de protección: establece controles de acceso a un objeto dependiendo
de permisos o reglas de autorización.
Estructura del patrón
Si echamos un vistazo al diagrama, vemos que la estructura de este patrón es bastante sencilla:
• Tenemos una clase abstracta (o interfaz) Elemento que define las operaciones que deberá
cumplimentar tanto nuestro objeto real (ElementoReal) como el proxy que actuará de
intermediario (ElementoProxy). Ambos elementos, al heredar de Elemento, deberán ser, por
tanto, intercambiables. De este modo, sustituir un objeto de la clase ElementoReal por un
ElementoProxy debería de ser -idealmente- transparente.
• La clase ElementoReal es aquella que contiene la verdadera funcionalidad, es decir, la clase que
se quiere “proteger” a través del proxy. En el ejemplo de los navegadores, se correspondería al
ordenador que realiza la petición HTTP. Este elemento tendrá un conjunto de operaciones,
heredadadas desde Elemento. Usando el símil de los ordenadores, una operación podría ser
HttpGet o HttpPost.
• La clase ElementoProxy también hereda de Elemento, y como tal, posee todos sus métodos. La
diferencia fundamental es que también incorpora una referencia a otro Elemento. Así, tal y como
ocurría en otros patrones como Adapter o Decorator, el método codificado dentro de esta clase
realizará ciertas operaciones de control y/o transformación para posteriormente invocar el
método original del ElementoReal que tiene referenciado. Por ejemplo, si nuestro proxy no
quisiera permitir las conexiones a una página en concreto, su comportamiento sería algo similar
al siguiente:
private class ElementoProxy : Elemento
{
// Incluímos una referencia a otro elemento.
private Elemento elemento;
// El constructor inyectará el ArrayList en el objeto // Operación 1: Reinicio del índice, colocándolo en el elemento
public override IteratorVehiculo(ArrayList listado) anterior al primero
{ public void Primero()
this.vehiculos = listado; {
} this.posicionActual = -1;
} }
Espera, espera, ¿Por qué colocarlo antes del primer elemento en lugar de referenciar directamente el primer elemento? ¿No sería más
intuitivo? Sí, pero sería erróneo. Si el método Primero() colocara el cursor en el primer elemento, ¿qué ocurriría si la lista estuviera vacía?
Exacto: estaríamos referenciando un elemento que no existe, y lo que es peor: no tendríamos modo alguno de que nuestro iterador nos
informara si existen elementos, ya que el método QuedanElementos(), encargado de esta operación, no podría actuar sobre el primer
elemento de la colección. Colocando el índice en el elemento inmediatamente anterior solucionaríamos directamente estos dos problemas.
Lo siguiente que haremos será codificar el método Actual(), que nos devolverá una referencia al elemento cuyo índice se corresponda con el
almacenado en posicionActual(), realizando, eso sí, las comprobaciones pertinentes.
// Obtenemos el iterator
IIteratorVehiculo iterador = registro.ObtenerIterator();
// Mostramos su contenido
Console.WriteLine(v.Marca + " " + v.Modelo + " fabricado el " + v.FechaFabricacion.ToShortDateString() + " (" + v.Precio + " euros)");
}
Aplicación del patron Iterator
Este patrón, como podemos ver, no tiene demasiada complejidad: es más simple que la maquinaria de un
botijo. Sin embargo, la pregunta que seguramente ronde la cabeza del lector sea similar a “pero si
tenemos un ArrayList, ¿para qué recorrerlo secuencialmente? ¿Para qué usar esas comprobaciones
cuando pueden codificarse en nuestro código de forma sencilla? ¿No es rizar el rizo? La verdad es que sí.
Este patrón no está pensado para aplicarlo a una estructura de datos que ya posea mecanismos de acceso,
como por ejemplo el ArrayList que hemos utilizado aquí: está pensado para ser utilizado en estructuras en
los que el cliente no deba o no necesite conocer su estructura interna. Este ejemplo es estúpido porque
hemos observado “las tripas” de la clase RegistroVehiculos y sabemos que dentro de ella vive un ArrayList.
Pero si no lo supiéramos, el Iterator se encargaría de proporcionarnos acceso a sus miembros.
Vale, de acuerdo, en la ignorancia está la felicidad. Pero aún así, el programador original de la aplicación
sabe que dentro de RegistroVehiculos hay un ArrayList, independientemente de que el cliente necesite
saberlo o no. ¿Por qué complicarse la vida de esta manera? Porque ¿qué ocurriría si dentro de nuestra
estructura de datos no hubiese un ArrayList. O mejor aún: ¿qué ocurriría si nuestro cliente abriese una
fábrica en otra ciudad y nuestra aplicación necesitara conectarse de forma remota a la información allí
contenida? ¿Cambiaríamos todas nuestras clases e interfaces para proporcionar un conjunto de
operaciones que nos permitiera conectarnos de forma remota al nuevo centro? Con este patrón no sería
necesario. Bastaría con implementar otra vez la interfaz IIteratorVehiculo de forma que sus operaciones
realizaran las peticiones necesarias al servicio remoto, haciéndolo transparente para el usuario. Por
ejemplo, podríamos idear una implementación como la siguiente:
public class IteratorVehiculoRemoto : IIteratorVehiculo { request.Method = "GET";
private String urlServicio; // Obtenemos la respuesta
// Almacenaremos el índice en el que se encuentra el iterador WebResponse response = request.GetResponse();
private int posicionActual = -1; Stream data = response.GetResponseStream();
// El constructor inyectará la dirección del servicio en el objeto response.Close();
public IteratorVehiculoRemoto(String urlServicio) { // Deserializamos el objeto
this.urlServicio = urlServicio; } byte[] buffer = new byte[data.Length];
public void Primero() { data.Read(buffer, 0, buffer.Length);
this.posicionActual = -1; } MemoryStream ms = new MemoryStream(buffer);
public Vehiculo Actual() { IFormatter formatter = new BinaryFormatter();
WebRequest request = WebRequest.Create(urlServicio + "?Index=" + ms.Seek(0, SeekOrigin.Begin);
posicionActual); Vehiculo v = (Vehiculo)formatter.Deserialize(ms);
((HttpWebRequest)request).UserAgent = "Cliente IteratorVehiculoRemoto"; return v;
request.Method = "GET"; }
WebResponse response = request.GetResponse(); public bool QuedanElementos() {
Stream data = response.GetResponseStream(); WebRequest request = WebRequest.Create(urlServicio + "?
response.Close(); GetMaxElements");
// Deserializamos el objeto ((HttpWebRequest)request).UserAgent = "Cliente IteratorVehiculoRemoto";
byte[] buffer = new byte[data.Length]; request.Method = "GET";
data.Read(buffer, 0, buffer.Length); // Obtenemos la respuesta
MemoryStream ms = new MemoryStream(buffer); WebResponse response = request.GetResponse();
IFormatter formatter = new BinaryFormatter(); Stream data = response.GetResponseStream();
ms.Seek(0, SeekOrigin.Begin); response.Close();
Vehiculo v = (Vehiculo)formatter.Deserialize(ms); // Obtenemos el resultado de la petición
return v; StreamReader reader = new StreamReader(data);
} string strResultado = reader.ReadLine();
public Vehiculo Siguiente() { if(!string.IsNullOrEmpty(strResultado))
// Aquí realizaríamos las comprobaciones necesarias para determinar si la return Boolean.Parse(strResultado);
petición es válida return false;
// Realizamos la petición HTTP }
WebRequest request = WebRequest.Create(urlServicio + "?Index=" + (+ }
+posicionActual));
((HttpWebRequest)request).UserAgent = "Cliente IteratorVehiculoRemoto";
Como podemos ver, ambos iteradores serán IIteratorVehiculo, y como tales, tendrán los mismos métodos. Por lo tanto,
la clase cliente no necesitará saber si está obteniendo los datos desde un ArrayList de nuestra máquina o desde un
servicio web alojado en otra Comunidad Autónoma. La transparencia es la clave de este patrón, y por lo tanto, será el
objetivo que debemos buscar. Incluso podríamos crear un tercer Iterator que de algún modo fusionara ambos
iteradores, haciendo que cuando se llegara al final del iterador del ArrayList se comenzaran a proporcionar elementos
del servicio web, tratando ambas colecciones de cara al cliente como si fuera una sola.
Nota: el código que he mostrado para simbolizar la implementación de un iterador ejecutado sobre un servicio no es
real. Se trata de una simulación en la que se pretende mostrar, de forma conceptual, cómo podría implementarse el
patrón para acceder a elementos remotos. Ningún servicio web ha sido maltratado durante la redacción de este
artículo.
Si nos centramos en el diagrama de clases del diseño del patrón, podremos identificar los
siguientes actores:
• ICommand: interfaz que expone el método genérico (execute()).
• CommandGenerico: implementa ICommand y posee una referencia al objeto cuyo
método execute() tendrá que encapsular. Este objeto recibe el nombre de Receiver.
• Receiver: como acabamos de decir, es el objeto que implementa la funcionalidad real.
Alguno de sus métodos será encapsulado por ICommand.execute().
• Invoker: clase encargada de invocar el método ICommand.execute(). Posee una
referencia (o varias) a ICommand, y su método SetCommand le permite cambiar su
funcionalidad en tiempo de ejecución. Ofrecerá también un método que invoque el
método ICommand.Execute() que, a su vez, invocará Receiver.MetodoAEncapsular().
Implementando el patrón Command
Volvamos a nuestra fábrica de vehículos. Se nos ha encargado programar parte de la centralita de un nuevo modelo,
concretamente de la activación y desactivación de las luces. Sabemos que contaremos con tres tipos de luces: posición,
cortas y largas, pero la implementación de los métodos encargados de este proceso aún no están definidas, ya que se han
subcontratado a una empresa coreana. Sin embargo, nuestro cliente nos exige que, pese a ello, debemos avanzar con el
diseño de este módulo.
El patrón Command es perfecto en este escenario: contamos con un entorno en el que debemos ejecutar un método que
aún no conocemos o que puede cambiar con el tiempo, por lo que crearemos una interfaz ICommand que exponga un
método Execute(). Posteriormente, una vez que los requisitos estén claros y que sepamos qué hay que ejecutar,
implementaremos clases que se acoplen entre nuestra centralita y el componente que la empresa coreana nos
proporcionará.
Por lo tanto, comenzaremos creando una interfaz ICommand que se encargará de ofrecer una firma para el método que
encapsulará el resto de los métodos: Execute():
public interface ICommand
{
// El método Execute() será aquel que el objeto que reciba la referencia
// será capaz de ejecutar.
void Execute();
}
A continuación modelaremos las clases que serán encapsuladas por el objeto Command. O más
específicamente, las clases cuyos métodos serán encapsulados por el método Execute() del objecto
Command(). Crearemos una clase abstracta de la que heredarán el resto de las clases que serán encapsuladas,
aunque en realidad esto no es estrictamente necesario (un objeto Command podría albergar diversos objetos
no necesariamente relacionados entre sí). Esta clase es conocida como Receiver, y se caracteriza porque se
encargará de albergar la lógica concreta del método. Si nos ceñimos a la descripción de una interfaz, la clase
Receiver sería aquella clase que se encargaría de implementarla.
// Clase abstracta de la que heredarán las clases que serán encapsuladas por los
// objectos Command. Por lo tanto, sus métodos serán aquellos que encapsulará el
// método Execute().
public abstract class LucesReceiver
{ A continuación implementaremos las clases concretas cuyos métodos
protected bool encendidas; serán encapsulados por ICommand.Execute(). Dado que heredan de la
protected int distanciaAlumbrado;
clase abstracta LucesReceiver, únicamente tendremos que implementar
// Propiedad de sólo lectura que devolverá el estado de las luces de forma explícita el método Encender(), ya que el resto de la
public bool Encendidas funcionalidad será común. Comenzaremos por las luces de posición:
{
get {return encendidas;}
}
// Método encargado de apagar las luces. Establece el estado del atributo 'encendidas'
// a 'false'. Será común a todas las clases que hereden de ella.
public void Apagar()
{
this.encendidas = false;
}
// El método Encender será distinto en cada una de las clases que hereden de esta clase.
public abstract int Encender();
}
public class LucesPosicion : public class LucesCortas : LucesReceiver
LucesReceiver {
{ private const int DISTANCIA = 40;
private const int DISTANCIA = 1; public override int Encender() {
public override int Encender() { this.encendidas = true;
return DISTANCIA;
this.encendidas = true;
}
return DISTANCIA; }
}
}
El procedimiento es sencillo:
• Se declara una clase abstracta, que será la plantilla o modelo. Esta clase definirá una serie de funciones y métodos.
Aquellas que sean comunes estarán implementadas. Aquellas que dependan de cada caso concreto, se declararán
como abstractas, obligando a las clases hijas a implementarlas.
• Cada clase derivada implementará los métodos específicos, acudiendo a la clase base para ejecutar el código común.
• La clase base también se encargará de la lógica del algoritmo, ejecutando los pasos en un orden preestablecido (las
clases hijas no deberían poder modificar el algoritmo, únicamente definir la funcionalidad específica que tienen que
implementar).
Dado que la clase padre es la que se encarga de llamar los métodos de las clases derivadas (los pasos del algoritmo
estarán implementado en la clase base), se trata de una aplicación manifiesta del principio de inversión de dependencias:
la clase base no tiene por qué saber nada acerca de sus hijas, pero aún así, se encargará de invocar su funcionalidad
cuando sea necesario. El principio de Hollywood (“no nos llames, nosotros te llamaremos”) vuelve a entrar en escena.
Implementando el patrón
En anteriores artículos hemos hecho uso de este patrón sin ser conscientes de ello al definir el funcionamiento de un motor de cuatro
tiempos. Veíamos que el funcionamiento de un motor de gasolina se basaba en el siguiente algoritmo:
• Admisión: el descenso del pistón crea un vacío que aspira la mezcla de aire y combustible de la válvula de admisión. La válvula de
escape permanece cerrada.
• Compresión: una vez que el pistón ha bajado hasta el final, se cierra la válvula de admisión. El pistón asciende, comprimiendo la
mezcla y aumentando la presión.
• Explosión: el pistón alcanza la parte superior y la bujía produce una chispa que hace explotar la mezcla de aire y combustible,
haciendo que el pistón vuelva a descender.
• Escape: la válvula de escape se abre. El pistón asciende nuevamente, empujando los gases resultantes de la explosión y comenzando
un nuevo ciclo.
Ahora veamos cómo funciona, a grandes rasgos, un motor diesel de cuatro tiempos:
• Admisión: el descenso del pistón crea un vacío que aspira aire desde la válvula de admisión. La válvula de escape permanece
cerrada.
• Compresión: una vez que el pistón ha bajado hasta el final, se cierra la válvula de admisión. El pistón asciende, comprimiendo el
aire y aumentando la presión.
• Combustión: los inyectores pulverizan el combustible, haciendo que la presión se encargue de aumentar la temperatura, haciendo
que se produzca la combustión y la expansión de los gases que fuerzan el descenso del pistón.
• Escape: la válvula de escape se abre. El pistón asciende nuevamente, empujando los gases resultantes de la explosión y comenzando
un nuevo ciclo.
Tal y como observamos, ambos motores tienen un funcionamiento muy similar. Las fases 2 y 4 (compresión y escape) son idénticas, la
fase 1 (admisión) varía ligeramente, mientras que la fase 3 (explosión en el motor de gasolina, combustión en el motor diesel) tiene un
comportamiento diferente. ¿Cómo encaja aquí el patrón Template Method?
Dado que el algoritmo para realizar el ciclo del motor tiene los mismos pasos efectuados en el mismo orden, es el contexto
adecuado para utilizar este patrón. Podemos crear una superclase Motor que implemente las fases comunes a ambos
motores (Compresión, Escape) más el algoritmo RealizarFaseMotor, que será el encargado de invocar los métodos en un
orden fijo. Esta ejecución será invariable, por lo que las clases derivadas únicamente podrán (y deberán) implementar las
partes específicas de cada motor.
Dado que la fase Admisión varía ligeramente (inyección de mezcla de combustible y gas en el motor de gasolina frente a
inyección de gas en el motor diesel), es posible implementar la parte común en el propio método de la superclase y
encapsular únicamente la parte que varía en otro método que será invocado desde Compresión. Así respetaremos otro de
los principios de la orientación a objetos: encapsular aquello que es susceptible de cambiar.
Comencemos creando la clase abstracta Motor. Si bien en otros casos hemos hablado de utilizar una interfaz o una clase
abstracta para implementar una abstracción, en este caso será necesario utilizar una clase abstracta, no una interfaz.
¿Por qué? Porque si utilizamos una interfaz únicamente podremos definir la firma de los métodos a utilizar, pero no
podremos codificar una funcionalidad común, que es lo que pretende este patrón. Por lo tanto, Motor será una clase
abstracta que implementará de forma explícita la parte común y declarará como abstractos los métodos que las clases
derivadas estarán obligadas a implementar.
Comenzaremos declarando unas cuantas variables: dos para modelar el estado de las válvulas y otras dos para modelar el
ángulo actual del cigüeñal y del árbol de levas. También crearemos un método que “reinicie” el ángulo de estos dos
elementos, manteniéndolos siempre entre 0 y 359 grados:
public abstract class Motor A continuación modelaremos los dos métodos que
{
// Estado de las válvulas implementará de forma explícita la clase base: compresión y
private bool valvulaAdmisionAbierta = false; escape:
private bool valvulaEscapeAbierta = false;
// Segunda Fase: Compresión
// Ángulos del cigueñal y del árbol de levas protected void Compresion() {
protected int anguloCiguenal = 0; Console.WriteLine("COMENZANDO FASE DE COMPRESION");
protected int anguloArbolLevas = 0; // Se cierra la válvula de admisión
valvulaAdmisionAbierta = false;
// Giros del cigueñal y del árbol de levas
anguloCiguenal = SumarAngulo(anguloCiguenal, 360);
// Método que mantendrá el ángulo entre 0 y 359 grados
anguloArbolLevas = SumarAngulo(anguloArbolLevas, 180);
protected int SumarAngulo(int anguloActual, int cantidad)
Console.WriteLine("Angulo del ciguenal: " + anguloCiguenal);
{
Console.WriteLine("Angulo del arbol de levas: " + anguloArbolLevas);
if (anguloActual + cantidad >= 360)
Console.WriteLine("Valvula de admision abierta: " + valvulaAdmisionAbierta);
return anguloActual + cantidad - 360; Console.WriteLine("Valvula de escape abierta: " + valvulaEscapeAbierta + "\n");
else }
return anguloActual + cantidad; // Cuarta Fase: Escape
} protected void Escape() {
Console.WriteLine("COMENZANDO FASE DE ESCAPE");
} // Se abre la válvula de escape
valvulaEscapeAbierta = true;
// Giros del cigueñal y del árbol de levas
anguloCiguenal = SumarAngulo(anguloCiguenal, 180);
anguloArbolLevas = SumarAngulo(anguloArbolLevas, 90);
Console.WriteLine("Angulo del ciguenal: " + anguloCiguenal);
Console.WriteLine("Angulo del arbol de levas: " + anguloArbolLevas);
Console.WriteLine("Gases expulsados. Fin de ciclo");
}
Hemos dicho que la tercera fase depende exclusivamente de
las clases derivadas. Por lo tanto, nuestra clase motor se // La bajada del pistón depende del motor concreto, por lo que deberá ser implementada
// por la clase hija.
limitará a declarar el método como abstracto y obligar a las protected abstract void BajarPiston();
clases derivadas a implementar la funcionalidad específica
// Primera Fase: Admisión
de cadNos queda la primera fase: admisión. Hemos dicho protected void Admision()
que parte de la funcionalidad es común (bajada del pistón, {
Console.WriteLine("COMENZANDO FASE DE ADMISION");
apertura de la válvula de admisión), pero otra parte
depende de si el motor es de gasolina (inyección de // Se abre la válvula de admisión y se cierra la válvula de escape
valvulaAdmisionAbierta = true;
combustible y aire) o diesel (inyección de aire). Por tanto, valvulaEscapeAbierta = false;
creamos otro método abstracto que las clases hija tendrán
// Se baja el pistón. Esta operación será distinta en el motor diesel (que
que codificar (BajarPiston()) y se invoca desde el método // inyectará aire) o gasolina (que inyectará una mezcla de aire y combustible)
Admision(), que codificará la parte común:a motor: BajarPiston();
// Tercera Fase: Consumo del combustible. Dado que depende del motor concreto, anguloCiguenal = SumarAngulo(anguloCiguenal, 180);
// este método será abstracto y deberá ser implementado por la clase derivada. anguloArbolLevas = SumarAngulo(anguloArbolLevas, 90);
protected abstract void ConsumirCombustible();
Console.WriteLine("Angulo del ciguenal: " + anguloCiguenal);
Console.WriteLine("Angulo del arbol de levas: " + anguloArbolLevas);
Console.WriteLine("Valvula de admision abierta: " + valvulaAdmisionAbierta);
Console.WriteLine("Valvula de escape abierta: " + valvulaEscapeAbierta + "\n");
}
Finalmente, codificamos el propio algoritmo, de carácter público, que
se encargará de invocar todos los pasos en un orden determinado:
// Método público que ejecutará el algoritmo completo
public void RealizarFaseMotor()
{
Admision(); // Parcialmente implementado en la clase base
Compresion(); // Implementado en la clase base
ConsumirCombustible(); // Delegado en las clases hijas
Escape(); // Implementado en la clase base
}
Lo siguiente será crear el contexto. Esta clase será la encargada de establecer la conexión entre el cliente y las clases que
implementan la estrategia, sustituyendo la clase que la implementa dependiendo del comportamiento esperado. Por lo tanto, se
compondrá de una referencia a la interfaz que implementarán las estrategias más un método que permita cambiar de instancia (es
decir, una property o un setter de toda la vida). A partir de esta funcionalidad básica, el contexto podrá realizar otras operaciones
relacionadas con la estrategia que pretende modelar, como por ejemplo la invocación de sus métodos o la encapsulación del cambio
de estrategia.
public class Contexto
{
En realidad, la propia clase cliente puede actuar como clase de
// Referencia a la interfaz contexto, pero siempre será mejor minimizar el acoplamiento
private ITipoConduccion tipoConduccion;
entre las estrategias y las reglas de negocio. De este modo,
// Propiedad que establecerá un nuevo tipo de conducción (cambio de estrategia) respetaremos otro de los principios de la orientación a objetos:
public ITipoConduccion TipoConduccion una clase, una responsabilidad. Para comprobar el
{
get { return tipoConduccion; }
funcionamiento de nuestro cliente, bastará con utilizar el
set { this.tipoConduccion = value; } siguiente código que hará uso del contexto para cambiar de
}
estrategia en tiempo de ejecución:
// Métodos de servicio (invocan los métodos implementados por las estrategias)
public string ObtenerDescripcion()
{
return tipoConduccion.ObtenerDescripcion();
}
public int IncrementarVelocidad(float combustible)
{
return tipoConduccion.ObtenerIncrementoVelocidad(combustible);
}
public int IncrementarPotencia(float combustible)
{
return tipoConduccion.ObtenerPotencia(combustible);
}
}
public class Vehiculo Finalmente, el código que invoca a nuestro cliente, que será el siguiente:
{
private Contexto contexto;
public Vehiculo()
static void Main(string[] args)
{
{
contexto = new Contexto();
Vehiculo v = new Vehiculo();
}
v.ConduccionDeportiva();
v.Acelerar(2.4f);
public void ConduccionDeportiva()
{ Console.WriteLine("");
ITipoConduccion conduccionDeportiva = new ConduccionDeportiva();
contexto.TipoConduccion = conduccionDeportiva; v.ConduccionNormal();
} v.Acelerar(2.4f);
public void ConduccionNormal() Console.ReadLine();
{ }
ITipoConduccion conduccionNormal = new ConduccionNormal();
contexto.TipoConduccion = conduccionNormal;
}
// Métodos que invocan la funcionalidad implementada por la interfaz
public void Acelerar(float combustible)
{
string descripcion = contexto.ObtenerDescripcion();
int incrementoVelocidad = contexto.IncrementarVelocidad(combustible);
int potencia = contexto.IncrementarPotencia(combustible);
Console.WriteLine("Tipo de conducción " + descripcion);
Console.WriteLine("Combustible inyectado: " + combustible);
Console.WriteLine("Potencia proporcionada: " + potencia);
Console.WriteLine("Incremento de velocidad: " + incrementoVelocidad);
}
}
¿Cuándo utilizar este patrón? Ejemplos reales
Este patrón es aconsejable, como ya hemos comentado, en situaciones en los que una misma operación (o conjunto de
operaciones) puedan realizarse de formas distintas. A grosso modo, el patrón Strategy realiza una tarea bastante similar
al patrón Template Method, salvo porque en este caso el algoritmo no tiene por qué contar con pasos en común y
porque Strategy confía en la composición mientras que Template Method se basa en la herencia.
Ejemplos reales de este patrón se aplican, por ejemplo, la serialización de objetos. Una interfaz que exponga un
método serialize() podrá codificar un objeto en distintos formatos (String64, XML, JSON). El cliente no necesita saber
cómo se realizará esta operación: bastará con que el contexto seleccione la estrategia adecuada y el resultado de la
operación dependerá de la opción concreta que se haya seleccionado.
Del mismo modo podemos pensar en una conexión a un servicio web: podremos realizarla mediante TCP/IP, HTTP,
HTTPS, Named Pipes… todo esto deberá ser transparente para el cliente: El contexto será el encargado de adoptar una
forma concreta de conexión.
¿Más ejemplos? Los compresores funcionan a través de estrategias (se utiliza un algoritmo distinto para comprimir en
zip o en rar), y en general, cualquier programa capaz de almacenar y transmitir datos en distintos formatos
implementarán este patrón. Por lo tanto, como podemos imaginar, nos encontramos, tanto por su utilidad como por su
sencillez, con uno de los patrones más utilizados de todos los expuestos por el GoF.
ID y Estrategia
LOS ROBOTS
Imaginemos que tenemos una clase llamada Robot, cada Robot puede
realizar una serie de operaciones fundamentales:
• Caminar
• Disparar
Desde luego hay muchas más pero esas serán suficientes para el
ejemplo.
Iniciemos con el análisis, en primera instancia revisemos la característica Disparar.
Si tenemos un conjunto de Robots donde todos ellos pueden disparar de manera
diferente podemos pensar en crear una clase base llamada Robot la cual desde luego
implementa la funcionalidad Disparar como virtual o abstract de tal forma que las
implementaciones concretas puedan establecer una forma particular de disparar en
cada caso.
Existen inicialmente 3 implementaciones de Robot
• RobotVigilante: Dispara como todos los Robot
• RobotMilitar: Dispara como todos los Robot
• RobotCasero: No dispara
Este es nuestro diagrama de clases:
public class Robot
{
public Robot()
{
}