Patrones de diseño creacional en Java

    Visión general

    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 artículo son:

    • Método / plantilla de fábrica
    • Fábrica abstracta
    • Constructor
    • Prototipo
    • único

    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 ordena la creación de objetos.

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

    Como un SessionFactory se utiliza para crear, actualizar, eliminar y manipular todos Session objetos, también lo es cualquier otra fábrica responsable de su conjunto de clases secundarias.

    Es importante tener en cuenta que no se puede llegar a las subclases sin utilizar 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 que pertenecen a un segmento lógico, cada una de las cuales implementa la misma interfaz. Luego crearemos una fábrica para estos objetos.

    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:

     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 designar una fábrica para ellas:

    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 forma, tenemos una fábrica para instanciar nuestros objetos de una manera predefinida por la fábrica, sin contacto directo con los objetos en sí.

    Ahora, observemos el resultado.

    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á:

    Dog is eating, woof!
    Cat is eating, meow!
    Rabbit is eating, squeak!
    

    Si desea leer un artículo detallado independiente sobre el patrón de diseño del método de fábrica, ¡lo tenemos cubierto!

    Fábrica abstracta

    los Fábrica abstracta patrón de diseño se basa en el Patrón de fábrica y actúa como la fábrica más alta de la jerarquía. Representa la práctica de crear un 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.

    los Animal se cambia el nombre de la interfaz a Pet interfaz y cada implementación se cambia:

    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:

    public interface Human {
        public void feedPet();
    }
    

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

    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 un AbstractFactory así como las respectivas clases Factory para estos dos grupos: PetFactory y HumanFactory.

    los AbstractFactoryLa preocupación es la capacidad de proporcionar estos objetos a la FactoryProducer, no para instanciarlos:

    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 el AbstractFactory, necesitamos crear nuestras dos fábricas.

    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;
        }
    
    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 tiene la responsabilidad de instanciar las fábricas adecuadas, con la ayuda del AbstractFactory:

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

    Pasando un String, la FactoryProducer devuelve el AbstractFactory con su fábrica infantil solicitada.

    Ahora, observemos el resultado:

    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, somos recibidos con:

    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:

    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 un static Builder clase dentro de la Computer clase.

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

    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 Computer class y los usa para construir el objeto en sí.

    los Computer constructor se hace privado de modo que la única forma de inicializarlo es a través del Builder clase.

    Con el Builder toda la configuración, podemos inicializar Computer objetos:

    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:

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

    Si desea leer un artículo independiente y detallado sobre el patrón de diseño del constructor, ¡lo tenemos cubierto!

    Prototipo

    El patrón de prototipo se utiliza principalmente para minimizar el costo de 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 creado, 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:

    // 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 se extienden Employee:

    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 una clase de una capa de datos para guardar, actualizar y recuperar estos empleados por nosotros.

    UN Hashtable se utilizará para simular una base de datos, y los objetos predefinidos simularán los objetos recuperados mediante consultas:

    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:

    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á:

    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 proporciona 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:

    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í misma, que representa la instancia global.

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

    Un método estático getInstance() se utiliza 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:

    public class Main {
        public static void main(String[] args) {
            SingletonClass singletonClass = SingletonClass.getInstance();
            singletonClass.showMessage();
        }
    }
    

    Ejecutar este código resultará en:

    I'm a singleton object!
    

    Conclusión

    Con esto, todos Patrones de diseño creacional en Java están completamente cubiertos, con ejemplos de trabajo.

    Si desea continuar leyendo acerca de los patrones de diseño en Java, el siguiente artículo cubre los patrones de diseño estructural.

    Etiquetas:

    Deja una respuesta

    Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *