Patrones de diseño de comportamiento en Java

P

Visión general

Este es el tercer artículo de una serie corta dedicada a los patrones de diseño en Java, y una continuación directa del artículo anterior: Patrones de diseño estructural en Java.

Patrones de comportamiento

Los patrones de comportamiento se preocupan por proporcionar soluciones con respecto a la interacción de objetos: cómo se comunican, cómo algunos dependen de otros y cómo segregarlos para que sean tanto dependientes como independientes y proporcionen flexibilidad y capacidades de prueba.

Los patrones de comportamiento en Java que se tratan en este artículo son:

  • Interprete
  • Método / patrón de plantilla
  • Cadena de responsabilidad
  • Mando
  • Iterador
  • Mediador
  • Recuerdo
  • Observador
  • Estado
  • Estrategia
  • Visitante

Interprete

El patrón de intérprete se utiliza en cualquier momento que necesitemos para evaluar cualquier tipo de gramática o expresiones del lenguaje. Un buen ejemplo de este patrón sería Google Translate , que interpreta la entrada y nos muestra la salida en otro idioma.

Otro ejemplo sería el compilador de Java. El compilador interpreta el código Java y lo traduce al código de bytes que utiliza la JVM para realizar operaciones en el dispositivo en el que se ejecuta.

Este patrón también representa una excelente manera de escribir programas simples que comprendan la sintaxis similar a la humana.

Implementación

Haremos una implementación simple con gramática simple, de lo contrario, se volvería complicado y demasiado complejo por el bien de este tutorial.

Para realizar este patrón de diseño, tendremos que definir un motor de interpretación, acompañado de diferentes expresiones que utilizará para interpretar el comando.

Definamos una interfaz para todas estas expresiones:

public interface Expression {
    public int interpret(InterpreterEngine engine);
}

Este motor de interpretación es simple:

public class InterpreterEngine {
    public int add(String input) {
        String[] tokens = interpret(input);
        int num1 = Integer.parseInt(tokens[0]);
        int num2 = Integer.parseInt(tokens[1]);
        return (num1+num2);
    }
    
    public int multiply(String input) {
        String[] tokens = interpret(input);
        int num1 = Integer.parseInt(tokens[0]);
        int num2 = Integer.parseInt(tokens[1]);
        return (num1*num2);
    }
     
    private String[] interpret(String input) {
        String string = input.replaceAll("[^0-9]", " ");
        string = string.replaceAll("( )+", " ").trim();
        String[] tokens = string.split(" ");
        return tokens;
    }
}

Reemplaza todos los caracteres que no son dígitos con caracteres vacíos y divide la entrada en tokens. Esto básicamente nos deja sin dígitos.

Ahora, implementemos la Expressioninterfaz con algunas clases concretas:

public class AddExpression implements Expression {
    private String expression;
    
    public AddExpression(String expression) {
        this.expression = expression;
    }
    
    @Override
    public int interpret(InterpreterEngine engine) {
        return engine.add(expression);
    }
}

public class MultiplyExpression implements Expression {
    private String expression;
    
    public MultiplyExpression(String expression) {
        this.expression = expression;
    }

    @Override
    public int interpret(InterpreterEngine engine) {
        return engine.multiply(expression);
    }
}

Y para ilustrar el punto del patrón:

public class Main {
    private InterpreterEngine engine;
    
    public Main(InterpreterEngine engine) {
        this.engine = engine;
    }
    
    public int interpret(String input) {
        Expression expression = null;
        
        if(input.contains("add")) {
            expression = new AddExpression(input);
        } else if(input.contains("multiply")) {
            expression = new MultiplyExpression(input);
        }
        
        int result = expression.interpret(engine);
        System.out.println(input);
        return result;
    }
    
    public static void main(String[] args) {
        Main main = new Main(new InterpreterEngine());
        
        System.out.println("Result: " + main .interpret("add 15 and 25"));
        System.out.println("Result: " + main .interpret("multiply " + main .interpret("add 5 and 5") + " and 10"));
    }
}

Dado que descartamos todos los caracteres que no son dígitos, este es el lugar para evaluar si el intérprete debe sumar o multiplicar la entrada.

Ejecutar este fragmento de código producirá:

add 15 and 25
Result: 40
add 5 and 5
multiply 10 and 10
Result: 100

Método de plantilla

El método de plantilla, también conocido como patrón de plantilla, está a nuestro alrededor. Se reduce a definir una clase abstracta que proporcione formas predefinidas de ejecutar sus métodos. Las subclases que heredan estos métodos también deben seguir la forma definida en la clase abstracta.

En algunos casos, la clase abstracta puede incluir ya una implementación de método, no solo instrucciones, si es una funcionalidad que se compartirá entre todas o la mayoría de las subclases.

Implementación

En una empresa, todos los empleados tienen algunas responsabilidades compartidas:

public abstract class Employee {
    abstract void work();
    abstract void takePause();
    abstract void getPaid();
    
    public final void comeToWork() {
        work();
        takePause();
        work();
        getPaid();
    }
}

Todos vienen a trabajar, todos descansan y se les paga.

Diferentes empleados realizan diferentes tipos de trabajo:

public class Programmer extends Employee {

    @Override
    void work() {
        System.out.println("Writing code.");
    }

    @Override
    void takePause() {
        System.out.println("Taking a small break from writing code.");
    }

    @Override
    void getPaid() {
        System.out.println("Getting paid for developing the project.");
    }
}
public class Manager extends Employee {

    @Override
    void work() {
        System.out.println("Managing other employees.");
    }

    @Override
    void takePause() {
        System.out.println("Taking a small break from managing employees.");
    }

    @Override
    void getPaid() {
        System.out.println("Getting paid for overseeing the development of the project.");
    }
}

Pero todavía siguen la plantilla de trabajar, hacer una pausa y cobrar, todo lo cual está establecido por la interfaz.

Para ilustrar el punto de este patrón:

public class Main {
    public static void main(String[] args) {
        Employee employee = new Programmer();
        employee.comeToWork();
     
        System.out.println();
        
        employee = new Manager();
        employee.comeToWork();
    }
}

Ejecutar este fragmento de código producirá:

Writing code.
Taking a small break from writing code.
Writing code.
Getting paid for developing the project.

Managing other employees.
Taking a small break from managing employees.
Managing other employees.
Getting paid for overseeing the development of the project.

Cadena de responsabilidad

El patrón de Cadena de Responsabilidad es ampliamente utilizado y adoptado. Define una cadena de objetos que, en conjunto, uno tras otro, procesan la solicitud, donde cada procesador de la cadena tiene su propia lógica de procesamiento.

Cada una de estas unidades de procesamiento decide quién debe continuar procesando la solicitud a continuación, y cada una tiene una referencia a la siguiente en la fila.

Es importante tener en cuenta que es muy útil para desvincular el remitente del receptor.

Implementación

Como de costumbre, definamos una clase abstracta:

public abstract class Employee {
    public static int PROGRAMER = 1;
    public static int LEAD_PROGRAMER = 2;
    public static int MANAGER = 3;
    
    protected int authorityLevel;
    
    protected Employee nextEmployee;
    
    public void setNextEmployee(Employee employee) {
        this.nextEmployee = employee;
    }
    
    public void doWork(int authorityLevel, String message) {
        if(this.authorityLevel <= authorityLevel) {
            write(message);
        }
        if(nextEmployee != null) {
            nextEmployee.doWork(authorityLevel, message);
        }
    }
    
    abstract protected void write(String message);
}

Esta clase abstracta contiene niveles de autoridad para todos los empleados. Un programador está ubicado menos en la jerarquía que un programador líder, que a su vez es más bajo que un gerente.

También hemos incluido una referencia al próximo empleado, por lo que pronto verá por qué es importante.

Se define un método común para todas estas clases, con una verificación de autoridad. Si una determinada clase no tiene la autoridad, pasa la solicitud a la siguiente en la cadena de responsabilidad.

Ahora, ampliemos esta clase:

public class Programmer extends Employee {
    
    public Programmer(int authorityLevel) {
        this.authorityLevel = authorityLevel;
    }

    @Override
    protected void write(String message) {
        System.out.println("Programmer is working on project: " + message);
    }
}

public class LeadProgrammer extends Employee {
    
    public LeadProgrammer(int authorityLevel) {
        this.authorityLevel = authorityLevel;
    }

    @Override
    protected void write(String message) {
         System.out.println("Lead programmer is working on project: " + message);
    }
}

public class Manager extends Employee {
    
    public Manager(int authorityLevel) {
        this.authorityLevel = authorityLevel;
    }

    @Override
    protected void write(String message) {
         System.out.println("Manager is working on project: " + message);
    }
}

Como se mencionó anteriormente, cada una de estas unidades proporciona su propia lógica de procesamiento.

Para ilustrar el punto de este patrón:

public class Main {
    private static Employee getChainOfEmployees() {
        Employee programmer = new Programmer(Employee.PROGRAMER);
        Employee leadProgrammer = new LeadProgrammer(Employee.LEAD_PROGRAMER);
        Employee manager = new Manager(Employee.MANAGER);
        
        programmer.setNextEmployee(leadProgrammer);
        leadProgrammer.setNextEmployee(manager);

        return programmer;
    }

    public static void main(String[] args) {
        Employee employeeChain = getChainOfEmployees();
        
        employeeChain.doWork(Employee.PROGRAMER, "This is basic programming work.");
        employeeChain.doWork(Employee.LEAD_PROGRAMER, "This is marginally more 
            sophisticated programming work.");
        employeeChain.doWork(Employee.MANAGER, "This is the work for a manager.");
    }
}

En primer lugar, getChainOfEmployees()se define un método estático . Este método se utiliza para establecer los niveles de autoridad de cada unidad, a través de sus constructores, y para definir el orden de responsabilidad.

Al establecer el siguiente Employeepara Programmer, básicamente le decimos a quién acudir, si la solicitud está fuera de su alcance.

Naturalmente, un programador recurrirá a su designado LeadProgrammer. Si la solicitud es demasiado para ellos, acudirán a ellos en Managerbusca de ayuda.

Ejecutar este fragmento de código producirá:

Programmer is working on project: This is basic programming work.
Programmer is working on project: This is marginally more sophisticated programming work.
Lead programmer is working on project: This is marginally more sophisticated programming work.
Programmer is working on project: This is the work for a manager.
Lead programmer is working on project: This is the work for a manager.
Manager is working on project: This is the work for a manager.

A Programmerestá asignado para trabajar en una solicitud en su propio nivel de autoridad, y lo hacen con elegancia.

Luego, entra una nueva solicitud, que necesita la autoridad de a LeadProgrammer, por lo que se hace cargo.

Finalmente, llega otra solicitud, que necesita la autoridad de a Manager. El programador pide ayuda a su programador líder designado, que a su vez decide pedir ayuda a su gerente, y el gerente cumple felizmente y hace el trabajo.

Mando

Otro patrón de diseño de desacoplamiento, el patrón Command funciona envolviendo la solicitud del remitente en un objeto llamado comando. Este comando se pasa luego al objeto invocador, que procede a buscar la forma adecuada de procesar la solicitud.

Una vez que encuentra el camino adecuado, pasa el comando, donde se ejecutará.

Implementación

Simulemos el trabajo de un programador para este patrón. Un cliente puede enviar un Order– un comando, para una Application– una solicitud. El programador puede entonces crear la aplicación y venderla al cliente.

Hagamos nuestro comando:

public interface Order {
    void placeOrder();
}

Y nuestra solicitud:

public class Application {
    private String name = "Computer Application";
    private int quantity = 2;
    
    public void make() {
        System.out.println(quantity + " application(s) are made for the client.");
    }
    
    public void sell() {
        System.out.println(quantity + "application(s) are sold to the client.");
    }
}

Suponiendo que el programador aceptó trabajar con el cliente, sería apropiado hacer la aplicación:

public class MakeApplication implements Order {
    private Application application;
    
    public MakeApplication(Application application) {
        this.application = application;
    }
    
    @Override
    public void placeOrder() {
        application.make();
    }
}

Y luego de hacerlo, el programador procederá a venderlo:

public class SellApplication implements Order {
    private Application application;
    
    public SellApplication(Application application) {
        this.application = application;
    }

    @Override
    public void placeOrder() {
        application.sell();
    }
}

Se necesita un objeto invocador, al que enviamos la solicitud:

public class Programmer {
    private List<Order> orderList = new ArrayList<>();
    
    public void takeOrder(Order order) {
        orderList.add(order);
    }
    
    public void placeOrders() {
        for(Order order : orderList) {
            order.placeOrder();
        }
        orderList.clear();
    }
}

La solicitud, aunque es un, Applicationestá envuelto como un Ordercomando, como se describe antes de la implementación.

Y para ilustrar el punto de este patrón:

public class Main {
    public static void main(String[] args) {
        // command
        Application application = new Application();
        
        / /wrapping requests
        MakeApplication makeApplication = new MakeApplication(application);
        SellApplication sellApplication = new SellApplication(application);

        // invoker
        Programmer programmer = new Programmer();
        programmer.takeOrder(makeApplication);
        programmer.takeOrder(sellApplication);

        // invoker processed the wrapped request
        programmer.placeOrders();
    }
}

Ejecutar este fragmento de código producirá:

2 application(s) are made for the client.
2 application(s) are sold to the client.

Iterador

El patrón Iterator se utiliza como patrón central del marco de colección de Java . Se usa para acceder a los miembros de las colecciones mientras oculta la implementación subyacente.

Implementación

Esta es una implementación bastante simple y se utiliza como patrón central en múltiples marcos, incluido el marco mencionado anteriormente.

Haremos un iterador simple para imprimir los nombres de nuestros empleados.

Todos nuestros empleados tienen su propio sector en el que operan. Entonces, trabajar bajo un sector también incluye un iterador para todos ellos.

Así que sigamos adelante y definamos nuestro Iterator:

public interface Iterator {
    public boolean hasNext();
    public Object next();
}

Este iterador se almacenará en una especie de contenedor. En nuestro caso, una obra Sector:

public interface Sector {
    public Iterator getIterator();
}

Ahora, definamos un repositorio para nuestros empleados:

public class EmployeeRepository implements Sector {
    public String[] employees = {"David", "Scott", "Rhett", "Andrew", "Jessica"};

    @Override
    public Iterator getIterator() {
        return new EmployeeIterator();
    }
    
    private class EmployeeIterator implements Iterator {
        int index;
        
        @Override
        public boolean hasNext() {
            if(index < employees.length) {
                return true;
            } 
            return false;
        }

        @Override
        public Object next() {
            if(this.hasNext()) {
                return employees[index++];
            }
            return null;
        }
    }
}

En aras de la simplicidad, solo hemos utilizado una matriz de cadenas y evitamos definir una Employeeclase separada .

Para ilustrar el punto de este patrón:

public class Main {
    public static void main(String[] args) {

        EmployeeRepository employeeRepository = new EmployeeRepository();

        for(Iterator iterator = employeeRepository.getIterator(); 
                iterator.hasNext();) {
            String employee = (String)iterator.next();
            System.out.println("Employee: " + employee);
        }
    }
}

Ejecutar este fragmento de código producirá:

Employee: David
Employee: Scott
Employee: Rhett
Employee: Andrew
Employee: Jessica

Mediador

Similar al patrón Adaptador, pero con un objetivo diferente. El patrón Mediator actúa como un puente y, como su nombre lo indica, el mediador entre diferentes objetos que se comunican de cualquier forma. En aplicaciones a gran escala, la comunicación directa significa un acoplamiento estrecho que dificulta la prueba, el mantenimiento y la escala.

El patrón Mediador aborda este problema actuando como un tercero sobre el que se realiza la comunicación, desacoplando a los mismos en el proceso.

Implementación

Esta es una implementación bastante simple, y probablemente la más notoria es una conversación entre dos personas.

Un Userobjeto desea comunicarse con otro, por lo que utilizan una plataforma de mediador entre ellos para hacerlo: a Chat:

public class Chat {
    public static void showMessage(User user, String message) {
        System.out.println(new Date().toString() + "[" + user.getName() + "]: " + message);
    }
}

Esta clase contiene solo un método y, aceptando un Usery un String, formatea los parámetros y muestra el mensaje.

public class User {
    private String name;

    public User(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }   

    public void sendMessage(String message) {
        Chat.showMessage(this, message);
    }
}

Nuestra Userclase define un sendMessage()método. Este método llama al staticmétodo de la Chatclase con thisinstancia del usuario y a Stringcomo argumentos.

Para ilustrar el punto de este patrón:

public class Main {
    public static void main(String[] args) {
        User david = new User("David");
        User scott = new User("Scott");
        
        david.sendMessage("Hi Scott! How are you?");
        scott.sendMessage("I'm great! Thanks for asking. How are you?");
    }
}

Estos dos objetos no se comunican directamente. Ninguno de ellos apunta a ninguna variable de referencia u otro objeto, sin embargo, la Chatclase actúa como mediador y los conecta.

Ejecutar este fragmento de código producirá:

Fri Aug 31 14:14:19 CEST 2018[David]: Hi Scott! How are you?
Fri Aug 31 14:14:19 CEST 2018[Scott]: I'm great! Thanks for asking. How are you?

Recuerdo

El patrón Memento se ocupa de los estados anteriores del objeto. Esto significa que el patrón se usa cuando queremos guardar algún estado de un objeto, en el caso de que queramos restaurar el objeto a ese estado más adelante.

Implementación

Este patrón se basa en el trabajo de tres clases, también conocidas como clases de actores. El Mementoobjeto contiene un estado que deseamos guardar para su uso posterior. El Originatorobjeto crea y almacena estados en los Mementoobjetos, mientras que el CareTakerobjeto se encarga del proceso de restauración.

Primero definamos nuestro recuerdo:

public class Memento {
    private String state;
    
    public Memento(String state) {
        this.state = state;
    }
    
    public String getState() {
        return state;
    }
}

Entonces nuestro creador y cuidador:

public class Originator {
    private String state;
    
    public void setState(String state) {
        this.state = state;
    }
    
    public String getState() {
        return state;
    }
    
    public Memento saveStateToMemento() {
        return new Memento(state);
    }
    
    public void getStateFromMemento(Memento memento) {
        state = memento.getState();
    }
}
public class CareTaker {
    private List<Memento> mementoList = new ArrayList<>();
    
    public void add(Memento memento) {
        mementoList.add(memento);
    }
    public Memento get(int index) {
        return mementoList.get(index);
    }
}

Y para ilustrar el punto del patrón:

public class Main {
    public static void main(String[] args) {
        Originator originator = new Originator();
        CareTaker careTaker = new CareTaker();
        
        originator.setState("State 1 at " + System.currentTimeMillis());
        originator.setState("State 2 at " + System.currentTimeMillis());
        careTaker.add(originator.saveStateToMemento());
        
        originator.setState("State 3 at " + System.currentTimeMillis());
        careTaker.add(originator.saveStateToMemento());
        
        System.out.println("Current state: " + originator.getState());
        
        originator.getStateFromMemento(careTaker.get(0));
        System.out.println("First saved state: " + originator.getState());
        originator.getStateFromMemento(careTaker.get(1));
        System.out.println("Second saved state: " + originator.getState());
    }
}

Ejecutar este fragmento de código producirá:

Current state: State 3 at 1535705131218
First saved state: State 2 at 1535705131218
Second saved state: State 3 at 1535705131218

Observador

El patrón Observer se usa para monitorear el estado de un determinado objeto, a menudo en un grupo o en una relación de uno a muchos. En tales casos, la mayoría de las veces, el estado cambiado de un solo objeto puede afectar el estado del resto, por lo que debe haber un sistema para notar el cambio y alertar a los otros objetos.

Si bien Java proporciona una clase y una interfaz con este patrón en mente, no está muy extendido porque no se realizó de una manera ideal.

Implementación

Para ilustrar este patrón, vamos a construir una pequeña oficina con una CEO, Manager, LeadProgrammery Programmer.

El programador será observado por sus superiores, los cuales tienen una opinión de él en función de lo bien que hace su trabajo:

public class Programmer {
    private List<Observer> observers = new ArrayList<>();
    private String state;
    
    public String getState() {
        return state;
    }
    
    public void setState(String state) {
        this.state = state;
        notifyObservers();
    }
    
    public void attach(Observer observer) {
        observers.add(observer);
    }
    
    public void notifyObservers() {
        for (Observer observer : observers) {
            observer.update();
        }
    }
}

Existe una relación de uno a muchos con sus observadores, y cada cambio de estado les notifica a todos.

Todos estos observadores tienen un par de cosas en común:

public abstract class Observer {
    protected Programmer programmer;
    public abstract void update();
}

Pero cada uno tiene su propia implementación:

public class CEO extends Observer {

    public CEO(Programmer programmer) {
        this.programmer = programmer;
        this.programmer.attach(this);
    }
    
    @Override
    public void update() {
        if(this.programmer.getState().equalsIgnoreCase("Successful")) {
            System.out.println("CEO is happy with Manager and Lead Programmer.");
        } else {
            System.out.println("CEO is unhappy with Manager and Lead Programmer.");
        }
    }
}

public class Manager extends Observer {
    
    public Manager(Programmer programmer) {
        this.programmer = programmer;
        this.programmer.attach(this);
    }
    
    @Override
    public void update() {
        if(this.programmer.getState().equalsIgnoreCase("Successful")) {
            System.out.println("Manager is happy with Lead Programmer and this Programmer.");
        } else {
            System.out.println("Manager is unhappy with Lead Programmer and this Programmer.");
        }
    }
}

public class LeadProgrammer extends Observer {

    public LeadProgrammer(Programmer programmer) {
        this.programmer = programmer;
        this.programmer.attach(this);
    }
    
     @Override
    public void update() {
        if(this.programmer.getState().equalsIgnoreCase("Successful")) {
            System.out.println("Lead Programmer is proud of his Programmer.");
        } else {
            System.out.println("Lead Programmer is not proud of his Programmer.");
        }
    }
}

El CEOno se preocupa por el programador, sino por el resultado, dejándolo en las manos capaces del Managery del LeadProgrammer. Al gerente le preocupa principalmente si el programador líder puede guiar al programador para que haga su trabajo. Y finalmente, al programador principal le preocupa principalmente lo que hace el programador.

Para ilustrar el punto de este patrón:

public class Main {
    public static void main(String[] args) {
        Programmer programmer = new Programmer();
        
        new CEO(programmer);
        new Manager(programmer);
        new LeadProgrammer(programmer);
        
        System.out.println("Programmer successfully did his job!");
        programmer.setState("Successful");
        System.out.println("Programmer failed his new assignment.");
        programmer.setState("Failed");
    }
}

Ejecutar este fragmento de código producirá:

Programmer successfully did his job!
CEO is happy with Manager and Lead Programmer.
Manager is happy with Lead Programmer and this Programmer.
Lead Programmer is proud of his Programmer.
Programmer failed his new assignment.
CEO is unhappy with Manager and Lead Programmer.
Manager is unhappy with Lead Programmer and this Programmer.
Lead Programmer is not proud of his Programmer.

Estado

El patrón de estado se utiliza cuando un objeto específico necesita cambiar su comportamiento, en función de su estado. Esto se logra proporcionando a cada uno de estos objetos uno o más objetos de estado.

Basándonos en estos objetos de estado, podemos cambiar completamente el comportamiento del objeto en cuestión.

Implementación

Definamos una interfaz simple:

public interface State {
    public void doAction(Context context);
}

Este estado se llevará a cabo a través de un contexto:

public class Context {
    private State state;
    
    public Context() {
        state = null;
    }
    
    public void setState(State state) {
        this.state = state;
    }
    
    public State getState() {
        return state;
    }
}

Y dos clases concretas lo implementan:

public class ApplicationStart implements State {

    @Override
    public void doAction(Context context) {
        System.out.println("The application is in the starting state of development.");
        context.setState(this);
    }
    public String toString() {
        return "Starting state.";
    }
}

public class ApplicationFinish implements State {

    @Override
    public void doAction(Context context) {
        System.out.println("The application is in the finished state of development.");
        context.setState(this);
    }
    public String toString() {
        return "Finished state.";
    }    
}

Ahora para ilustrar el punto de este patrón:

public class Main {
    public static void main(String[] args) {
        Context context = new Context();
        
        ApplicationStart start = new ApplicationStart();
        start.doAction(context);
        
        System.out.println(context.getState());
        
        ApplicationFinish finish = new ApplicationFinish();
        finish.doAction(context);
        
        System.out.println(context.getState());
    }
}

Ejecutar este fragmento de código producirá:

The application is in the starting state of development.
Starting state.
The application is in the finished state of development.
Finished state.

Como puede ver, el estado cambia el comportamiento del transportista.

Estrategia

El patrón de estrategia se emplea en situaciones en las que los algoritmos o el comportamiento de las clases deben ser dinámicos. Esto significa que tanto el comportamiento como los algoritmos se pueden cambiar en tiempo de ejecución, en función de la entrada del cliente.

Similar al patrón de estado, este patrón emplea múltiples objetos de estrategia que definen diferentes estrategias para la clase objetivo. La clase objetivo adapta sus algoritmos y comportamientos en función de estas estrategias.

Implementación

Empecemos por definir una estrategia:

public interface Strategy {
    public String build(String location);
}

Esta estrategia se utilizará para construir diferentes tipos de edificios, en diferentes ubicaciones. Estos tipos de edificios implementan la estrategia cada uno de una manera diferente:

public class Skyscraper implements Strategy {

    @Override
    public String build(String location) {
        return "Building a skyscraper in the " + location + " area.";
    }
}

public class House implements Strategy {

    @Override
    public String build(String location) {
        return "Building a house in the " + location + " area.";
    }
}

public class Mall implements Strategy {

    @Override
    public String build(String location) {
        return "Building a mall in the " + location + " area.";
    }
}

Similar al patrón de estado, una Contextclase usará la estrategia:

public class BuildContext {
    private Strategy strategy;
    
    public BuildContext(Strategy strategy) {
        this.strategy = strategy;
    }
    
    public String executeStrategy(String location) {
        return strategy.build(location);
    }
}

Y para ilustrar el punto de este patrón:

public class Main {
    public static void main(String[] args) {
        BuildContext buildContext = new BuildContext(new Skyscraper());
        System.out.println("Requesting a skyscraper: " + buildContext.executeStrategy("Downtown"));
        
        buildContext = new BuildContext(new House());
        System.out.println("Requesting a house: " + buildContext.executeStrategy("Outskirts"));
        
        buildContext = new BuildContext(new Mall());
        System.out.println("Requesting a mall: " + buildContext.executeStrategy("City Centre"));
    }
}

Ejecutar este fragmento de código producirá:

Requesting a skyscrapper: Building a skyscrapper in the Downtown area.
Requesting a house: Building a house in the Outskirts area.
Requesting a mall: Building a mall in the City Centre area.

Visitante

El patrón de visitante se utiliza para mover la lógica operativa de cada elemento individual de un grupo a una nueva clase, que realiza la operación por ellos utilizando los datos de cada elemento individual.

Esto se hace haciendo que todos los elementos acepten un “visitante”. Este visitante realizará cambios en una clase separada, sin cambiar la estructura de la clase visitada en absoluto. Esto facilita la adición de nuevas funciones sin cambiar las clases visitadas en absoluto.

Dicho esto, los objetos no tienen que ser iguales y pueden no estar relacionados, implementando diferentes interfaces, etc. Un ejemplo sería una aplicación que cuenta el número de usuarios en un sitio web. Algunos de estos usuarios son administradores, algunos son clientes, algunos son moderadores, etc.

Aunque pueden implementar diferentes interfaces y realizar diferentes funciones, este patrón puede ayudar a adquirir la cantidad correcta de usuarios.

Implementación

Cada artículo de nuestra tienda podrá aceptar un visitante:

public interface Item {
    public int accept(Visitor visitor);
}

Y aquí está nuestro visitante:

public interface Visitor {
    int visit(Pen pen);
    int visit(Notebook notebook);
}

Definamos clases concretas para los artículos de nuestra tienda:

public class Pen implements Item {
    private int price;
    private String model;
    
    public Pen(int price, String model) {
        this.price = price;
        this.model = model;
    }
    
    public int getPrice() {
        return price;
    }
    
    public String getModel() {
        return model;
    }

    @Override
    public int accept(Visitor visitor) {
        return visitor.visit(this);
    }
}
public class Notebook implements Item {
    private int price;
    private int numberOfPages;
    
    public Notebook(int price, int numberOfPages) {
        this.price = price;
        this.numberOfPages = numberOfPages;
    }
    
    public int getPrice() {
        return price;
    }
    
    public int getNumberOfPages() {
        return numberOfPages;
    }

    @Override
    public int accept(Visitor visitor) {
        return visitor.visit(this);
    }
}

Y ahora implementemos la interfaz de visitante y mostremos este patrón de diseño. La clase de implementación tendrá su propia lógica para calcular el precio de los artículos, no los artículos en sí:

public class VisitorImpl implements Visitor {

    @Override
    public int visit(Pen pen) {
        int price = pen.getPrice();
        System.out.println(pen.getModel() + " costs " + price);
        return price;
    }

    @Override
    public int visit(Notebook notebook) {
        int price = 0;
        if(notebook.getNumberOfPages() > 250) {
            price = notebook.getPrice()-5;
        } else {
            price = notebook.getPrice();
        }
        System.out.println("Notebook costs " + price);
        
        return price;
    }
}

Y para ilustrar el punto del patrón:

public class Pharos.shJavaDesignPatterns {
    public static void main(String[] args) {
        Item[] items = new Item[]{new Pen(10, "Parker"), new Pen(5, "Pilot"), new Notebook(50, 150), new Notebook(75, 300)};
        
        int total = getTotalPrice(items);
        System.out.println("Total price of items: " + total);
    }
    
    private static int getTotalPrice(Item[] items) {
        Visitor visitor = new VisitorImpl();
        int result = 0;
        for(Item item : items) {
            result = result + item.accept(visitor);
        }
        return result;
    }
}

Ejecutar este fragmento de código producirá:

Parker costs 10
Pilot costs 5
Notebook costs 50
Notebook costs 70
Total price of items: 135

Conclusión

Con esto, todos los patrones de diseño de comportamiento en Java están completamente cubiertos, con ejemplos de trabajo.

Si desea continuar leyendo sobre los patrones de diseño en Java, el siguiente artículo trata sobre los patrones de diseño J2EE.

 

About the author

Ramiro de la Vega

Bienvenido a Pharos.sh

Soy Ramiro de la Vega, Estadounidense con raíces Españolas. Empecé a programar hace casi 20 años cuando era muy jovencito.

Espero que en mi web encuentres la inspiración y ayuda que necesitas para adentrarte en el fantástico mundo de la programación y conseguir tus objetivos por difíciles que sean.

Add comment

Sobre mi

Últimos Post

Etiquetas

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con tus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, aceptas el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Más información
Privacidad