Patrones de diseño creacional en Java

Este es el primer artículo de una breve serie dedicada a los patrones de diseño en Java. Patrones de creación Los patrones de creación en Java que se tratan en este...

Visión general

This is the first article in a short series dedicated to Patrones de diseño en Java.

Patrones de creación {#patrones de creación}

Los patrones de creación en Java que se tratan en este artículo son:

Método de fábrica {#método de fábrica}

El método de fábrica, también llamado patrón de fábrica, es un patrón de diseño ampliamente utilizado que dirige la creación de objetos.

En este patrón, se crea una clase Factory como la clase principal de todas las subclases que pertenecen a un determinado segmento lógico de clases relacionadas.

Al igual que una SessionFactory se usa para crear, actualizar, eliminar y manipular todas las [Sesiones](https: //docs.jboss.org/hibernate/orm/3.5/api/org/hibernate/Session.html), por lo que cualquier otra fábrica es responsable de su conjunto de clases secundarias.

Es importante tener en cuenta que las subclases no se pueden alcanzar sin usar su fábrica respectiva. De esta forma, su creación queda oculta al cliente y depende de la fábrica.

Implementación:

Construyamos un proyecto pequeño y simple para demostrar esto.

Vamos a definir algunas clases pertenecientes a un segmento lógico, cada una de ellas implementando la misma interfaz. Luego vamos a crear una fábrica para estos objetos.

1
2
3
public interface Animal {
    void eat();    
}

La interfaz solo tiene un método para la conveniencia de presentar el punto.

Ahora, definamos algunas clases que implementan esta interfaz, cada una a su manera:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
 public class Dog implements Animal {
    @Override
    public void eat() {
        System.out.println("Dog is eating, woof!");
    }    
}

public class Cat implements Animal {
    @Override
    public void eat() {
        System.out.println("Cat is eating, meow!");
    }   
}

public class Rabbit implements Animal {
    @Override
    public void eat() {
        System.out.println("Rabbit is eating, squeak!");
    } 
}

Nota: estas clases son archivos .java separados, están agrupados de esta manera para facilitar la lectura.

Ahora que tenemos un grupo de clases, podemos designarles una fábrica:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class AnimalFactory {
    
    public Animal getAnimal(String animal) {
        if(animal.equals(null)) return null;
        
        if(animal.equalsIgnoreCase("Dog")) {
            return new Dog();
        } else if(animal.equalsIgnoreCase("Cat")) {
            return new Cat();
        } else if(animal.equalsIgnoreCase("Rabbit")) {
            return new Rabbit();
        }
        return null;        
    }  
}

De esta manera, tenemos una fábrica para instanciar nuestros objetos de una manera predefinida por la fábrica, sin contacto directo con los objetos mismos.

Ahora, observemos el resultado.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class Main {
    public static void main(String[] args) {
        AnimalFactory animalFactory = new AnimalFactory();
      
        Animal animal = animalFactory.getAnimal("dOg");
        animal.eat();
      
        Animal animal2 = animalFactory.getAnimal("CAT");
        animal2.eat();
      
        Animal animal3 = animalFactory.getAnimal("raBbIt");
        animal3.eat();
    }
}

Ejecutar este fragmento de código producirá:

1
2
3
Dog is eating, woof!
Cat is eating, meow!
Rabbit is eating, squeak!

If you'd like to read a standalone detailed article on El patrón de diseño del método de fábrica, we've got you covered!

Fábrica abstracta

El patrón de diseño de Abstract Factory se basa en el Patrón de fábrica y actúa como la fábrica más alta en la jerarquía. Representa la práctica de crear una fábrica de fábricas.

Este patrón es responsable de crear todas las demás fábricas como sus subclases, exactamente como las fábricas son responsables de crear todas sus propias subclases.

Implementación:

El ejemplo anterior se puede utilizar como una buena base para esta implementación.

La interfaz Animal se renombra a la interfaz Mascota y se cambia cada implementación:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class Dog implements Pet {
    @Override
    public void eat() {
        System.out.println("Dog is eating, woof!");
    }
}

public class Cat implements Pet {
    @Override
    public void eat() {
        System.out.println("Cat is eating, meow!");
    } 
}

public class Rabbit implements Pet {
    @Override
    public void eat() {
        System.out.println("Rabbit is eating, squeak!");
    }  
}

Se define una nueva interfaz:

1
2
3
public interface Human {
    public void feedPet();
}

Y como de costumbre, algunas clases concretas implementan esta interfaz:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class Child implements Human {
    @Override
    public void feedPet() {
        System.out.println("Child is feeding pet irresponsibly.");
    }
}

public class Adult implements Human {
    @Override
    public void feedPet() {
        System.out.println("Adult is feeding pet responsibly.");
    }
}

public class Elder implements Human {
    @Override
    public void feedPet() {
        System.out.println("Elder is overfeeding the pet.");
    } 
}

En este punto, tenemos las clases adecuadas para crear una AbstractFactory así como las respectivas clases Factory para estos dos grupos: PetFactory y HumanFactory.

La preocupación de AbstractFactory es la capacidad de proporcionar estos objetos al FactoryProducer, no instanciarlos:

1
2
3
4
public abstract class AbstractFactory {
    public abstract Pet getPet(String pet);
    public abstract Human getHuman(String human);
}

Antes de definir la clase que instancia estos objetos usando AbstractFactory, necesitamos crear nuestras dos fábricas.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class HumanFactory extends AbstractFactory {

    @Override
    Human getHuman(String human) {
        if(human.equals(null)) return null;
      
        if(human.equalsIgnoreCase("chILd")) {
            return new Child();
        } else if(human.equalsIgnoreCase("adult")) {
            return new Adult();
        } else if(human.equalsIgnoreCase("elDeR")) {
            return new Elder();
        }
        return null;
    }
    
    @Override
    Pet getPet(String pet) {
        // don't implement
        return null;
    }
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class PetFactory extends AbstractFactory {
    
    @Override
    public Pet getPet(String pet) {
        if(pet.equals(null)) return null;
        
        if(pet.equalsIgnoreCase("Dog")) {
            return new Dog();
        } else if(pet.equalsIgnoreCase("Cat")) {
            return new Cat();
        } else if(pet.equalsIgnoreCase("Rabbit")) {
            return new Rabbit();
        }
        return null;        
    }

    @Override
    Human getHuman(String human) {
        //don't implement
        return null;
    }
}

Y ahora, con estos, podemos crear el FactoryProducer que se encarga de instanciar las fábricas adecuadas, con la ayuda de AbstractFactory:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class FactoryProducer {
    public static AbstractFactory getFactory(String factory) {
        if(factory.equalsIgnoreCase("Human")) {
            return new HumanFactory();
        } else if(factory.equalsIgnoreCase("Pet")) {
            return new PetFactory();
        }
        return null;   
    }
}

Al pasar un String, el FactoryProducer devuelve el AbstractFactory con su factoría secundaria solicitada.

Ahora, observemos el resultado:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class Main {
    public static void main(String[] args) {

        AbstractFactory humanFactory = FactoryProducer.getFactory("Human");
        AbstractFactory petFactory = FactoryProducer.getFactory("Pet");
        
        Human human = humanFactory.getHuman("Child");
        human.feedPet();
        
        Pet pet = petFactory.getPet("Dog");
        pet.eat();
        
        Human human2 = humanFactory.getHuman("Elder");
        human2.feedPet();
        
        Pet pet2 = petFactory.getPet("Rabbit");
        pet2.eat();
    }
}

Al ejecutar este fragmento de código, nos recibe lo siguiente:

1
2
3
4
Child is feeding pet irresponsibly.
Dog is eating, woof!
Elder is overfeeding the pet.
Rabbit is eating, squeak!

Constructor

El patrón Builder se utiliza para ayudar a construir objetos finales, para clases con una gran cantidad de campos o parámetros, paso a paso. No es muy útil en clases pequeñas y simples que no tienen muchos campos, pero los objetos complejos son difíciles de leer y mantener por sí mismos.

Inicializar un objeto con más de unos pocos campos usando un constructor es complicado y susceptible a errores humanos.

Implementación:

Definamos una clase con algunos campos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Computer {
    private String computerCase;
    private String CPU;
    private String motherboard;
    private String GPU;
    private String HDD;
    private String operatingSystem;
    private int powerSupply;
    private int amountOfRAM;
   
    public Computer(String computerCase, String CPU, String motherboard, String GPU, 
    String HDD, String operatingSystem, int powerSupply, int amountOfRAM) {
        this.computerCase = computerCase;
        this.CPU = CPU;
        this.motherboard = motherboard;
        this.GPU = GPU;
        this.HDD = HDD;
        this.operatingSystem = operatingSystem;
        this.powerSupply = powerSupply;
        this.amountOfRAM = amountOfRAM;
   }

    //getters and setters
}

El problema es evidente: incluso una clase pequeña y simple como esta requiere un constructor grande y desordenado.

Las clases pueden tener fácilmente muchos más campos que este, lo que dio origen al patrón de diseño Builder.

Para aplicarlo, anidaremos una clase Static Builder dentro de la clase Computer.

Este constructor se usará para construir nuestros objetos de una manera clara y legible, a diferencia del ejemplo anterior:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public class Computer {
    
   public static class Builder {
       private String computerCase;
       private String CPU;
       private String motherboard;
       private String GPU;
       private String HDD;
       private String operatingSystem;
       private int powerSupply;
       private int amountOfRAM;
        
       public Builder withCase(String computerCase) {
           this.computerCase = computerCase;
           return this;
        }
        
        public Builder withCPU(String CPU) {
            this.CPU = CPU;
            return this;
        }
        
        public Builder withMotherboard(String motherboard) {
            this.motherboard = motherboard;
            return this;
        }
        
        public Builder withGPU(String GPU) {
            this.GPU = GPU;
            return this;
        }
        
        public Builder withHDD(String HDD) {
            this.HDD = HDD;
            return this;
        }
        
        public Builder withOperatingSystem(String operatingSystem) {
            this.operatingSystem = operatingSystem;
            return this;
        }
        
        public Builder withPowerSupply(int powerSupply) {
            this.powerSupply = powerSupply;
            return this;
        }
        
        public Builder withAmountOfRam(int amountOfRAM) {
            this.amountOfRAM = amountOfRAM;
            return this;
        }
        
        public Computer build() {
            Computer computer = new Computer();
            computer.computerCase = this.computerCase;
            computer.CPU = this.CPU;
            computer.motherboard = this.motherboard;
            computer.GPU = this.GPU;
            computer.HDD = this.HDD;
            computer.operatingSystem = this.operatingSystem;
            computer.powerSupply = this.powerSupply;
            computer.amountOfRAM = this.amountOfRAM;
            
            return computer;
        }
   }
   
   private Computer() {
       //nothing here
   }
   
    //fields
    //getters and setters
}

Esta clase anidada tiene los mismos campos que la clase Computer y los usa para construir el objeto en sí.

El constructor Computer se hace privado, por lo que la única forma de inicializarlo es a través de la clase Builder.

Con Builder configurado, podemos inicializar los objetos Computer:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class Main {
    public static void main(String[] args) {
        Computer computer = new Computer.Builder()
                .withCase("Tower")
                .withCPU("Intel i5")
                .withMotherboard("MSI B360M-MORTAR")
                .withGPU("nVidia Geforce GTX 750ti")
                .withHDD("Toshiba 1TB")
                .withOperatingSystem("Windows 10")
                .withPowerSupply(500)
                .withAmountOfRam(8)
                .build();
    }
}

Esta es una forma mucho más limpia y detallada que escribir:

1
2
3
4
5
6
public class Main {
    public static void main(String[] args) {
        Computer computer = new Computer("Tower", "Intel i5", "MSI B360M-MORTAR",  
        "nVidia GeForce GTX 750ti, "Toshiba 1TB", "Windows 10", 500, 8);
    }
}

If you'd like to read a standalone, detailed article on El patrón de diseño del constructor, we've got you covered!

Prototipo

El patrón de prototipo se usa principalmente para minimizar el costo de la creación de objetos, generalmente cuando las aplicaciones a gran escala crean, actualizan o recuperan objetos que cuestan muchos recursos.

Esto se hace copiando el objeto, una vez que se crea, y reutilizando la copia del objeto en solicitudes posteriores, para evitar realizar otra operación con muchos recursos. Depende de la decisión del desarrollador si se trata de una copia completa o superficial del objeto, aunque el objetivo es el mismo.

Implementación:

Dado que este patrón clona objetos, sería apropiado definir una clase para ellos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// to clone the object, the class needs to implement Cloneable
public abstract class Employee implements Cloneable { 

    private String id;
    protected String position;
    private String name;
    private String address;
    private double wage;
    
    abstract void work();
    
    public Object clone() {
        Object clone = null;
        try {
            clone = super.clone();
        } catch(CloneNotSupportedException ex) {
            ex.printStackTrace();
        }
        return clone;
    }
   //getters and setters
}

Ahora, como de costumbre, definamos algunas clases que amplían Employee:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Programmer extends Employee {
    public Programmer() {
        position = "Senior";
    } 
    @Override
    void work() {
        System.out.println("Writing code!");
    }   
}

public class Janitor extends Employee {
    public Janitor() {
        position = "Part-time";
    }
    @Override
    void work() {
        System.out.println("Cleaning the hallway!");
    } 
}

public class Manager extends Employee {
    public Manager() {
        position = "Intern";
    }
    @Override
    void work() {
        System.out.println("Writing a schedule for the project!");
    }  
}

En este punto, tenemos todo lo que necesitamos para que una clase de una capa de datos guarde, actualice y recupere a estos empleados por nosotros.

Se utilizará una tabla hash para simular una base de datos y los objetos predefinidos simularán los objetos recuperados a través de consultas:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class EmployeesHashtable {
    
    private static Hashtable<String, Employee> employeeMap = new Hashtable<String, Employee>();
    
    public static Employee getEmployee(String id) {
        Employee cacheEmployee = employeeMap.get(id);
        // a cast is needed because the clone() method returns an Object
        return (Employee) cacheEmployee.clone();
    }
    
    public static void loadCache() {
        // predefined objects to simulate retrieved objects from the database
        Programmer programmer = new Programmer();
        programmer.setId("ETPN1");
        employeeMap.put(programmer.getId(), programmer);
        
        Janitor janitor = new Janitor();
        janitor.setId("ETJN1");
        employeeMap.put(janitor.getId(), janitor);
        
        Manager manager = new Manager();
        manager.setId("ETMN1");
        employeeMap.put(manager.getId(), manager);
    }
}

Para observar el resultado:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class Main {
    public static void main(String[] args) {
        EmployeesHashtable.loadCache();
        
        Employee cloned1 = (Employee) EmployeesHashtable.getEmployee("ETPN1");
        Employee cloned2 = (Employee) EmployeesHashtable.getEmployee("ETJN1");
        Employee cloned3 = (Employee) EmployeesHashtable.getEmployee("ETMN1");
        
        System.out.println("Employee: " + cloned1.getPosition() + " ID:" 
            + cloned1.getId());
        System.out.println("Employee: " + cloned2.getPosition() + " ID:" 
            + cloned2.getId());
        System.out.println("Employee: " + cloned3.getPosition() + " ID:"                 
            + cloned3.getId());
    }
}

Ejecutar este fragmento de código producirá:

1
2
3
Employee: Senior ID:ETPN1
Employee: Part-time ID:ETJN1
Employee: Intern ID:ETMN1

Único

El patrón Singleton asegura la existencia de una sola instancia de objeto en toda la JVM.

Este es un patrón bastante simple y brinda la capacidad de acceder a este objeto incluso sin instanciarlo. Otros patrones de diseño usan este patrón, como los patrones Abstract Factory, Builder y Prototype que ya hemos cubierto.

Implementación:

Esta es una implementación bastante simple de una clase Singleton:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class SingletonClass {
    
    private static SingletonClass instance = new SingletonClass();
   
    private SingletonClass() {}
    
    public static SingletonClass getInstance() {
        return instance;
    }
    
    public void showMessage() {
        System.out.println("I'm a singleton object!");   
    }
}

Esta clase está creando un objeto estático de sí mismo, que representa la instancia global.

Al proporcionar un constructor privado, no se puede crear una instancia de la clase.

Se utiliza un método estático getInstance() como punto de acceso global para el resto de la aplicación.

Se puede agregar cualquier cantidad de métodos públicos a esta clase, pero no es necesario hacerlo para este tutorial.

Con esto, nuestra clase cumple con todos los requisitos para convertirse en Singleton.

Definamos un código que recupere este objeto y ejecute un método:

1
2
3
4
5
6
public class Main {
    public static void main(String[] args) {
        SingletonClass singletonClass = SingletonClass.getInstance();
        singletonClass.showMessage();
    }
}

Ejecutar este código dará como resultado:

1
I'm a singleton object!

Conclusión

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

If you'd like to continue reading about Design Patterns in Java, the following article covers Patrones de diseño estructural.