El patrón de diseño del constructor en Java

El patrón Builder Design se ocupa de la construcción de objetos y nos permite construir objetos complejos con menos errores inducidos por humanos y mejora la capacidad de mantenimiento y la escalabilidad.

Introducción

En este artículo, desglosaremos el Patrón de diseño de constructor y mostraremos su aplicación en Java.

Patrones de diseño son simplemente conjuntos de prácticas estandarizadas comúnmente utilizadas en la industria del desarrollo de software. Representan soluciones, proporcionadas por la comunidad, a problemas comunes que se enfrentan en las tareas cotidianas relacionadas con el desarrollo de software.

Conocer la abstracción, la herencia y el polimorfismo no necesariamente lo convierte en un buen diseñador orientado a objetos listo para usar. Un experto en diseño crea diseños que se pueden mantener y son flexibles, pero lo más importante: comprensibles.

Una buena idea limitada al inventor no es tan buena idea.

Patrones de diseño creativo {#patrones de diseño creativo}

Patrones de diseño creacional centrado en la creación de objetos. La creación de objetos es una parte realmente importante en el diseño orientado a objetos, y optimizar esta tarea en aplicaciones complejas y de alto rendimiento es primordial.

Estos patrones controlan la forma en que definimos y diseñamos los objetos, así como también cómo los instanciamos. Algunos encapsulan la lógica de creación lejos de los usuarios y manejan la creación (Factory y Abstract Factory; ), algunos se centran en el proceso de creación de los propios objetos (Builder), algunos minimizan el coste de creación ([Prototype](/design-patterns -creative-in-java/ )) y algunos controlan el número de instancias en toda la JVM (único).

En este artículo, nos sumergiremos en el Patrón de diseño de constructor.

El patrón de diseño del constructor

Definición

El patrón de diseño del constructor separa la construcción de un objeto complejo de su representación. Esto se hace a través de una clase ’estática’ anidada que asigna los valores requeridos antes de que se devuelva la instancia.

Otra cosa a tener en cuenta es que el Patrón Constructor se usa a menudo para crear objetos inmutables. La existencia de métodos setter prácticamente desafía la inmutabilidad, y dado que no los usamos cuando tenemos el patrón Builder en su lugar, es mucho más fácil crear objetos inmutables, sin tener que pasar todos los parámetros en el constructor. llamar.

Motivación

Instanciar un objeto en Java es simple. Usamos la palabra clave nuevo, seguido del constructor y los parámetros que estamos asignando al objeto. Una instanciación típica puede verse así:

1
Cookie chocolateChip = new Cookie("Chocolate Chip Cookie");

Se pasa una cadena al constructor, y es bastante evidente sin ver la definición de clase que representa el tipo/nombre de la cookie.

Sin embargo, si queremos instanciar una clase más compleja, como una red neuronal, en este estilo, nos enfrentamos a:

1
2
3
SingleLayerNetwork configuration = new NeuralNetConfiguration(4256, STOCHASTIC_GRADIENT_DESCENT,
                                                              new Adam(), 1e-4, numRows*numColumns,
                                                              1000, RELU, XAVIER);

Incluso con solo 8 parámetros, el código rápidamente se vuelve ilegible e incomprensible. Incluso para el desarrollador que escribió la definición de clase en primer lugar. ¿Qué sucede cuando un nuevo desarrollador intenta usar esta clase?

O mejor aún, imagina tener que llamar al constructor de esta clase para instanciarla:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class SmartHome {
    private String name;
    private int serialNumber;
    private String addressName;
    private String addressNumber;
    private String city;
    private String country;
    private String postalCode;
    private int light1PortNum;
    private int light2PortNum;
    private int door1PortNum;
    private int door2PortNum;
    private int microwavePortNum;
    private int tvPortNum;
    private int waterHeaterPortNum;

    public SmartHome(String name, int serialNumber, String addressName, String addressNumber, String city, String country, String postalCode, int light1PortNum, int light2PortNum, int door1PortNum, int door2PortNum, int microwavePortNum, int tvPortNum, int waterHeaterPortNum) {
        // Assigning values in the constructor call
    }

    // Getters and Setters
}

Nos enfrentamos a demasiados argumentos de constructor, y con poca variedad de tipos, estaremos viendo una gran llamada de constructor sin forma de saber qué es qué.

También tenga en cuenta que en Java no se aceptan dos constructores con el mismo tipo de parámetro, pero con diferentes nombres de variables.

Tener estos dos constructores no está permitido en Java ya que el compilador no puede diferenciarlos:

1
2
public SmartHome(int door1PortNum) { ... }
public SmartHome(int door2PortNum) { ... }

Incluso si tenemos un constructor con el tipo de parámetro int:

1
public SmartHome(int portNum) { ... }

Sabemos que tenemos que configurar un número de puerto, pero no sabremos si ese número es el puerto de la puerta, la luz, el microondas, la TV o el calentador de agua.

Esta clase rápidamente se vuelve inutilizable en un entorno de equipo. Incluso si es un espectáculo de un solo hombre, buena suerte recordando el orden de los parámetros después de una semana de no instanciar la clase.

Aquí es donde entra en juego el patrón constructor:

El Patrón Constructor separa la construcción de la representación.

¿Qué significa esto?

La construcción se realiza en la propia clase. La representación es lo que vemos como el usuario de la clase. En este momento, nuestras dos clases anteriores tienen estas dos unidas: llamamos directamente al constructor con los argumentos pasados.

Al separar estos dos, podemos hacer que la representación de la clase sea mucho más simple, ordenada y legible, mientras que el constructor hace su parte.

Implementación

Hay algunos pasos a seguir para implementar el patrón Builder. Continuando con nuestros ejemplos anteriores, usaremos la clase SmartHome para mostrar estos pasos:

  • Se debe anidar una clase constructora estática en nuestra clase SmartHome
  • El constructor SmartHome debe ser privado para que el usuario final no pueda llamarlo
  • La clase de constructor debe tener un nombre intuitivo, como SmartHomeBuilder
  • La clase SmartHomeBuilder tendrá los mismos campos que la clase SmartHome
  • Los campos de la clase SmartHome pueden ser finales o no, dependiendo de si quieres que sea inmutable o no
  • La clase SmartHomeBuilder contendrá métodos que establecen los valores, de forma similar a los métodos setter. Estos métodos incluirán SmartHomeBuilder como tipo de retorno, asignarán los valores pasados ​​a los campos de la clase de constructor estático y seguirán la convención de nomenclatura del constructor. Por lo general, comenzarán con with, in, at, etc. en lugar de set.
  • La clase de constructor estático contendrá un método build() que inyecta estos valores en SmartHome y devuelve una instancia del mismo.

Dicho esto, implementemos el patrón Builder en nuestra clase de ejemplo:

 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
public class SmartHome {
    // Fields omitted for brevity
    // The same fields should be in `SmartHome` and `SmartHomeBuilder`

    // Private constructor means we can't instantiate it
    // by simply calling `new SmartHome()`
    private SmartHome() {}

    public static class SmartHomeBuilder {
        private String name;
        private int serialNumber;
        private String addressName;
        private String addressNumber;
        private String city;
        private String country;
        private String postalCode;
        private int light1PortNum;
        private int light2PortNum;
        private int door1PortNum;
        private int door2PortNum;
        private int microwavePortNum;
        private int tvPortNum;
        private int waterHeaterPortNum;

        public SmartHomeBuilder withName(String name) {
            this.name = name;
            return this;
        }

        public SmartHomeBuilder withSerialNumber(int serialNumber) {
            this.serialNumber = serialNumber;
            return this;
        }

        public SmartHomeBuilder withAddressName(String addressName) {
            this.addressName = addressName;
            return this;
        }

        public SmartHomeBuilder inCity(String city) {
            this.city = city;
            return this;
        }

        public SmartHomeBuilder inCountry(String country) {
            this.country = country;
            return this;
        }

        // The rest of the methods are omitted for brevity
        // All follow the same principle

        public SmartHome build() {
            SmartHome smartHome = new SmartHome();
            smartHome.name = this.name;
            smartHome.serialNumber = this.serialNumber;
            smartHome.addressName = this.addressName;
            smartHome.city = this.city;
            smartHome.country = this.country;
            smartHome.postalCode = this.postalCode;
            smartHome.light1PortNum = this.light1PortNum;
            smartHome.light2PortNum = this.light2PortNum;
            smartHome.door1PortNum = this.door1PortNum;
            smartHome.door2PortNum = this.door2PortNum;
            smartHome.microwavePortNum = this.microwavePortNum;
            smartHome.tvPortNum = this.tvPortNum;
            smartHome.waterHeaterPortNum = this.waterHeaterPortNum;

            return smartHome;
        }
    }
}

La clase SmartHome no tiene constructores públicos y la única forma de crear un objeto SmartHome es a través de la clase SmartHomeBuilder, así:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
SmartHome smartHomeSystem = new SmartHome
    .SmartHomeBuilder()
    .withName("RaspberrySmartHomeSystem")
    .withSerialNumber(3627)
    .withAddressName("Main Street")
    .withAddressNumber("14a")
    .inCity("Kumanovo")
    .inCountry("Macedonia")
    .withPostalCode("1300")
    .withDoor1PortNum(342)
    .withDoor2PortNum(343)
    .withLight1PortNum(211)
    .withLight2PortNum(212)
    .withMicrowavePortNum(11)
    .withTvPortNum(12)
    .withWaterHeaterPortNum(13)
    .build();

System.out.println(smartHomeSystem);

Si bien hemos hecho que la clase en sí sea más complicada al incluir una clase anidada con campos duplicados, la representación está separada de la creación.

Es evidente lo que estamos construyendo al instanciar el objeto. Es legible, comprensible y cualquiera puede usar sus clases para construir objetos.

Volviendo al ejemplo de la red neuronal del mundo real, se vería algo así:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
MultiLayerNetwork conf = new NeuralNetConfiguration.Builder()
    .seed(rngSeed)
    .optimizationAlgo(OptimizationAlgorithm.STOCHASTIC_GRADIENT_DESCENT)
    .updater(new Adam())
    .l2(1e-4)
    .list()
    .layer(new DenseLayer.Builder()
        .nIn(numRows * numColumns) // Number of input datapoints.
        .nOut(1000) // Number of output datapoints.
        .activation(Activation.RELU) // Activation function.
        .weightInit(WeightInit.XAVIER) // Weight initialization.
        .build())
    .layer(new OutputLayer.Builder(LossFunctions.LossFunction.NEGATIVELOGLIKELIHOOD)
        .nIn(1000)
        .nOut(outputNum)
        .activation(Activation.SOFTMAX)
        .weightInit(WeightInit.XAVIER)
        .build())
    .pretrain(false).backprop(true)
    .build()

[Crédito del código: DeepLearning4j - Inicio rápido]{.small}

Ventajas y desventajas

Además del punto más obvio de usar el patrón de construcción, hay un par de otras ventajas que pueden no ser demasiado obvias a primera vista:

  • Puede cambiar la implementación del objeto de la forma que desee y simplemente actualizar los métodos. El usuario final se enfrenta a una interfaz abstracta a través de la clase de constructor estático y no se preocupa por la implementación subyacente.
  • Soporta la encapsulación al desacoplar la representación del objeto de la construcción.

La única desventaja real es que aumenta la cantidad de código en los modelos de dominio. Por lo general, ya son largos, aunque son relativamente simples (campos, getters y setters). Sin embargo, rara vez manipularías estas clases de todos modos.

En general, las ventajas superan con creces a las desventajas cuando se trata de Builder Pattern, que es la razón por la que generalmente se emplea en muchos, especialmente en aplicaciones, marcos y bibliotecas complejos.

Conclusión

Patrones de diseño son simplemente conjuntos de prácticas estandarizadas utilizadas en la industria del desarrollo de software. Representan soluciones, proporcionadas por la comunidad, a problemas comunes que se enfrentan en las tareas cotidianas relacionadas con el desarrollo de software.

En este artículo, nos hemos sumergido en un patrón de diseño creativo clave que se ocupa de la construcción de objetos y permite a los desarrolladores crear objetos complejos con muchos menos errores inducidos por humanos y mejora la capacidad de mantenimiento y la escalabilidad.

El patrón Builder Design ofrece varias ventajas sobre la simple creación de instancias de clases a través de constructores, con una desventaja que realmente no se compara con la cantidad de beneficios que puede obtener al emplearlo. emplearlo.