Patrones de diseño
Patrones de diseño
Introducción
Los patrones de diseño son soluciones recurrentes a problemas de diseño que aparecen una y otra vez.
Puntos clave
- Cada patrón de diseño es una solución general y reutilizable para problemas que ocurren repetidamente y de forma habitual.
- Normalmente cada patrón es una plantilla de solución.
- Los patrones son genéricos y fácilmente personalizables.
Elementos
Los elementos esenciales de un patrón de diseño son:
- Nombre del patrón
- Descripción del problema que resuelve
- Solución (sin describir una especificación en concreto)
- Consecuencias
Ventajas
Las ventajas de los patrones de diseño son:
- Reutilizar soluciones existentes de calidad para problemas que aparecen habitualmente.
- Establecer una terminología común para mejorar la comunicación dentro de los equipos.
- Cambiar el nivel de pensamiento a una perspectiva superior.
- Decidir cuándo se tiene el diseño correcto, no solo uno que funcione.
- Mejorar el aprendizaje individual y del equipo.
- Mejorar la calidad del código y simplifica su modificación.
- Facilitar la adopción de mejores alternativas de diseño, incluso cuando no se usen explícitamente los patrones.
- Descubrir alternativas a grandes jerarquías de herencia.
Los 23 patrones de diseño GoF
- Creational
- #Singleton
- Prototype
- Factory Method
- Builder
- Abstract Factory
- Structural
- Proxy
- Flyweight
- Composite
- Bridge
- Façade
- Decorator
- Adapter
- Behavioral
- Observer
- Strategy
- Template Method (class)
- Command
- Iterator
- Memento
- State
- Mediator
- Chain of Responsibility
- Visitor
- Interpreter (class)
Patrones de clase y patrones de objeto
Estos patrones se refieren al ámbito de clase u objeto según la tabla siguiente:
Ámbito | Creational | Structural | Behavioral |
---|---|---|---|
Clase | Factory | Adapter (object) | Interpreter |
Template | |||
Objeto | Singleton | Proxy | Observer |
Prototype | Flyweight | Strategy | |
Builder | Composite | Command | |
Abstract Factory | Bridge | Iterator | |
Façade | Memento | ||
Decorator | State | ||
Adapter (class) | Mediator | ||
Chain of Responsibility | |||
Visitor |
Los patrones de clase se enfocan en relaciones estáticas y los patrones de objeto en las relaciones dinámicas.
Patrones de clase | Patrones de Objeto | |
---|---|---|
Creación | Difiere la creación de objetos a las subclases | Difiere la creación de objetos a otro objeto |
Estructura | Se enfoca en la composición de las clases (usando el concepto de herencia) | Se enfoca en las diferentes formas de composición de objetos |
Comportamiento | Describe algoritmos y flujos de ejecución | Describe cómo pueden trabajar juntos diferentes objetos para completar una tarea |
Tipos de patrones de diseño
Perspectiva
Los patrones de diseño cambian según su granularidad, nivel de abstracción y cómo se relacionan con otros.
Algunos patrones a menudo se usan conjuntamente; por ejemplo, composite (objeto compuesto) se suele utilizar con iterator (iterador) o visitor (visitante).
Hay patrones que son alternativos; por ejemplo, prototype es a menudo una alternativa de abstract factory.
Algunos patrones, aunque son distintos, llevan a diseños parecidos; por ejemplo los diagramas de estructura de composite y decorator son similares.
Los patrones de diseño se clasifican en familias de patrones relacionados.
Los 23 patrones de diseño dados por GoF (gang of four)1 se organizan según su propósito en estos tres bloques:
- Patrones de creación. Relativos al proceso de creación de objetos.
- Patrones estructurales. Tratan la composición de clases y objetos.
- Patrones de comportamiento. Caracterizan las formas en que clases y objetos interactúan y distribuyen responsabilidades.
<!-- Nota -->
1. Se llama Gang of four a Erich Gamma, John Vlissides, Ralph Johnson y Richard Helm que en 1995 publicaron Design Patterns: Elements of Reusable Object-Oriented Software
<!-- Fin nota -->
También se pueden organizar por alcance:
- Patrones de clase. Tratan las relaciones entre clases y subclases.
- Estas relaciones de establecen a través de la herencia.
- Patrones de objeto. Tratan las relaciones entre objetos, que pueden cambiar en tiempo de ejecución y son más dinámicos.
- Describen cómo los objetos se pueden componer en estructuras mayores usando composición de objetos o incluyendo unos objetos dentro de otros.
Patrones creacionales
Los patrones creacionales proveen varios mecanismos para crear objetos, lo que incrementa la flexibilidad y la reutilización del código existente.
En java, la forma más sencilla de crear una instancia de un objeto es usando el operador new
:
casa = new Casa(); //instancia de la clase Casa
Los patrones de diseño de creación abstraen el proceso de instanciación:
- la lógica de creación está oculta,
- se encapsula el conocimiento sobre qué clases en concreto usa el sistema,
- el programador puede llamar un método o usar otro objeto, en lugar de instanciar objetos directamente con el operador
new
.
Todo lo que conoce el sistema sobre los objetos son sus interfaces, definidas como clases abstractas
- da al programador una gran flexibilidad sobre qué se crea, quién lo crea, cuándo y cómo,
- permite configurar un sistema con objetos "producto" que varían ampliamente en estructura y funcionalidad,
- la configuración puede ser estática (en tiempo de compilación) o dinámica (en tiempo de ejecución).
Los patrones creacionales son:
- #Factory Method
Provee una interfaz para crear objetos en una superclase, pero permite a las subclases modificar el tipo de los objetos que se crearán. - #Abstract Factory
Permite producir familias de objetos relacionados sin especificar sus clases concretas. - Builder
Permite construir objetos complejos paso a paso. El patrón posibilita producir distintos tipos y representaciones de un objeto usando el mismo código de construcción. - Prototype
Deja copiar objetos existentes sin hacer el código dependiente de sus clases. - Singleton
Nos asegura que una clase tiene una única instancia, mientra provee un punto de acceso global a esta instancia.
Patrones estructurales
Describen cómo se pueden combinar clases y objetos para formar estructuras mayores
- utilizan la herencia para componer interfaces o implementaciones,
- describen formas de ensamblar objetos
Los patrones estructurales son:
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
Patrones de comportamiento
Se ocupan de las comunicaciones entre objetos.
Los patrones de comportamiento son:
- Chain of Responsibility
- Command
- Interpreter
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
Patrones de diseño creacionales
Factory Method
El factory method (Método Factoría) o Constructor Virtual es uno de los patrones de diseño más utilizado en Java.
Este patrón define un interfaz para crear un objeto: Creator.
Se utiliza cuando una clase quiere instanciar una subclase de otra clase, pero no sabe cuál. Deja a las subclases decidir qué clase instanciar.
Crea objetos sin exponer la lógica de creación al cliente (Creator) y se refiere al nuevo objeto creado usando una interfaz común (Producto).
Nos da una forma de encapsular las instanciaciones de tipos específicos.
El factory method es ampliamente utilizado en frameworks. Los frameworks usan clases abstractas para definir y mantener las relaciones entre objetos.
El framework no debería saber ni le debería importar cómo instanciar objetos específicos. Las decisiones sobre objetos específicos se tienen que diferir a los usuarios del framework.
El patrón factory method se utiliza cuando:
- una clase no puede anticipar la clase de los objetos que debe crear
- una clase quiere que sus subclases especifiquen los objetos que crea
También es útil cuando se implementa jerarquías de clases paralelas; cuando alguna de las responsabilidades cambia de una clase a otra.
Ejemplos
- Los métodos
getInstance()
dejava.util.Calendar
,ResourceBundle
yNumberFormat
. - El método
valueOf
de clases comoBoolean
,Integer
, etc. SAXParserFactory
. El métodonewInstance
es el factory method que instancia los parseadores sax según cierta lógica predefinida.
Motivación
Supongamos que está desarrollando una aplicación de gestión de logística. La primera versión de la aplicación solo maneja el transporte por camiones, por lo que la mayoría del código reside en la clase Camión
.
Tiempo después la aplicación se vuelve muy popular, y recibe peticiones de las compañías de transporte para incorporar el transporte marítimo. Añadir la clase Barco
en la aplicación obliga a cambiar por completo el código. Si después se decide añadir otro medio de transporte habrá que hacer los cambios de nuevo.
El método factoría nos sugiere reemplazar las llamadas directas a la construcción de objetos (con el operador new
) con llamadas a un método especial factoría. Los objetos devueltos por el método factoría se denominan productos.
classDiagram
class Logistica {
<<interface>>
+ planearEntrega()
+ crearTransporte()
}
class LogisticaCarretera {
+ crearTransporte()
}
class LogisticaMar {
+ crearTransporte()
}
Logistica <|--LogisticaCarretera
Logistica <|--LogisticaMar
- Logistica.planearEntrega()
Transporte t = crearTransporte()
- LogisticaCarretera.crearTransporte()
return new Camion()
- LogisticaMar.crearTransporte()
return new Barco()
Ahora se puede sobreescribir el método factoría en una subclase y cambiar la clase de los productos creados por el método.
Existe una pequeña limitación: las subclases pueden devolver diferentes tipos de productos solo si estos productos tienen una clase base o interfaz común. También el método factoría en la clase básica deberían tener su tipo de retorno declarado en esta interfaz
classDiagram
class Transporte {
<<interface>>
+ entregar()
}
class Camion {
+ entregar()
}
class Barco {
+ entregar()
}
Transporte <|-- Camion
Transporte <|-- Barco
Estructura
classDiagram
class Creador {
<<abstract>>
+ algunaOperacion()
+ Producto: crearProducto()
}
class CreadorA {
+ ProductoA: crearProducto()
}
Creador <|-- CreadorA
class CreadorB {
+ ProductoB: crearProducto()
}
Creador <|-- CreadorB
class Producto {
<<interface>>
+ hacerCosas()
}
Creador --> Producto
Producto <|.. ProductoA
Producto <|.. ProductoB
-
El Producto declara la interfaz, que es común a todos los objetos que se pueden producir por el creador y sus subclases.
-
Los Productos concretos (ProductoA y ProductoB) son distintas implementaciones del interfaz producto.
-
La clase Creador declara el método factoría que devuelve los nuevos objetos producto. Es importante que el tipo que devuelva este método coincida con el interfaz producto.
Se puede declarar el método factoría como abstracto para forzar a todas las subclases a implementar sus propias versiones del método. Como alternativa, el método factoría base puede devolver algún producto tipo por defecto.
Pese a su nombre, la creación del producto no es la principal responsabilidad del creador. Normalmente, la clase creador también tiene cierta lógica de negocio relacionada con los productos. El método factoría ayuda a desacoplar esta lógica de las clases concretas de producto.
-
Los Creadores Concretos (CreadorA y CreadorB) sobrescriben el método factoría base para que devuelva un tipo diferente de producto.
Hay que tener en cuenta que el método factoría no tiene que crear nuevas instancias todo el tiempo. También puede devolver objetos de una caché, un pool, etc.
¿Cuándo aplicarlo?
-
Cuando no se conozca de antemano los tipos exactos y las dependencias de los objetos con los que tiene que trabajar nuestro código.
El Factory Method separa el código de construcción del producto del código que realmente usa el producto. Por tanto, es sencillo extender el código de construcción del producto de forma independiente del resto del código.
Por ejemplo, para añadir un nuevo tipo de producto a la aplicación es suficiente con crear una nueva subclase creador y sobrescribir en ella el método factoría.
-
Cuando se quiera proveer a nuestros usuarios de una librería o framework como una forma de extender sus componentes internos.
-
Cuando se quiera guardar recursos del sistema reutilizando objetos existentes en vez de reconstruirlos cada vez.
- A menudo esto es necesario cuando se trabaja con objetos grandes que consumen muchos recursos, como conexiones a bases de datos, sistemas de ficheros y recursos de red.
- Para reutilizar un objeto existente:
- Es necesario crear algún almacenamiento para seguir la pista a todos los objetos creados.
- Cuando alguien solicite un objeto el programa debe buscar un objeto libre en el pool
- y devolverlo al código cliente.
- Si no existiera ningún objeto libre, el programa debe crear uno nuevo (y añadirlo al pool).
Ventajas
- Desacopla la lógica de negocio de creación de una clase de la propia lógica de la clase; es decir, separa la implementación del producto de su uso.
- Permite cambiar el diseño de la aplicación más fácilmente. Hace el código más robusto, menos acoplado y más fácil de extender.
- Fomenta programar para la interfaz, en vez de para la implementación.
- Provee abstracción entre la implementación y las clases cliente a través de la interfaz.
- Conecta jerarquías de clases paralelas.
- Principio de Responsabilidad Única. Se puede mover el código de creación del producto a un único lugar en el programa, haciendo el código más sencillo de mantener.
- Principio Abierto/Cerrado. Se pueden introducir nuevos tipos de productos en el programa sin estropear el código cliente existente.
Inconvenientes
- Los clientes puede que tengan que hacer subclases de la clase Creator solo para crear un objeto ConcreteProduct particular.
- Hace el código más difícil de leer ya que todo el código está detrás de una abstracción que, a su vez, puede esconder otras abstracciones.
- Se puede convertir en un antipatrón cuando no se utiliza correctamente.
- Al crear demasiados objetos a veces puede bajar el rendimiento.
Implementación
- Se hace que todos los productos sigan la misma interfaz. Esta interfaz debe declarar métodos que tengan sentido en cada producto.
- Se añade un método factoría dentro de la clase creadora. El tipo devuelto por el método debe coincidir con la interfaz común de producto.
- En el código creador se buscan todas las referencias a los constructores de productos. Una a una, se reemplazan con llamadas al método factoría, mientras se extrae el código de creación del producto en el código factoría.
- Ahora se hace un conjunto de subclases creadoras para cada tipo de producto listado en el método factoría. Se sobrescribe el método factoría en las subclases y se extraen los trozos de código de construcción apropiados del método basse.
- Si después de todas las abstracciones, el método factoría base ha quedado vacío, se puede hace abstracto. Si queda algo, se puede hacer el comportamiento por defecto del método
Creador abstracto
La clase Creator es una clase abstracta. Se crea una subclase de la clase Creator por cada tipo de producto que contiene una implementación del factory method. Para usar el factory method (crear objetos) solo se especifica una instancia de ese tipo y se invoca al factory method.
El inconveniente es que cada nuevo producto tiene que hacer una subclase de la clase creator e implementar su factory method.
Esta es la implementación estricta del patrón.
- Producto es la interfaz para el tipo de objeto que crea el método factoría (Factory Method).
- Creador es la interfaz que define el método factoría.
- Cualesquiera otros métodos implementados aquí está escritos para operar en productos producidos por el método factoría.
- La clase creador está escrita sin ningún tipo de conocimiento sobre los productos reales que se van a crear.
- Los clientes necesitan subclases de la clase Creador para hacer productos concretos en particular.
- Solo la subclases implementan realmente el método factoría y crean productos.
- Se decide qué productos reales se van a crear mediante la elección de la subclase a usar.
Ejemplo
-
AbstractShapeFactory.java
:public abstract class AbstractShapeFactory { protected abstract Shape factoryMethod(); public Shape getShape() { return factoryMethod(); } // other helper methods } class RectangleFactory extends AbstractShapeFactory { protected Shape factoryMethod() { return new Rectangle(); } } class SquareFactory extends AbstractShapeFactory { protected Shape factoryMethod() { return new Square(); } } class CircleFactory extends AbstractShapeFactory { protected Shape factoryMethod() { return new Circle(); } }
-
Client.java
:public class Client { public static void main(String[] args) { //get an object of Circle and call its draw method. Shape shape1 = new CircleFactory().getShape(); shape1.draw(); //get an object of Rectangle and call its draw method. Shape shape2 = new RectangleFactory().getShape(); //call draw method of Rectangle shape2.draw(); //get an object of Square and call its draw method. Shape shape3 = new SquareFactory().getShape(); //call draw method of square shape3.draw(); } }
-
Shape.java
:interface Shape { void draw(); } class Rectangle implements Shape { @Override public void draw() { System.out.println("Inside Rectangle::draw() method."); } } class Square implements Shape { @Override public void draw() { System.out.println("Inside Square::draw() method."); } } class Circle implements Shape { @Override public void draw() { System.out.println("Inside Circle::draw() method."); } }
Creador concreto
Relación con otros patrones
- Muchos diseños comienzan utilizando Factory Method (menos complicado y más personalizable) y evolucionan hacia Abstract Factory, Prototype o Builder (más flexibles, pero complicados).
- Las clases Abstract Factory a menudo se basan en un conjunto de Factory Methods, pero también se puede usar Prototype para los métodos de esas clases.
- Se puede usar Factory Method junto con Iterator para que subclases colección puedan devolver diferentes tipos de iteradores que sean compatibles con las colecciones.
- Prototype no está basado en la herencia, así que no tiene sus inconvenientes; por otro lado requiere una inicialización complicada del objeto clonado. Factory Method está basado en la herencia, pero no requiere un paso de inicialización.
- Factory Method es una especialización de Template Method. A la vez un Factory Method puede servir como un paso en un Template Method grande.
Abstract Factory
Abstract Factory (fábrica abstracta) nos permite producir familias de objetos relacionados sin especificar sus clases concretas.
Motivación
Estamos creando una aplicación para una tienda de muebles. El código consiste en clases que representan:
- Una familia de productos relacionados, como
Sillón
+Sofá
+MesaCafé
. - Distintas variaciones de esta familia.
Necesitamos una forma de crear muebles individuales que encajen con otros objetos de la misma familia; por ejemplo, no podemos enviar a un comprador un sillón art decó junto con un sofá victoriano.
Tampoco queremos cambiar el código existente cuando se añadan nuevos productos o familias de productos al programa. Los vendedores de muebles actualizan sus catálogos muy a menudo y no se quiere estar cambiando el código en cada modificación del catálogo.
La solución que indica el patrón Abstract Factory es declarar explícitamente interfaces para cada producto distinto de la familia de productos (sillón, sofá o mesá café). Entonces se pueden hacer todas las variantes de productos a través de dichas interfaces. Por ejemplo, todas las variantes de sillón pueden implementar el interfaz Sillon
.
classDiagram
class Sillon {
<<interface>>
+ tienePatas()
+ sentarse()
}
class SillonVictoriano {
+ tienePatas()
+ sentarse()
}
class SillonModerno {
+ tienePatas()
+ sentarse()
}
Sillon <|.. SillonVictoriano
Sillon <|.. SillonModerno
El siguiente paso es declarar el Abstract Factory, una interfaz con una lista de métodos de creación para todos los productos que son parte de la familia de productos (por ejemplo, creaSillon
, creaSofa
y creaMesaCafe
). Estos métodos deben devolver tipos de producto abstractos representados por las interfaces anteriores: Sillon
, Sofa
, MesaCafe
.
Para cada variante de una familia de productos, se crea una clase factoría separada basada en la interfaz AbstractFactory
. Una factoría es una clase que devuelve productos de un tipo particular. Por ejemplo FactoriaModerna
solo puede crear objetos SillonModerno
, SofaModerno
y MesaCafeModerna
.
classDiagram
class FactoriaMuebles {
<<interface>>
+crearSillon() Sillon
+crearMesaCafe() MesaCafe
+crearSofa() Sofa
}
class FactoriaMueblesVictorianos {
<<interface>>
+crearSillon() Sillon
+crearMesaCafe() MesaCafe
+crearSofa() Sofa
}
class FactoriaMueblesModernos {
<<interface>>
+crearSillon() Sillon
+crearMesaCafe() MesaCafe
+crearSofa() Sofa
}
FactoriaMuebles <|.. FactoriaMueblesVictorianos
FactoriaMuebles <|.. FactoriaMueblesModernos
El código cliente tiene que funcionar con factorías y productos vía sus respectivas interfaces abstractas. Esto que permite cambiar el tipo de una factoría que se pasa al código cliente, tanto como una variante de producto que el cliente código recibe, sin estropear el código cliente real.
Si el cliente quiere una factoría para producir un sillón, no tiene que preocuparse sobre la clase de la factoría, ni tampoco qué tipo de sillón es. El cliente tiene que tratar todos los sillones de la misma forma, usando la interfaz abstracta Sillon
.
Estructura
- Productos abstractos. Declaran interfaces para un conjunto de productos distintos pero relacionados que componen una familia de productos.
- Productos concretos. Son varias implementaciones de los productos abstractos, agrupados por variantes. Cada producto abstracto (sillón, sofá) se debe implementar en todas sus variantes (victoriana, moderna).
- Factoría abstracta. Interfaz que declara un conjunto de métodos para crear cada uno de los productos abstractos.
- Factorías concretas. Implementan los métodos de creación de la factoría abstracta. Cada factoría concreta corresponde a una variante específica de productos y solo crea esa variante de productos.
- Cliente. Aunque las factorías concretas instancian productos concretos, sus métodos de creación deben devolver los productos abstractos correspondientes. De esta forma el código del cliente que usa una factoría no está acoplado a la variante específica del del producto que obtiene de la factoría.
¿Cuándo aplicarlo?
- Cuando se necesite trabajar con varias familias de productos relacionados, pero no se quiera depender de clases concretas de dichos productos (porque puede que no sean conocidos de antemano, o se quiera permitir una extensibilidad futura).
Implementación
- Crear una matriz de los distintos productos contra las variantes de dichos productos.
- Declarar interfaces para cada tipo de producto abstracto. Después construir todas las clases de producto concretas que implementen esas interfaces.
- Declarar el interfaz de la factoría abstracta con un conjunto de métodos de creación para todos los productos abstractos.
- Implementar un conjunto de clases de factoría concretas, una por cada variante de producto.
- Crear un código de inicialización de la factoría en algún lugar de la aplicación. Debería instnaciar una de las clases de factoría concretas, dependiendo de la configuración de la aplicación del entorno actual. Se pasa es objeto factoría a todos las clases que construyen productos.
- Se busca en el código todas las llamadas directas a constructores de productos. Se reemplazan con las llamadas al método apropiado de creación del objeto en la factoría.
Ventajas
- Se puede asegurar que los productos que se obtienen de una factoría son compatibles entre sí.
- Se evita el acoplamiento entre productos concretos y código cliente.
- Principio de Responsabilidad Única. Se puede extraer el código de creación del producto a un único lugar, haciendo el código más sencillo de mantener.
- Principio Abierto/Cerrado. Se pueden introducir nuevas variantes de productos sin estropear el código existente.
Inconvenientes
- El código se puede volver más complicado de lo que debería, ya que se añaden muchas nuevas interfaces y clases.
Relación con otros patrones
- Muchos diseñadores comienzan usando el Factory Method (menos complicado y más personalizable vía subclases) y evolucionan hacia Abstract Factory, Prototype o Builder (más flexibles, pero más complicados).
- Builder se centra en construir objetos completos paso a paso. Abstract Factory está especializado en crear familias de objetos relacionados. Abstract Factory devuelve el producto inmediatamente, mientras que Buidder permite ejecutar pasos de construcción adicionales antes de obtener el producto.
- Las clases Abstract Factory se suelen basar en un conjunto de Factory Methods, pero también se puede usar Prototype para componer los métodos de esas clases.
- Abstract Factory puede ser una alternativa a Facade cuando solo se quiere esconder la forma en que los objetos del subsistema son creados al código cliente.
- Se puede usar Abstract Factory junto con Bridge. Es útil cuando algunas abstracciones definidas por Bridge solo pueden trabajar con implementaciones específicas; Abstract Factory puede encapsular estas relaciones y esconder la complejidad al código cliente.
- Abstract Factories, Builders y Prototypes se pueden implementar como Singletons.
Singleton
Instancia única
Garantiza que una clase tenga una única instancia, a la vez que proporciona un punto de acceso global a dicha instancia.
Problema
El patrón de diseño resuelve dos problemas a la vez, vulnerando el Principio de responsabilidad única:
-
Garantizar que una clase tenga una única instancia.
El motivo más habitual para querer que una clase tenga solo una instancia es controlar el acceso a un recurso compartido, como una base de datos.
La forma de funcionamiento es: si se ha creado un objeto y al cabo de un tiempo se decide crear otro nuevo, en vez de recibir un objeto nuevo lo que se recibirá es el que se había creado en un principio.
Este comportamiento es imposible implementarlo con un constructor normal, ya que una llamada al constructor siempre debe devolver un nuevo objeto.
-
Proporcionar un punto de acceso global a dicha instancia.
Al igual que una variable global, el patrón Singleton nos permite acceder a un objeto desde cualquier parte del programa. No obstante, evita que otro código sobreescriba esa instancia.
Solución
Todas las implementaciones del patrón Singleton tienen estos dos pasos en común:
- Hacer privado el constructor por defecto para evitar que otros objetos utilicen el operador
new
con la clase Singleton. - Hacer un método de creación estático que actúe como constructor. Este método lo que hace la primera vez que se le llama es invocar al constructor privado para crear un objeto y lo guarda en un campo estático. Las siguientes llamadas a este método devuelven el método almacenado en caché.
Estructura
classDiagram
Singleton <|-- Singleton
Singleton <|-- Client
class Singleton {
- instance: Singleton
- Singleton()
+getInstance(): Singleton
}
if (instance == null) {
//Si se soporta multihilo se debe colocar un bloqueo de hilo aquí
instance = new Singleton()
}
return instance
Aplicabilidad
- Cuando una clase solo deba tener una instancia disponible para todos los clientes: por ejemplo un único objeto de base de datos compartido por distintas partes del programa.
- Cuando se necesite un control más estricto de las variables globales.
Implementación
-
Añadir un campo estático privado a la clase para almacenar la instancia Singleton.
-
Declarar un método de creación estático público para obtener la instancia Singleton.
-
Implementar una inicialización diferida dentro del método estático.
Debe crear un nuevo objeto en su primera llamada y colocarlo dentro del campo estático. El método deberá devolver siempre esa instancia en todas las llamadas siguientes.
-
Declarar el constructor de clase como privado. El método estático de la clase seguirá siendo capaz de invocar al constructor, pero no a los otros objetos.
-
Repasar el código cliente y sustituir todas las llamadas directas al constructor de la instancia Singleton por llamadas a su método de creación estático.
Pros
- Se tiene la certeza de que una clase tiene una única instancia.
- Se obtiene un punto de acceso global a dicha instancia.
- El objeto Singleton solo se inicializa cuando se requiere por primera vez.
Contras
- Vulnera el Principio de responsabilidad única. El patrón resuelve dos problemas al mismo tiempo.
- Puede enmascarar un mal diseño. Por ejemplo, cuando los componentes del programa saben demasiado los unos sobre los otros.
- Requiere de un tratamiento especial en un entorno con múltiples hilos de ejecución, para que varios hilos no creen un objeto Singleton varias veces.
- Puede resultar complicado realizar la prueba unitaria del código cliente del Singelton.
Relaciones con otros patrones
- Una clase fachada se puede transformar a menudo en una Singleton.
- Flyweight se asemeja a Singleton reduciendo los estados compartidos de los objetos a un único objeto flyweight.
- Los patrones Abstract Factory, Builder y Prototype se pueden implementar como Singleton.
Patrones de diseño orientado a objetos
Inyección de dependencias
Inyección de dependencias (Dependency Injection, DI) es un patrón de diseño orientado a objetos, en el que se suministran objetos a una clase en lugar de ser la propia clase la que cree dichos objetos. Esos objetos cumplen contratos que necesitan nuestras clases para poder funcionar (de ahí el concepto de dependencia). Nuestras clases no crean los objetos que necesitan, sino que se los suministra otra clase 'contenedora' que inyectará la implementación deseada a nuestro contrato.
Se trata de un patrón de diseño que se encarga de extraer la responsabilidad de la creación de instancias de un componente para delegarla en otro.
Implementación del patrón en Java
El contenedor
La forma habitual de implementar este patrón es mediante un "Contenedor DI", también llamado "Contenedor IoC" y objetos planos o simples -por ejemplo los llamados POJO en Java-. El contenedor inyecta a cada objeto los objetos necesarios según las relaciones de dependencia registradas en la configuración previa.
Típicamente este contenedor es implementado por un framework externo a la aplicación (como Spring), por lo cual en la aplicación también se utilizará inversión de control al ser el contenedor (almacenado en una biblioteca) quien invoque el código de la aplicación.
Declaración de la inyección
La inyección de dependencias puede realizarse referenciando a las clases de dichas dependencias. Sin embargo, esa no es una buena práctica dado que sus componentes tienen una fuerte relación entre sí, que al final nos supondrá un inconveniente para el mantenimiento del software. Por eso, en la inyección de dependencias, normalmente, se usan interfaces. De esta forma conseguimos abstraer la relación entre una clase A que depende de una clase B sin importar la implementación de cada uno de los dos. De esta forma, conseguimos desacoplamiento.
Formas de inyectar las dependencias
En el constructor
Por ejemplo si una clase A tiene la dependencia de un objeto con el interfaz B
public class A {
private B dependency;
public A(B instancedepency)
{
this.dependency=instancedepency;
}
//... En su implementación A usa el objeto dependency
}
En un método
Usando un método, típicamente el método suele ser un setter.
public class A {
private B dependency;
public setDependecy(B instancedepency)
{
this.dependency=instancedepency;
}
//... En su implementación A usa el objeto dependency
}
En una variable de instancia
Usando una variable de instancia o propiedad.
public class A {
public B dependency;
//... En su implementación A usa el objeto dependency
}
Ejemplo
A continuación se muestra una implementación sin inyección de dependencias.
public class Vehiculo {
private Motor motor = new Motor();
/** @return retorna la velocidad del vehículo*/
public Double enAceleracionDePedal(int presionDePedal) {
motor.setPresionDePedal(presionDePedal);
int torque = motor.getTorque();
Double velocidad = ... //realiza el cálculo
return velocidad;
}
}
//se omite la clase Motor ya que no es relevante para este ejemplo
Esta implementación necesita crear una instancia de Motor para calcular su velocidad.
A continuación se muestra una implementación realizando inyección de dependencias usando un método setter.
public class Vehiculo {
private Motor motor = null;
public void setMotor(Motor motor){
this.motor = motor;
}
/** @retorna la velocidad del vehículo*/
public Double enAceleracionDePedal(int presionDePedal) {
Double velocidad = null;
if (null != motor){
motor.setPresionDePedal(presionDePedal);
int torque = motor.getTorque();
velocidad = ... //realiza el cálculo
}
return velocidad;
}
}
//se omite la clase Motor ya que no es relevante para este ejemplo
public class VehiculoFactory {
public Vehiculo construyeVehiculo() {
Vehiculo vehiculo = new Vehiculo();
Motor motor = new Motor();
vehiculo.setMotor(motor);
return vehiculo;
}
}
En este ejemplo VehiculoFactory
representa al proveedor. Es una aplicación sencilla del patrón de diseño factory que hace posible que la clase Vehículo
no requiera saber cómo obtener un motor por sí misma, sino que es la responsabilidad de VehiculoFactory
.