Herramientas de usuario

Herramientas del sitio


code:dessignpatterns

Dessign Patterns

Creational Patterns

Abstract Factory

El patrón Abstract Factory se basa en un objeto que clasifica los objetos por famílias y crea dichos objetos según la família demandada.
UML

  • AbstractProductA y AbstractProductB son las abstracciones para los productos A y B, estos dos, a su vez pueden pertenecer a distintas familias: 1 y 2.
  • AbstractFactory es la base para las clases ConcreFactory1 y ConcretFactory2, ambas pueden crear productos A y B, pero cada uno será de distintas famílias.
  • El cliente sólo interactúa con el AbstratProductA y el AbstractProductB, para él es intrascendente (trivial, nimio) de qué família provengan.
// AbstractFactory
interface Factory {
  Button getButton ();
  TextBox getTextBox();
}
 
// ConcreteFactory1
class FactoryFramework1 implements AbstractFactory {
  Button getButton () {
    // Retorna un botón del framework 1
  }
  TextBox getTextBox () { ... }
}
 
// ConcreteFactory2
class FactoryFramework2 implements AbstractFactory {
  Button getButton () {
    // Retorna un botón del framework 2
  }
  TextBox getTextBox () { ... }
}
 
// AbstractProductA
interface Button {
  public void Click ();
}
 
// ConcreteProductA1
class ButtonFramework1 extends GKButton implements Button {
  public void Click () {
    this.pushButton();
  }
}
 
// ConcreteProductB1
class ButtonFramework2 extends QTButton implements Button {
  public void Click () {
    this.ClickEvent();
  }
}
 
// Client
Factory fact = new FactoryFramework1 ();
Button b = fact.getButton();               // Devolverá el botón de la família demandada (1)
  • El cliente no tiene por qué tener conocimientos del uso de varias familias y no tiene que tratar con ninguna de ellas, sino con un tipo de datos standard.
  • Es fácil para el cliente instanciar una nueva familia, pero no la implementación de esta.

Builder

Separa la construcción del objeto de su interacción con él, para ello se encapsula los métodos de creación en una interface y es a partir de los métodos de esta con la que crearemos el objeto.

UML En el UML…

  • Builder es la especificación de los métodos necesarios para construir un Product.
  • ConcretBuilder cada clase que corresponde al objeto constructor de un objeto producto. Hereda de Builder e implementando los métodos de Builder especifica las distintas propiedades de los objetos Product.
  • Director llama a los métodos de un objeto Builder que le pasen para construirlo.
// Clase Product
public class Vehicle {
  // getters y setters de sus propiedades
}
 
// Clase Builder
abstract class VehicleBuilder {
    protected Vehicle vehicle;
    public VehicleBuilder () {
       this.vehicle = new Vehicle();
    }
 
    public Vehicle getVehicle {
      return this.vehicle;
    }
 
    public abstract void BuildEngine();
    public abstract void BuildWheels();
    public abstract void BuildDoors();
}
 
// ConcretBuilder 1
class MotorCycleBuilder : VehicleBuilder {
    public override void BuildEngine() {
      vehicle.setEngine(500);
    }
 
    public override void BuildWheels() {
      vehicle.setWheels(2);
    }
 
    public override void BuildDoors() {
      vehicle.setDoors(0);
    }
}
 
// ConcreteBuilder2
class CarBuilder : VehicleBuilder {
    public override void BuildEngine() {
      vehicle.setEngine(2500);
    }
 
    public override void BuildWheels() {
      vehicle.setWheels(4);
    }
 
    public override void BuildDoors() {
      vehicle.setDoors(4);
    }
}
 
// Clase Director
class Shop {
    public static Vehicle Construct(VehicleBuilder vehicleBuilder) {
      vehicleBuilder.BuildEngine();
      vehicleBuilder.BuildWheels();
      vehicleBuilder.BuildDoors();
      return vehicleBuilder.getVehicle();
    }
}
 
// Construir un coche:
Vehicle myCar = Shop.Construct (new CarBuilder());

Factory Method

Cuando tenemos que escoger entre varias clases relacionadas para crear un objeto utilizaremos el Factory Method, este se basa en la idea de una creación dinámica, que cambia según las necesidades del objeto que está a punto de crearse.
Para ello se utiliza una interfaz (o clase abstracta), llamada creadora de productos, que contiene el método factoría (factory method) este devuelve objetos del tipo Producto, que es una interface de la cual heredan los productos concretos. El método factoría, el que devuelve los productos, lo que hace es “configurar” un producto concreto, dando a este las propiedades que lo distinguen de los demás productos concretos. La clave para realizar dicha “configuración” viene dada por las clases creadoras concretas que heredan de la creadora de productos y sobreescriben el método factoría, cada una de ellas devuelve en este método el producto concreto que le toca personalizando las propiedades de este.
UML
Por ejemplo, idearemos un programa que te crea textos concretos (libros, articulos, ensayos, libros de referencia…), para ello configura el texto con diversas secciones. En este caso la interface Seccion sería el producto, cada una de las clases que heredasen de Seccion serían los productos concretos. La clase abstracta Texto será la creadora de productos y cada tipo de textos los creadores concretos. El método factoría sería el configSecciones.

// Producto
interface Seccion { ... };
 
// Productos concretos
class Indice : Seccion { ... };
class Presentacion : Seccion { ... };
class Introduccion : Seccion { ... };
class Capitulo : Seccion { ... };
class Prologo : Seccion { ... };
class Epilogo : Seccion { ... };
class Anexo : Seccion { ... };
 
// Creadora de productos
abstract class Texto {
   LinkedList secciones = new LinkedList();
   public Texto {
      this.configSecciones ();
   }
   public abstract void configSecciones ();
};
 
// Producto concreto: Libro
class Libro : Texto {
  public void configSecciones () {
    this.secciones.add (new Introduccion ());
    this.secciones.add (new Prologo ());
    this.secciones.add (new Capitulo ());
    this.secciones.add (new Epilogo ());
  }
}
 
// Producto concreto: Articulo
class Articulo : Texto {
  public void configSecciones () {
    this.secciones.add (new Indice ());
    this.secciones.add (new Introduccion ());
    this.secciones.add (new Capitulo ());
  }
}

Utilizaríamos el ejemplo de anterior creando objetos de la siguiente manera:

Texto texto1 = new Libro ();
Texto texto2 = new Articulo ();

Prototype

Especifica que la aparición de un nuevo objeto en nuestro escenario programado no tiene por qué crearse como uno nuevo sino que puede ser una copia de otro objeto ya creado previamente.
Algunos frameworks ya proveen interfaces que facilitan la implementación de este patrón, por ejemplo Java permite usar la interface Cloneable que contiene un método Clone. Para implementar el patrón prototype en una clase própia debemos implementar esta interface y sobreescribir este método realizando y devolviendo en él una copia del objeto.

UML

  • En según qué casos es más complejo copiar un objeto que volver a crear uno igual.
  • A veces puede ser más eficiente copiar un objeto que crearlo de nuevo.

Singleton

Su propósito es asegurar que sólo existe una única instancia de una clase para todo el programa y que esta sea de acceso global. Para ello el acceso al constructor ha de ser privado, o como muy permisivo protegido. La clase deberá de tener de forma estática y privada una instancia al objeto único que existirá y un método estático público que devolverá dicha instancia; de esa forma, el método puede acceder al constructor y al objeto para crearlo y devolverlo (sólo lo creará la primera vez) pero desde fuera de la clase no se podrán crear otros objetos.

public class Singleton {
    private static Singleton INSTANCE = null;
    private Singleton() {}
 
    private synchronized static void createInstance() {
        if (INSTANCE == null)
            INSTANCE = new Singleton();
    }
 
    public static Singleton getInstance() {
        if (INSTANCE == null) 
            createInstance();
        return INSTANCE;
    }
}

Structural Patterns

Adapter

Consiste en adaptar una clase a otra; en otras palabras, modificar una clase (clase adaptada) mediante otra (clase adaptadora) para que otra (clase cliente) la utilice. Sus usos son:

  • Teniendo una clase que debe ser usada de una forma especial, utilizar otra clase para que esta forma de utilización pueda llevarse a cabo. Por ejemplo tenemos una clase linked list (adaptada) que sólo debe almacenar “objetos árboles” de tamaño mayor que 5, para no tener que modificar esta clase y realizar así dicha restricción añadimos otra clase llamada “lista de árboles” (adaptadora), esta tendrá métodos añadir, eliminar… que una linked list tiene y, a la vez, llamará a esos para realizar dichas acciones, aunque adaptará el método añadir para filtrar que los árboles introducidos tengan altura mínima 5.
  • Teniendo una serie de clases (adaptadas) que representan una lista de objetos (pilas, arrays, colas…) y que para utilizarlas de forma uniforme es tedioso (ya sea porque ninguna hereda de una clase en común, porque sus nombres de métodos son distintos…), se crea una clase adaptadora para cada una que facilite la utilización de estas.

Existen dos formas de llevar a cabo el patrón adaptador:

  • Por herencia, la clase adaptadora hereda de la clase adaptada y sobreescribe los métodos que quiera modificar.
  • Por delegación, la clase adaptadora contiene un objeto de la clase adaptada, implementa los métodos que quiera adaptar y estos llamarán a los métodos de su objeto.

UML Otro ejemplo. Desarrollamos una librería para dibujar figuras, conocemos como dibujar línias y cuadrados y para ello creamos las clases Line y Square que heredan de Shape, para dibujar círculos creamos la clase Circle pero como no sabemos dibujarlos utilizamos una librería que contiene una clase que los dibuja, la XCircle. Nuestra clase Circle contendrá un objeto XCircle y llamará a los métodos de este cuando se llame a los suyos.

Bridge

En un objeto separa la abstracción (interface, clase abstracta de la que hereda) de la implementación (codificación) para agilizar así el cambio de estas independientemente, de esa forma mejora la extensivilidad del código y, a la vez, esconde la implementación a los clientes.
Esquema
En el esquema tenemos una clase abstracta Window que tiene un objeto interno del tipo WindowImp, este es una interface de la que heredan XWindowImp y PMWindowImp. Debemos saber con antelación qué motor implementará la ventana (XWindow o PMWindow), y cuando hagamos alguna acción sobre esta se llamará a los métodos de la instancia WindowImp que tiene. Las implementaciones IconWindow y TransientWindow no son más que tipos de ventana específicos.

UML

Composite

Es la cualidad de crear objetos complejos a partir de otros más sencillos y similares, aprovechando una estructura recursiva y arbolea.
 UML y Esquema

  • Permite a una serie de objetos ser tratados como uno solo.
  • Al tener todos estos objetos una interface común pueden ser tratados de la misma manera.
  • Los objetos tienen una relación de árbol; los objetos pueden contener otros objetos que a su vez pueden contener otros.
  • Muy usado en el desarrollo de GUI, por ejemplo a un objeto Ventana se le añade un objeto Panel y a este, a su vez, tiene añadidos otros objetos (cuadros de texto, botones…).

UML La función de los elementos que componen este diagrama es:

  • Component: Es una abstracción (interface o clase abstracta) de un objeto compuesto, define una forma de acceder a los objetos de los que está compuesto (hijos) (puede también añadir una forma para acceder al objeto padre); si es una clase abstracta implementa todos los métodos comunes de un objeto compuesto.
  • Leaf: Representa el conjunto de hijos, cada uno con su comportamiento concreto.
  • Composite: Implementan el comportamiento de un componente concreto a parte de almacenar otros componentes hijos e implementar métodos para relacionarlos (añadir, eliminar…).

Decorator

Añaden funcionalidad extra, dinámica y externamente a una o varias clases.
Por ejemplo un objeto cuadro de texto al que queremos asignar unas barras de scroll, estas son otro objeto de otra clase y la funcionalidad que le añaden es la de la movilidad por el texto. El objeto Decorator sería el correspondiente a las barras de scroll, y como tal, debe contener una lista de los objetos a los que afecta; aunque en este caso concreto ha sido únicamente un cuadro de texto podría haber sido un objeto Libro, Vídeo… a los que se les añade la capacidad de ser prestados.

  • Utilizar objetos decorator para añadir funcionalidad es un método más flexible que la herencia tradicional.
  • Los objetos pueden ser agregados o desagregados en tiempo de ejecución.
  • Podemos añadir un objeto decorator a otro (por lo que también tiene que implementar la abstracción que implementan los objetos que manipula).
  • Un decorator y los objetos que engloba no son idénticos.
  • A veces añadir un decorator es mejor que manipular las entrañas del objeto, optar por esta segunda opción sería utilizar el patrón Strategy.
  • Los objetos se deben poder agregar y desagregar de forma sencilla.

UML

Facade

En este patrón una única clase representa todo un conjunto de clases y de acciones, la gracia está en que esta clase ha de proveer una forma sencilla de utilizar el sistema al que reemplaza. Ha de proveer clases sencillas que sirvan para tareas concretas.
Este patrón es útil, por ejemplo, cuando…

  • Existe un grupo de tareas que se han de realizar frecuentemente, el patrón facade agrupa estas tareas en único método más sencillo y claro que se llamará cuando sea necesario realizar dichas tareas.
  • Tenemos una API muy útil, pero mal diseñada. Se crea una API intermedia que facilite la utilización de la primera.
  • Existe un lío de dependencias entre “clientes” y bibliotecas. Se crea dependencia entre clientes → clase Facade → biblioteca.
  • Una biblioteca es dificilmente comprensible. Se crea una intermedia que utiliza esta pero permite realizar sus funciones de una forma más sencilla.

Esquema
* La diferencia entre el patrón adaptador y el facade es que el primero adapta (amplia, mejora, restringe…) un código, mientras que el facade facilita el uso.

Flyweight

Su objetivo es optimizar el uso de la memoria agrupando en un objeto (FlyweightObject) propiedades comunes de otros objetos; el FlyweightObject estará contenido como una referencia dentro de los demás que tengan esas propiedades comunes y de igual valor.
Por ejemplo, imaginemos 100 objetos pelota, cada uno de ellos con un diámetro, un color y una posición en pantalla. De esas 100 pelotas 75 son rojas y de diámetro 4, las otras 25 azules y de diámetro 5; lo único que cambia para cada una de ellas es la posición. El FlyweightObject contendría las propiedades color y diámetro y las pelotas una referencia a un objeto FlyweightObject y un objeto Point que indica la posición, existirían sólo dos objetos FlyweightObject (uno para las rojas de diámetro 4 y otro para las azules de diámetro 5) y 100 objetos pelota con sus 100 objetos Point.
Otro ejemplo sería el de carácteres de un párrafo en un procesador de textos, cada carácter contiene un formato concreto (fuente, tamaño, negrita…), pero en general todos contienen las mismas propiedades, es correcto que ellos almacenen un FlyweightObject.

  • Debemos combinar este patrón con el patrón Factory, el cual se encargaría de gestionar, dar a cada objeto los objetos FlyweightObject y crear nuevos FlyweightObject si son necesarios.

Proxy

Se utiliza un intermediario entre varios objetos para controlar el acceso de uno a otro. Para ello el cliente tiene acceso a un objeto Proxy que enmascara otro objeto, el que realmente está utilizando el cliente, es decir, dentro del Proxy se llaman a los métodos del objeto real permitiendo\impidiendo que se realicen acciones sobre este.

  • Se impide que un objeto que puede ocupar gran espacio en memoria se cree o duplique innecesariamente.

UML
El patrón Proxy permite las siguientes variantes:

  • Proxy remoto: Cuando provee acceso a otro objeto localizado en otra porción de memoria o máquina.
  • Proxy virtual: Realiza las operaciones de otro objeto para que este no se cree a no ser que sea necesario.
  • Copy-on-write proxy: Retrasa la clonación de un objeto hasta que no sea realmente necesaria (proxy virtual).
  • Proxy de protección: Distingue entre tipos de clientes para darles distintos niveles de acceso.
  • Proxy con caché: Almacena temporalmente resultados de operaciones de alto coste y las comparte con varios clientes.
  • Firewall-Proxy: Protege un objeto de operaciones que otro realice.
  • Proxy de sincronización: Provee control de concurrencia entre objetos.
  • Proxy inteligente: Provee acciones adicionales del objeto que referencia (como indicar número de copias existentes…).

Proxy Virtual

// Subject
interface Image {
   public void displayImage();
}
 
// RealSubject
class RealImage implements Image {
   private String filename;
   public RealImage(String filename) { 
       this.filename = filename;        
       System.out.println("Cargando "+ filename);
   }
   public void displayImage() { 
       System.out.println("Displaying "+ filename); 
   }
}
 
// Proxy
class ProxyImage implements Image {
   private String filename;
   private Image image;
   public ProxyImage(String filename) { 
       this.filename = filename; 
   }
   public void displayImage() {
       if (image == null) {
           image = new RealImage(filename); // load only on demand
       }
       image.displayImage();
   }
}
 
class Client {
   public static void main(String[] args) {
       List<Image> images = new ArrayList<Image>();
       images.add( new ProxyImage("HiRes_10MB_Photo1") );
       images.add( new ProxyImage("HiRes_10MB_Photo2") );
       images.add( new ProxyImage("HiRes_10MB_Photo3") );
 
       images.get(0).displayImage();
       images.get(1).displayImage();
       images.get(0).displayImage();
   }
}
 
/*
1. Se carga la primera imágen y se muestra.
2. Se carga la segunda imágen y se muesra.
3. Se muestra la primera imágen ya cargada.
4. La tercera imágen no llega a cargarse nunca.

Proxy de protección

   public interface IClient{
       string Login();
   }
 
   public class RealClient : IClient{
       public string Login(){
           return "Logged";
       }
   }
 
 
   public class ProtectionProxy : IClient 
   {
       private string password=null;
       RealClient client=null;
 
       public ProtectionProxy(string pwd)
       {
           Console.WriteLine("ProtectionProxy: Initialized");
           password=pwd;
       }
       public String Login() {
           if(tmpPwd == password) {
               client=new RealClient();
               return "Successfull!";
           } else
               return "Fail!";
       }
   }
 
   class ProtectionProxyExample {
       public static void Main(string[] args) {
           IClient client = new ProtectionProxy("thePassword");
           Console.WriteLine(client.Login());
       }
   }

Behavioral Patterns

Chain of Responsibility

Se basa en una serie de objetos por la que circula una petición realizada a un objeto inicial, tras recibirla este la pasa al siguiente objeto y así irá pasándose hasta llegar al objeto adecuado para procesarla. Todos estos objetos receptores derivarán de una misma interface o clase abstracta, que provea un método para obtener el sucesor, esto hará que la cadena sea más flexible y transparente.
Existen varias técnicas para desarrollar esta cadena:

  • Aprovechando enlaces ya creados, como por ejemplo las relaciones padre-hijo que un patrón composite implementa.
  • Si no existiesen enlaces deberíamos crear uno conectando los objetos. Una forma sería haciendolos derivar de una misma interface, esta debe de tener internamente un objeto que también deriva de ese mismo tipo el cual representaría el siguiente en la cadena.

UML

// Handler
public abstract class Empleado {
  Empleado sucesor;
  public void setSucesor (Empleado e) {
    this.sucesor = e;
  }
  public void ReceivePeticion (Peticion pet) {
    if (Condition(pet))
      Process (pet);
    else {
      if (this.sucesor != null)
        this.sucesor.ReceivePeticion(pet);
      else
        // Gestión de la petición si no existe sucesor
    } 
  }
  abstract void Process (Peticion pet);
  abstract boolean Condition (Peticion pet);
}
 
// Concrete Handler1
public class Director extends Empleado {
  boolean Condition (Peticion pet) {
    return (pet.dinero >= 5000);
  } 
  void Process (Peticion pet) {
    // Proceso de una petición para el director
  }
}
 
// Concrete Handler2
public class Gerente extends Empleado {
  boolean Condition (Peticion pet) {
    return (pet.dinero >= 2500);
  } 
  void Process (Peticion pet) {
    // Proceso de una petición para el gerente
  }
}
 
// Deben ser asignados los sucesores
Gerente ger = new Gerente();
Director dir = new Director();
dir.setSucesor(ger);
  • Se simplifica la estructura interna del objeto, cada objeto se encarga de un problema concreto.
  • Se añade flexibilidad, distribuyendose la responsabilidad entre varios objetos.
  • Se desvincula al emisor de la petición del receptor.
  • Se debe debe decidir si se realizará un control para toda petición sea procesada; en definitiva, si se controlará qué ocurre si no llega a existir ningún elemento de la cadena encargado de un tipo de peticiones concreto (este control puede ser una ventaja o desventaja según se mire).

Command

Utiliza las acciones de los objetos como otros objetos, de esa forma nos permite clasificar las peticiones, registrarlas y hasta deshacerlas.

UML

  • Command es la interface para cualquier acción\operación. Las operaciones en concreto son las ConcretCommand, ellas almacenan un objeto Receiber este recibirá la acción cuando se llame al método Execute declarado en Command.
  • Invoquer será el encargado de hacer que el comando se ejecute, colocará todo comando que vaya lanzando en una pila, de esa forma se implementará la capacidad de hacer\rehacer.
  • Client crea un ConcretComman y le asgna un Receiber.
interface Command {
  public Number Execute (Number n);
  public Number Undo (Number n);
}
 
// Receiver
class Number {
  int value = 0;
  public Number (int v) {
    this.value = v;
  }
  public int getValue () {
    return this.value;
  }
}
 
class Invoker {
  int idx = 0;
  Stack commands;
  Number vActual;
  public void Execute (char operation, Number n) {
    Command c;
    switch (operation) {
      case '+':
        c = new Suma (n);
        break;
      case '-':
        c = new Resta (n);
        break;
    }
    vActual = c.Execute();
    commands.Push(c);
    idx++;
  }
  public void Undo () {
    idx--;
    Command c = commands.Pop();
    vActual = c.Undo(vActual);
  }
}
 
// ConcreteCommand
public class Suma implements Command {
  Number value;
  public Suma (Number n) {
    this.value = n;
  }
  public Number Execute (Number n) {
    return new Number(this.value.getValue() + n.getValue());
  }
  public Number Undo (Number n) {
    return new Number(n.getValue() - this.value.getValue());
  }
}

Este patrón nos proporciona:

  • Capacidad de deshacer acciones, cuando un usuario quiere deshacer uno de los comandos lanzados el programa hará un pop de la pila de comandos sacanda el último y ejecutará su método undo.
  • Capacidad de rehacer; si se ejecuta un método deshacer se puede devolver el command contrario (un command suma pasa a ser un command resta) y cuando se pide rehacer volvería a pasarse el contrario, el primero (el command resta devolvería un suma).
  • Se facilita la creación de un algoritmo commit-rollback.
  • Facilita la creación de wizards (asistentes). Cada página de este contiene un objeto command que se almacena en una cola a medida que las páginas van pasando, una vez se llegue al final se llamarán a los métodos execute de cada uno de estos command.
  • Facilidad para la creación de macros, si se almacenan los objetos command lanzados luego pueden volver a ser lanzados de nuevo de forma secuencial.
  • Podemos traducir cada command a una o varias sentencias de script. (De command a script y viceversa).
  • Podemos enviar objetos command en red y estos serán ejecutados en otra máquina objetivo.

Interpreter

Su idea básica es la especificación e implementación un sub-lenguaje que resuelva rápidamente un tipo de problemas dinámicos.
Por ejemplo, desarrollar una clase interprete para expresiones matemáticas, esta recibiría en el constructor un String con la expresión a calcular y tendría un método “Solution” que procesaría dicha expresión y retornaría un resultado. Para procesar la expresión se ayudaría de clases que representasen los números, los signos (una para cada signo, realizando cada una la acción concreta con los objetos número)…

  • La gramática del lenguaje interpretado no debe ser compleja.
  • Tal vez tengas que utilizar árboles sintácticos o expresiones regulares.
  • Para la solución del problema la eficiencia\rapidez no es un factor importante.
  • Es fácil extender y ampliar la gramática, ya que el intérprete usa clases para representar las reglas.
  • Puede que necesites otros patrones para ampliar funcionalidades del interprete o facilitar su función: Composite (para manejar el árbol sintáctico), Flyweight (para dividir los símbolos en el árbol sintáctico), Iterator (para manejar independientemente los elementos) o Visitor (para mantener\ampliar el comportamiento de un árbol con una sola clase).

Iterator

Se basa en utilizar una interface que defina los métodos adecuados para realizar acciones sobre listas (recorrer secuencialmente y posicionarse en sus elementos (siguiente, anterior, primero, ultimo), añadirlos, eliminarlos…); todas las clases que correspondan a estas listas deberán implementar la interface, de esa forma todas las listas de objetos se tratarán igual aunque su estructura interna (pila, cola, hash list…) sea totalmente distinta.

  • Un método de la interface Iterator deberá retornar un objeto de este mismo tipo, la clase que lo implemente retornará un puntero a 'this' en ese método.
  • Podemos crear una clase adaptadora que contenga una lista e implemente la interface Iterator.

Mediator

Define una comunicación simplificada entre clases, para ello en un objeto encapsula la forma en la que otros interactúan.
Cuando en un escenario un gran número de clases interactúan entre sí, haciendo que unas cambien el estado de las otras, puede llegarse a una estructura muy compleja. Además, la reutilización y personalización de los objetos se complica debido a que existe una gran interdependencia entre ellos y es complejo separarlos del del grupo. Necesitamos un mediator!

UML

  • Mediator es una interface para comunicar objetos Colleage.
  • ConcreteMediator es el encargado de gestionar la comunicación entre objetos Colleage, implementa los métodos de Mediator.
  • Colleague es la interface que implementan los ConcreteColleague, esta ha de permitir a estos acceder a un Mediator. Se comunicarán gracias a este.

Existen varias formas de implementar un Mediator:

  • Podemos omitir la interface Mediator si todos los objetos van a comunicarse con un solo mediador.
  • Podemos implementar este patrón como un patrón Observer, ConcretMediator “observa” los cambios en los ConcreteColleague.
  • El objeto ConcretMediator puede realizar acciones sobre los objetos ConcreteColleague cuando uno de ellos hace una petición, acciones como cámbio de estado, actualización de valores…

Memento

Captura externamente el estado de un objeto para luego restaurarlo, y esto sin violar las reglas de encapsulación. Se almacenará el estado en una clase independiente a la del objeto a restaurar. Implementaciones alternativas:

  • Utilizar dos métodos “getState” y “restoreState”, el primero devolvería un objeto con el estado actual y el otro, pasándole este estado, lo restauraría.
  • En Java, utilizar la serialización.

UML

  • Memento almacena el estado del objeto Originator y protege el acceso contra otros objetos que no sean de este tipo.
  • Caretaker almacena mementos, no los examina ni opera con ellos, su único fin es devolver el memento adecuado al Originator adecuado.
  • Originator crea un memento con el estado actual. También es capaz de volver a un estado anterior a partir de un memento.

Observer

Provee una forma de que un objeto avise a otros que ha cambiado de estado, y así estos puedan actualizarse automáticamente y autónomamente, teniendo los dos tipos de objetos una relación nula (el que avisa no tiene por qué conocer a los receptores). El objeto que cambia de estado y avisa es el observado y los que reciben el aviso que ha cambiado los observadores.

  • Es un patrón muy adecuado para implementar eventos.
  • Evita bucles que revisan un objeto innecesaria y continuamente.


UML

  • Tenemos una interface Observer que hace que las clases que la implementen codifiquen el método Update; este método es el que se llama cuando se recibe un aviso de cambio de estado del observable.
  • Una clase abstracta Subject (o Observable) y una clase ConcreteSubject que hereda de la Subject. Subject deberá guardar una lista de los observadores (los que implementan Observer), de la misma forma que tendrá un método Notify que será llamado cuando el ConcreteSubject cambie y este, lo que hará, será recorrer todos los observadores y llamar a sus métodos Update. Deberá también implementar métodos que permitan añadir y eliminar observadores.
  • Los ConcreteObserver (objetos que observan al observable), heredarán de Observer y tendrán una instancia del objeto ConcreteSubject pero no será necesario que la examinen hasta que no se llame al método Update.
public abstract class Subject {
  LinkedList<Observer> listObservers;
  ...
  public void AddObserver (Observer) {...}
  public void DeleteObserver (Observer) {...}
  public Notify () {
    for (Observer o : listObservers)
      o.Update();
  }
}
 
public class Pelota extends Subject {
  Point point;
  ...
  void Mover (int x, int y) {
    point = new Point (x, y);
    this.Notify();
  }
}
 
public class Espectador implements Observer {
  Pelota p;
  ...
  void Update () {
    MirarPosicion(p.getPoint());
  } 
}

State

Cambia el comportamiento del objeto cuando su estado cambia. Para lograrlo crea un objeto por cada estado, codificando los métodos adecuados para cada estado concreto, el objeto que cambia de estado almacena una instancia a este tipo de objeto estado y actuará según le dicte este objeto. UML

  • La clase Context es la que cambia de estado. Contiene una instancia a ConcreteState definiendola del tipo State.
  • State es la interface de la que heredan los distintos estados ConcreteState.
  • Es necesario comprender que el cliente contendría la clase Context, que es la que internamente cambia de estado, como si esta cambiase dinámicamente de implementación.
public interface State {
  public void setVelocity (int vel);
  public int getGasto ();
}
 
public class stateStop implements State {
  private Car the_car;
  public stateStop (Car c) {
    this.the_car = c;
  }
  public void setVelocity (int vel) {
    if (vel > 0)
      the_car.setState(new stateRun());
  }
  public int getGasto () {
    return 0;
  }
}
 
public class stateRun implements State {
  private Car the_car;
  public stateRun (Car c) {
    this.the_car = c;
  }
  public void setVelocity (int vel) {
    if (vel <= 0)
      the_car.setState(new stateStop());
  }
  public int getGasto () {
    return 1;
  }
}
 
public class Car {
  State state;  
  int gasolina = 100;
  public Car () {
    state = new stateStop(this);
  }
  public void setState(State s) {
    this.state = s;
  }
  // A este método se llama una vez por minuto.
  public void gastar () {
    this.gasolina -= this.state.getGasto();
  }
}

En este ejemplo se muestra una de las dos posibles implementaciones para el cambio de estado, el estado es quien elige cual será el siguiente cambio de estado. Otra forma de realizar la transición entre estados podría ser la implementación en la misma clase Context.

Strategy

Se basa en la óptima utilización de métodos abstractos y de la implementación de la herencia de las clases que los contienen.
Una clase (A) con un método abstracto, el ejecutor de la estrategia, define varias estratégias según las clases (E1, E2, E3…) que heredan de esta e implementan dicho método. Otra clase (B) que quiera utilizar una de estas estrategias definirá un objeto del primer tipo (A) como uno nuevo de sus herederos (E1, E2, E3…) y llamará al método de la estrategia.

// E1, E2 y E3 heredan de A
A obj1 = new E1();
A obj2 = new E2();
A obj3 = new E3();
// A contiene el método abstracto strategy que E1, E2 y E3 han implementado
obj1.strategy();
obj2.strategy();
obj3.strategy();

Template Method

En una clase abstracta que contiene un método para realizar un algoritmo y este, a su vez, utiliza otros métodos abstractos de esa clase para realizar dicho algoritmo; en las clases que hereden de esta se podrán sobreescribir estos métodos (o deberán si se implementan como abstractos) para cambiar el algoritmo. UML
Los pasos para implementar el TemplateMethod son los métodos PrimitiveOperation1 y PrimitiveOperation2, el algoritmo cambiará una vez los cambiemos.

Visitor

Permite una nueva operación a una clase sin efectuar ningún cambio en esta.
Existe un objeto (ObjectStructure en el UML) que contiene una colección de elementos visitables (Element en el UML), a este se le pasa un objeto visitador (en el UML el visitador es un objeto ConcretVisitor que hereda de Visitor). El objeto al que le ha llegado el visitador recorre todos los elementos de la lista y se los pasa al visitador para que realice su visita (en el UML esta corresponde al método VisitConcretElement). Una visista consiste en la nueva operación que queremos implementar sobre los objetos visitables.
También puede ser implementado mediante un patrón composite, ya que para implementar este patrón añadiendo nueva funcionalidad a la estructura esta no ha de sufrir cambio alguno para poder realizar las visitas.

UML

// Interfaces
public interface Visitor {
     public void visit (Visitable v);
}
public interface Visitable {
     public void accept (Visitor visitador);
}
 
// Element
public abstract class GeometricRegularFigure implements Visitable {
  protected int nLados;
  protected int sizeLado;
  public void accept (Visitor visitador) {
     visitador.visit(this);
  }
  public int getNLados() {
     return nLados;
  }
  public int getSizeLado() {
     return sizeLado;
  }
}
 
// ConcreteElement1 (Visitable)
public class Square extends GeometricRegularFigure {
  public Square (int size) {
     this.nLados = 4;
     this.sizeLado = size;
  }
}
 
// ConcreteElement2 (Visitable)
public class Triangle extends GeometricRegularFigure{
  public Triangle (int size) {
     this.sizeLado = size;
     this.nLados = 3;
  }
}
 
// ObjectStructure
public class ContainerFigures {
  private ArrayList<GeometricRegularFigure> figures;
  public ContainerFigures() {
     this.figures = new ArrayList<GeometricRegularFigure>();
  }
  public void Add (GeometricRegularFigure f) {
     this.figures.add(f);
  }
  public void realizaVisitas (Visitor visitador) {
     for (GeometricRegularFigure grf : figures)
        visitador.visit(grf);
  }
}
 
// Visitador
public class PerimeterCalculator implements Visitor {
  private int perimeter;
  public PerimeterCalculator() {
     this.perimeter = 0;
  }
  public void visit(Visitable v) {
     this.perimeter += (((GeometricRegularFigure)v).getNLados() * ((GeometricRegularFigure)v).getSizeLado());
  }
  public int getCalculo () {
     return this.perimeter;
  }
}
 
// Cliente
ContainerFigures coleccion = new ContainerFigures();
coleccion.Add(new visitorATD.Triangle(5));
coleccion.Add(new visitorATD.Triangle(4));
coleccion.Add(new visitorATD.Square(15));
coleccion.Add(new visitorATD.Square(5));
 
PerimeterCalculator pc = new PerimeterCalculator();
coleccion.realizaVisitas(pc);

Documentos

code/dessignpatterns.txt · Última modificación: 2020/05/09 09:25 (editor externo)