He visto muchas implementaciones del patrón Builder (principalmente en Java). Todos ellos tienen una clase de entidad (digamos una Person
clase) y una clase de generador PersonBuilder
. El constructor "apila" una variedad de campos y devuelve un new Person
con los argumentos pasados. ¿Por qué necesitamos explícitamente una clase de generador, en lugar de poner todos los métodos de generador en la Person
clase misma?
Por ejemplo:
class Person {
private String name;
private Integer age;
public Person() {
}
Person withName(String name) {
this.name = name;
return this;
}
Person withAge(int age) {
this.age = age;
return this;
}
}
Simplemente puedo decir Person john = new Person().withName("John");
¿Por qué la necesidad de una PersonBuilder
clase?
El único beneficio que veo es que podemos declarar los Person
campos como final
, asegurando así la inmutabilidad.
java
design-patterns
builder-pattern
Boyan Kushlev
fuente
fuente
chainable setters
: DwithName
devolver una copia de la Persona con solo el campo de nombre cambiado. En otras palabras,Person john = new Person().withName("John");
podría funcionar incluso siPerson
es inmutable (y este es un patrón común en la programación funcional).void
métodos. Entonces, por ejemplo, siPerson
tiene un método que imprime su nombre, aún puede encadenarlo con una interfaz fluidaperson.setName("Alice").sayName().setName("Bob").sayName()
. Por cierto, anoto aquellos en JavaDoc con exactamente su sugerencia@return Fluent interface
: es lo suficientemente genérico y claro cuando se aplica a cualquier método que lo hacereturn this
al final de su ejecución y es bastante claro. Por lo tanto, un generador también estará haciendo una interfaz fluida.Respuestas:
Es para que pueda ser inmutable Y simular parámetros con nombre al mismo tiempo.
Eso mantiene a tus guantes alejados de la persona hasta que se establece su estado y, una vez configurado, no te permite cambiarlo, sin embargo, cada campo está claramente etiquetado. No puede hacer esto con solo una clase en Java.
Parece que estás hablando de Josh Blochs Builder Pattern . Esto no debe confundirse con el Patrón de Constructor de la Banda de los Cuatro . Estas son diferentes bestias. Ambos resuelven problemas de construcción, pero de maneras bastante diferentes.
Por supuesto, puedes construir tu objeto sin usar otra clase. Pero luego tienes que elegir. Pierde la capacidad de simular parámetros con nombre en lenguajes que no los tienen (como Java) o pierde la capacidad de permanecer inmutable durante toda la vida útil de los objetos.
Ejemplo inmutable, no tiene nombres para parámetros
Aquí estás construyendo todo con un solo constructor simple. Esto le permitirá permanecer inmutable, pero perderá la simulación de parámetros con nombre. Eso se vuelve difícil de leer con muchos parámetros. A las computadoras no les importa, pero es difícil para los humanos.
Ejemplo de parámetro con nombre simulado con setters tradicionales. No inmutable
Aquí está creando todo con setters y simulando parámetros con nombre, pero ya no es inmutable. Cada uso de un setter cambia el estado del objeto.
Entonces, lo que obtienes al agregar la clase es que puedes hacer ambas cosas.
La validación se puede realizar
build()
si un error de tiempo de ejecución para un campo de edad faltante es suficiente para usted. Puede actualizar eso y aplicar eso queage()
se llama con un error del compilador. Simplemente no con el patrón de construcción Josh Bloch.Para eso necesita un lenguaje interno específico de dominio (iDSL).
Esto le permite exigir que llamen
age()
yname()
antes de llamarbuild()
. Pero no puede hacerlo simplemente volviendothis
cada vez. Cada cosa que regresa devuelve una cosa diferente que te obliga a llamar a la siguiente.El uso podría verse así:
Pero esto:
provoca un error del compilador porque
age()
solo es válido llamar al tipo devuelto porname()
.Estos iDSL son extremadamente potentes ( JOOQ o Java8 Streams, por ejemplo) y son muy agradables de usar, especialmente si usa un IDE con finalización de código, pero su configuración es bastante complicada . Recomiendo guardarlos para cosas que tengan un poco de código fuente escrito en su contra.
fuente
AnonymousPersonBuilder
que tiene su propio conjunto de reglas.Por qué usar / proporcionar una clase de constructor:
fuente
PersonBuilder
no tiene captadores, y la única forma de inspeccionar los valores actuales es llamar.Build()
para devolver aPerson
. De esta manera .Build puede validar un objeto construido correctamente, ¿correcto? ¿Es este el mecanismo destinado a evitar el uso de un objeto "en construcción"?Build
operación para evitar objetos defectuosos y / o poco construidosgetAge
al generador puede volvernull
cuando esta propiedad aún no se haya especificado. Por el contrario, laPerson
clase puede imponer la invariante que la edad nunca puede sernull
, lo que es imposible cuando el constructor y el objeto real se mezclan, como con elnew Person() .withName("John")
ejemplo del OP , que felizmente crea unPerson
sin edad. Esto se aplica incluso a clases mutables, ya que los establecedores pueden imponer invariantes, pero no pueden imponer valores iniciales.Una razón sería asegurar que todos los datos pasados sigan las reglas de negocio.
Su ejemplo no toma esto en consideración, pero digamos que alguien pasó una cadena vacía o una cadena que consiste en caracteres especiales. Desea hacer algún tipo de lógica basada en asegurarse de que su nombre sea realmente un nombre válido (que en realidad es una tarea muy difícil).
Podría poner todo eso en su clase de Persona, especialmente si la lógica es muy pequeña (por ejemplo, solo asegurándose de que una edad no sea negativa), pero a medida que la lógica crece, tiene sentido separarla.
fuente
john
Un pequeño ángulo diferente sobre esto de lo que veo en otras respuestas.
El
withFoo
enfoque aquí es problemático porque se comportan como setters pero se definen de una manera que hace que parezca que la clase admite la inmutabilidad. En las clases de Java, si un método modifica una propiedad, es costumbre comenzar el método con 'set'. Nunca me ha encantado como estándar, pero si haces algo más, sorprenderá a la gente y eso no es bueno. Hay otra forma en que puede admitir la inmutabilidad con la API básica que tiene aquí. Por ejemplo:No proporciona mucho en cuanto a la prevención de objetos construidos parcialmente de manera inadecuada, pero evita cambios en cualquier objeto existente. Probablemente sea una tontería para este tipo de cosas (y también lo es el JB Builder). Sí, creará más objetos, pero esto no es tan costoso como podría pensar.
En su mayoría, verá este tipo de enfoque utilizado con estructuras de datos concurrentes, como CopyOnWriteArrayList . Y esto sugiere por qué la inmutabilidad es importante. Si desea que su código sea seguro, casi siempre debe considerarse la inmutabilidad. En Java, cada subproceso puede mantener un caché local de estado variable. Para que un subproceso vea los cambios realizados en otros subprocesos, debe emplearse un bloque sincronizado u otra característica de concurrencia. Cualquiera de estos agregará algo de sobrecarga al código. Pero si sus variables son finales, no hay nada que hacer. El valor siempre será el que se inicializó y, por lo tanto, todos los subprocesos ven lo mismo sin importar qué.
fuente
Otra razón que aún no se ha mencionado explícitamente aquí, es que el
build()
método puede verificar que todos los campos son 'campos que contienen valores válidos (ya sea establecidos directamente o derivados de otros valores de otros campos), que probablemente sea el modo de falla más probable eso ocurriría de otra manera.Otro beneficio es que su
Person
objeto terminaría teniendo una vida más simple y un conjunto simple de invariante. Sabes que una vez que tienes unPerson p
, tienes unp.name
y un válidop.age
. Ninguno de sus métodos tiene que estar diseñado para manejar situaciones como "bueno, ¿qué pasa si se establece la edad pero no el nombre, o qué pasa si se establece el nombre pero no la edad? Esto reduce la complejidad general de la clase.fuente
URI
, unaFile
, o unaFileInputStream
, y los usos lo que fuera capaz de efectuar unaFileInputStream
que en última instancia va a la llamada al constructor como argTambién se puede definir un generador para devolver una interfaz o una clase abstracta. Puede usar el constructor para definir el objeto, y el constructor puede determinar qué subclase concreta devolver en función de qué propiedades están establecidas o en qué están configuradas, por ejemplo.
fuente
El patrón de construcción se usa para construir / crear el objeto paso a paso configurando las propiedades y cuando se configuran todos los campos requeridos, devuelve el objeto final utilizando el método de construcción. El objeto recién creado es inmutable. El punto principal a tener en cuenta aquí es que el objeto solo se devuelve cuando se invoca el método de compilación final. Esto asegura que todas las propiedades se establecen en el objeto y, por lo tanto, el objeto no está en estado inconsistente cuando la clase de constructor lo devuelve.
Si no usamos la clase de constructor y colocamos directamente todos los métodos de la clase de constructor en la clase Persona en sí, primero tenemos que crear el Objeto y luego invocar los métodos de establecimiento en el objeto creado, lo que conducirá al estado inconsistente del objeto entre la creación. de objeto y establecer las propiedades.
Por lo tanto, al usar la clase de constructor (es decir, alguna entidad externa que no sea la clase Person), nos aseguramos de que el objeto nunca estará en un estado inconsistente.
fuente
Reutilizar objeto constructor
Como otros han mencionado, la inmutabilidad y la verificación de la lógica empresarial de todos los campos para validar el objeto son las principales razones para un objeto generador separado.
Sin embargo, la reutilización es otro beneficio. Si quiero crear una instancia de muchos objetos que son muy similares, puedo hacer pequeños cambios en el objeto generador y continuar creando instancias. No es necesario volver a crear el objeto generador. Esta reutilización permite al constructor actuar como plantilla para crear muchos objetos inmutables. Es un pequeño beneficio, pero podría ser útil.
fuente
En realidad, puede tener los métodos de construcción en su propia clase y aún así tener inmutabilidad. Simplemente significa que los métodos del generador devolverán nuevos objetos, en lugar de modificar el existente.
Esto solo funciona si hay una manera de obtener un objeto inicial (válido / útil) (por ejemplo, de un constructor que establece todos los campos requeridos, o un método de fábrica que establece los valores predeterminados), y los métodos de construcción adicionales devuelven objetos modificados basados en el existente. Esos métodos de creación deben asegurarse de no obtener objetos inválidos / inconsistentes en el camino.
Por supuesto, esto significa que tendrá muchos objetos nuevos, y no debe hacerlo si sus objetos son caros de crear.
Lo utilicé en el código de prueba para crear emparejadores de Hamcrest para uno de mis objetos comerciales. No recuerdo el código exacto, pero se parecía a esto (simplificado):
Luego lo usaría así en mis pruebas unitarias (con importaciones estáticas adecuadas):
fuente
name
pero noage
), entonces el concepto no funciona. Bueno, en realidad podrías estar devolviendo algo así comoPartiallyBuiltPerson
que no es válido, pero parece un truco para enmascarar al Constructor.