miércoles, 4 de mayo de 2016

Patrones de Diseño de Comportamiento Parte 3

19. Observer: También conocido como el patrón "Publish-Subscribe" es un patrón de diseño de comportamiento, que define una relación de uno a muchos de tal manera que cuando un objeto cambia su estado, todos los objetos dependientes son notificados de la actualización automáticamente. Nos permite crear objetos observables que mantienen una lista de observadores y les notifica a éstos de cualquier cambio en su estado, normalmente llamando a uno de sus métodos. Está relacionado con algoritmos de funcionamiento y asignación de responsabilidades a clases y objetos. Esta patrón se conoce como el patrón  de publicación-inscripción o modelo-patrón. Estos nombres sugieren la idea básicas del patrón, que son: el obejto de datos, que se le puede llamar Sujeto a partir  de ahora, contiene atributos mediante los cuales cualquier objeto Observador o vista se puede subscribir a él pasándole una referencia a sí mismo. El sujeto mantiene así una lista de las referencias a sus observadores. Los observadores a su vez están obligados a implementar unos métodos determinados mediante los cuales el Sujeto es capaz de notificar a sus observadores suscritos los cambios que sufre para que todos ellos tengan la oportunidad  de refrescar el contenido representado. De manera que cuando se produce un cambio en el Sujeto ejecutado, por ejemplo, por alguno de los observadores, el objeto de datos puede recorrer la lista de observadores avisando a cada uno. Este patrón suele observarse en los frameworks de interfaces gráficas orientados a objetos, en los que la forma de capturar los eventos es suscribir listeners a los objetos que pueden disparar eventos. El patrón Observer es la clave del patrón de arquitectura Modelo Vista Controlador(MVC).

Representación UML

Ejemplo
Supongamos que tenemos una clase Producto con un precio y queremos emitir un mensaje en la terminal cuando un producto cambie de precio. Para implementar este patrón Producto debería extender de Observable y otra clase hacer que implemente la interfaz Observer, sin embargo, si no queremos o no podemos hacer que nuestra clase extienda de Observable para no limitarnos en nuestra jerarquía de clases o porque ya extiende de otra podemos usar composición, por otro lado, si no queremos registrar el observador en cada instancia de Producto sino observar cualquier instancia que se cree podemos implementar el Observer de forma estática en la clase Producto. El observable ProductoObservable amplia la visibilidad del método setChanged para poder hacer uso de él usando compocisión, deberemos invocarlo para que los observadores sean notificados. Vamos a ver el código del siguiente ejemplo:

package io.github.picodotdev.blogbitix.patronobserver;

import java.math.BigDecimal;
import java.util.Observable;

public class Producto {

    public class PrecioEvent {
      
        private Producto producto;
        private BigDecimal precioAntiguo;
        private BigDecimal precioNuevo;
      
        public PrecioEvent(Producto producto,  BigDecimal precioAntiguo, 
        BigDecimal precioNuevo) {
            this.producto = producto;
            this.precioAntiguo = precioAntiguo;
            this.precioNuevo = precioNuevo;
        }
      
        public Producto getProducto() {
            return producto;
        }
      
        public BigDecimal getPrecioAntiguo() {
            return precioAntiguo;
        }
      
        public BigDecimal getPrecioNuevo() {
            return precioNuevo;
        }      
    }

    private static final ProductoObservable OBSERVABLE;

    private String nombre;
    private BigDecimal precio;

    static {
        OBSERVABLE = new ProductoObservable();
    }

    public static Observable getObservable() {
        return OBSERVABLE;
    }

    public Producto(String nombre, BigDecimal precio) {
        this.nombre = nombre;
        this.precio = precio;
    }

    public String getNombre() {
        return nombre;
    }

    public BigDecimal getPrecio() {
        return precio;
    }

    public void setPrecio(BigDecimal precio) {
        PrecioEvent event = new PrecioEvent(this, this.precio, precio);

        this.precio = precio;

        synchronized (OBSERVABLE) {
            OBSERVABLE.setChanged();
            OBSERVABLE.notifyObservers(event);          
        }
    }
  
    private static class ProductoObservable extends Observable {
        @Override
        public synchronized void setChanged() {
            super.setChanged();
        }
    }
}

package io.github.picodotdev.blogbitix.patronobserver;

import java.util.Observable;
import java.util.Observer;

import io.github.picodotdev.blogbitix.patronobserver.Producto.PrecioEvent;

public class ProductoObserver implements Observer {

    @Override
    @SuppressWarnings("unchecked")
    public void update(Observable observable, Object args) {
        if (args instanceof PrecioEvent) {
            PrecioEvent evento = (PrecioEvent) args;
            System.out.printf("El producto %s ha cambiado de precio de %s a %s%n",
            evento.getProducto().getNombre(),
            evento.getPrecioAntiguo(), evento.getPrecioNuevo());
        }
    }
}
package io.github.picodotdev.blogbitix.patronobserver;

import java.math.BigDecimal;
import java.util.Observer;

public class Main {
    public static void main(String[] args) {
        Producto p1 = new Producto("Libro", new BigDecimal("3.99"));
        Producto p2 = new Producto("Lector libros electrónico", new BigDecimal("129"));

        Observer o1 = new ProductoObserver();
        Producto.getObservable().addObserver(o1);

        p1.setPrecio(new BigDecimal("4.99"));
        p2.setPrecio(new BigDecimal("119"));
    }
}

20. State: Permitir a un objeto modificar su modificamiento cuando su estado interno cambia. El objeto aparecerá para cambiar su clase. Las ventajas del patrón State son que localiza el comportamiento de un estado específico y divide el comportamiento para diferentes estado es decir el patrón State pone todo el comportamiento asociado con un estado particular en un objeto. Debido a que todo el código de un estado específico está en una subclase de State, los nuevos estados y transiciones pueden ser añadidos fácilmente añadiendo nuevas subclases. Hace las transiciones entre estados explícitas, es decir, cuando un objeto define su estado actual únicamente en términos de datos internos, sus transiciones no tienen una representación explícita. Introducir objetos separados para diferentes estados hace las transiciones más explícitas. Los objetos de los estados pueden ser compartidos, si los objetos de los estados no tienen variables instanciadas, el estado que representan está codificado completamente en su tipo, entonces el contexto puede compartir el objeto del estado. La clase Contexto define la interfaz de interés para los clientes. La clase State define una interfaz para encapsular el comportamiento asociado con un estado particular del contexto. Cada Clase ConcreteState implementa un comportamiento asociado con un estado del contexto.

Representación UML

 Ejemplo

Pero para comprenderlo mejor veamos un sencillo ejemplo  en el que controlamos el estado de un semáforo:

package State;

public class Main
{
    public static void main(String[] args)
    {
         Semaforo objSemaforo = new Semaforo();
        // Muestra el aviso por defecto (verde, no hay alerta)
        objSemaforo.mostrar();
         objSemaforo.setEstado( new EstadoNaranja() );
         objSemaforo.mostrar();
        objSemaforo.setEstado( new EstadoRojo() );
        objSemaforo.mostrar();
    }
}

Semaforo.java (Contexto según el diagrama anterior):

package State;
public class Semaforo
{
    private EstadoSemaforo objEstadoSemaforo;
    // -------------------------------------------
    public Semaforo() {
         this.objEstadoSemaforo = new EstadoVerde();
    }
    // -------------------------------------------
     public void setEstado( EstadoSemaforo objEstadoSemaforo ) {
         this.objEstadoSemaforo = objEstadoSemaforo;
    }
    // -------------------------------------------
     @Override
     public void mostrar() {
        this.objEstadoSemaforo.mostrar();
    }
}

EstadoSemaforo.java (Estado según el diagrama anterior):

Package State;
public abstract class EstadoSemaforo
{
    // Método que deberán crear las clases que hereden de ésta
    public abstract void mostrar();
}

EstadoVerde.java (un EstadoConcreto según el diagrama anterior):

package State;
 public class EstadoVerde extends EstadoSemaforo
{
    public EstadoVerde() {
    }
    // -------------------------------------------
    @Override
     public void mostrar() {
        System.out.println("Luz verde");
    }
}

 EstadoNaranja.java (un EstadoConcreto según  el diagrama anterior):

package State;
 public class EstadoNaranja extends EstadoSemaforo
{
    public EstadoNaranja() {
    }
    // -------------------------------------------
    @Override
     public void mostrar() {
        System.out.println("Luz naranja");
    }
}

 EstadoRojo.java(un EstadoConcreto según el diagrama anterior):

package State;
 public class EstadoRojo extends EstadoSemaforo
{
    public EstadoRojo() {
    }
    // -------------------------------------------
    @Override
     public void mostrar() {
        System.out.println("Luz roja");
    }
}

21. Strategy: Este patrón permite  que el algoritmo a ejecutarse se seleccione en tiempo de ejecución. Este algoritmo proporciona una familia de algoritmos, encapsula cada uno dentro de un objeto y los hace intercambiables. Esto permite que el algoritmo a ejecutarse varíe en función del cliente que lo use. En primer lugar, es mucho más fácil comprender cada uno de los distintos comportamientos si su implementación está encapsulada en distintas clases, y no entrelazada en un único método. Esto permite de forma simple añadir nuevos comportamientos, y eliminar  o modificar los existentes. En los casos en que existan varios objetos cuyo comportamiento sea practicamente el mismo, esto se puede reducir a una única clase que haga uso de distintas estrategias. Esto reduce el uso de subclases y, por tanto, el acoplamiento entre ellas. Las posibles estrategias se ejecutan dentro de un objeto de contexto que se encarga de recuperar la estrategia apropiada para el cliente. Cada una de las estrategias implementa una interfaz que define la firma del método de la estrategia.

Representación UML

Ejemplo: 

Pese a que el ejemplo de nuestro videojuego es bastante ilustrativo, dado que en esta serie de artículos tenemos fijación por los coches, implementateremos el patrón a través de un hipotético módulo de la centralita del vehículo que nos permitirá alternar entre una conducción normal y deportiva. La diferencia entre ambas será simple: mayor consumo, mayor potencia, y mayor velocidad. Podríamos añadir más comportamiento "personalizados", como el endurecimiento de la suspensión, pero con estos dos elementos será suficiente para captar la idea.

public interface ITipoConduccion
{
    string ObtenerDescripcion();
    int ObtenerPotencia(float decilitrosCombustible);
    int ObtenerIncrementoVelocidad(float decilitrosCombustible);
}
 A continuación añadiremos las estrategias en sí, es decir, las clases que implementan la interfaz y dotan de distintos comportamientos que serán seleccionados por el contexto. Habíamos dicho que utilizaríamos dos: conducción normal y conducción deportiva. Por lo tanto, crearemos dos clases que proporcionen distintos comportamientos para los mismos métodos:

public class ConduccionNormal : ITipoConduccion
{
    public string ObtenerDescripcion()
    {
        return "Conduccion Normal";
    }

    public int ObtenerPotencia(float decilitrosCombustible)
    {
        return (int)(decilitrosCombustible * 0.842) + 3;
    }

    public int ObtenerIncrementoVelocidad(float decilitrosCombustible)
    {
        return (int)(decilitrosCombustible * 0.422) + 2;
    }
}
public class ConduccionDeportiva : ITipoConduccion
{
    public string ObtenerDescripcion()
    {
        return "Conduccion Deportiva";
    }

    public int ObtenerPotencia(float decilitrosCombustible)
    {
        return (int)(decilitrosCombustible * 0.987) + 5;
    }

    public int ObtenerIncrementoVelocidad(float decilitrosCombustible)
    {
        return (int)(decilitrosCombustible * 0.618) + 3;
    }
}

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 estrategía, 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 e sus métodos o la encapsulación del cambio de estrategia.

public class Contexto
{
    // Referencia a la interfaz
    private ITipoConduccion tipoConduccion;

    // Propiedad que establecerá un nuevo tipo de conducción (cambio de estrategia)
    public ITipoConduccion TipoConduccion
    {
        get { return tipoConduccion; }
        set { this.tipoConduccion = value; }
    }

    // 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);
    }
}

En realidad, la propia clase el cliente puede actuar como clase de contexto, pero siempre será mejor minimizar el acoplamiento entre las estrategias y las reglas de negocio. De este modo, respetaremos otro de los principios de la orientación a objetos: una clase, una responsabilidad. Para comprobar el funcionamiento de nuestro cliente, bastará con utilizar el siguiente código que hará uso del contexto para cambiar de estrategia en tiempo de ejecución.

public class Vehiculo
{
    private Contexto contexto;

    public Vehiculo()
    {
        contexto = new Contexto();
    }

    public void ConduccionDeportiva()
    {
        ITipoConduccion conduccionDeportiva = new ConduccionDeportiva();
        contexto.TipoConduccion = conduccionDeportiva;
    }

    public void ConduccionNormal()
    {
        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);
    }
}

Finalmente, el código que invoca a nuestro cliente, que será el siguiente:

public class MainClass{
 public static void Main(string[] args)
  {
    Vehiculo v = new Vehiculo();
    v.ConduccionDeportiva();
    v.Acelerar(2.4f);

    Console.WriteLine("");

    v.ConduccionNormal();
    v.Acelerar(2.4f);

    Console.ReadLine();
  }
}

No hay comentarios:

Publicar un comentario