sirve - set border java
¿Cómo mejorar el patrón del constructor? (10)
Motivación
Recientemente busqué una forma de inicializar un objeto complejo sin pasar muchos parámetros al constructor. Lo probé con el patrón del generador, pero no me gusta el hecho de que no pueda verificar en tiempo de compilación si realmente configuré todos los valores necesarios.
Patrón de constructor tradicional
Cuando uso el patrón de generador para crear mi objeto Complex
, la creación es más "segura", porque es más fácil ver para qué se usa un argumento:
new ComplexBuilder()
.setFirst( "first" )
.setSecond( "second" )
.setThird( "third" )
...
.build();
Pero ahora tengo el problema, que fácilmente puedo pasar por alto un parámetro importante. Puedo verificarlo dentro del método build()
, pero eso es solo en tiempo de ejecución. En tiempo de compilación no hay nada que me advierta, si me perdí algo.
Patrón de constructor mejorado
Ahora mi idea era crear un constructor que me "recuerde" si omití un parámetro necesario. Mi primer intento se ve así:
public class Complex {
private String m_first;
private String m_second;
private String m_third;
private Complex() {}
public static class ComplexBuilder {
private Complex m_complex;
public ComplexBuilder() {
m_complex = new Complex();
}
public Builder2 setFirst( String first ) {
m_complex.m_first = first;
return new Builder2();
}
public class Builder2 {
private Builder2() {}
Builder3 setSecond( String second ) {
m_complex.m_second = second;
return new Builder3();
}
}
public class Builder3 {
private Builder3() {}
Builder4 setThird( String third ) {
m_complex.m_third = third;
return new Builder4();
}
}
public class Builder4 {
private Builder4() {}
Complex build() {
return m_complex;
}
}
}
}
Como puede ver, cada setter de la clase de generador devuelve una clase de generador interno diferente. Cada clase interna de compilador proporciona exactamente un método setter y el último proporciona solo un método build ().
Ahora la construcción de un objeto se ve así:
new ComplexBuilder()
.setFirst( "first" )
.setSecond( "second" )
.setThird( "third" )
.build();
... pero no hay forma de olvidar un parámetro necesario. El compilador no lo aceptaría.
Parámetros opcionales
Si tuviera parámetros opcionales, usaría la última clase interna de Builder4
para establecerlos como un constructor "tradicional", retornando a sí mismo.
Preguntas
- ¿Es este un patrón bien conocido? ¿Tiene un nombre especial?
- ¿Ves algún escollo?
- ¿Tiene alguna idea para mejorar la implementación, en el sentido de menos líneas de código?
¿Por qué no pones parámetros "necesarios" en el constructor de constructores?
public class Complex
{
....
public static class ComplexBuilder
{
// Required parameters
private final int required;
// Optional parameters
private int optional = 0;
public ComplexBuilder( int required )
{
this.required = required;
}
public Builder setOptional(int optional)
{
this.optional = optional;
}
}
...
}
Este patrón se describe en Java efectivo .
El patrón de construcción generalmente se usa cuando tienes muchos parámetros opcionales. Si encuentra que necesita muchos parámetros necesarios, considere estas opciones primero:
- Tu clase podría estar haciendo demasiado. Verifique que no viola el Principio de Responsabilidad Individual . Pregúntese por qué necesita una clase con tantas variables de instancia requeridas.
- Tu constructor podría estar haciendo demasiado . El trabajo de un constructor es construir. (No se volvieron muy creativos cuando lo nombraron; D) Al igual que las clases, los métodos tienen un Principio de Responsabilidad Individual. Si su constructor está haciendo algo más que la asignación de campo, necesita una buena razón para justificar eso. Es posible que necesite un Método de fábrica en lugar de un Constructor.
- Tus parámetros pueden estar haciendo muy poco . Pregúntese si sus parámetros se pueden agrupar en una estructura pequeña (o un objeto similar a una estructura en el caso de Java). No tengas miedo de hacer pequeñas clases. Si encuentra que necesita hacer una estructura o clase pequeña, no olvide volver a configurar la functionality que pertenece a la estructura en lugar de su clase más grande.
El patrón de constructor tradicional ya maneja esto: simplemente tome los parámetros obligatorios en el constructor. Por supuesto, nada impide que una persona que llama pase nulo, pero tampoco lo hace su método.
El gran problema que veo con su método es que o bien tiene una explosión combinatoria de clases con el número de parámetros obligatorios, o bien le obliga al usuario a establecer los parámetros en un sqeuence particular, lo cual es molesto.
Además, es mucho trabajo adicional.
En lugar de usar múltiples clases, solo usaría una clase y múltiples interfaces. Aplica su sintaxis sin requerir tanta escritura. También le permite ver todos los códigos relacionados juntos, lo que hace que sea más fácil entender qué está sucediendo con su código a un nivel mayor.
En mi humilde opinión, esto parece hinchado. Si tiene que tener todos los parámetros, páselos al constructor.
Lo he visto / usado esto:
new ComplexBuilder(requiredvarA, requiedVarB).optional(foo).optional(bar).build();
Luego pasa esto a tu objeto que los requiere.
No, no es nuevo. Lo que está haciendo allí es crear una especie de DSL extendiendo el patrón de generador estándar para admitir sucursales, que es, entre otras cosas, una forma excelente de asegurarse de que el generador no produzca un conjunto de configuraciones conflictivas para el objeto real.
Personalmente, creo que esta es una gran extensión para el patrón de construcción y puedes hacer todo tipo de cosas interesantes con ella, por ejemplo, en el trabajo tenemos constructores de DSL para algunas de nuestras pruebas de integridad de datos que nos permiten hacer cosas como assertMachine().usesElectricity().and().makesGrindingNoises().whenTurnedOn();
. OK, tal vez no sea el mejor ejemplo posible, pero creo que entiendes el punto.
Para obtener más información sobre cuándo utilizar el patrón de generador y sus ventajas , debe consultar mi publicación para obtener otra pregunta similar here
Pregunta 1: En cuanto al nombre del patrón, me gusta el nombre "Step Builder":
- http://rdafbn.blogspot.com/2012/07/step-builder-pattern_28.html
- http://www.javacodegeeks.com/2013/05/building-smart-builders.html
Pregunta 2/3: Con respecto a las trampas y recomendaciones, esto se siente demasiado complicado para la mayoría de las situaciones.
Estás aplicando una secuencia de cómo usas tu constructor, algo inusual en mi experiencia. Pude ver cómo esto sería importante en algunos casos, pero nunca lo he necesitado. Por ejemplo, no veo la necesidad de forzar una secuencia aquí:
Person.builder().firstName("John").lastName("Doe").build()
Person.builder().lastName("Doe").firstName("John").build()
Sin embargo, muchas veces el constructor necesitaba imponer algunas restricciones para evitar la creación de objetos falsos. Tal vez quiera asegurarse de que se proporcionen todos los campos obligatorios o que las combinaciones de campos sean válidas. Supongo que esta es la verdadera razón por la que desea introducir la secuenciación en el edificio.
En este caso, me gusta la recomendación de Joshua Bloch para hacer la validación en el método build (). Esto ayuda con la validación de campo cruzado porque todo está disponible en este punto. Vea esta respuesta: https://softwareengineering.stackexchange.com/a/241320
En resumen, no agregaría ninguna complicación al código solo porque le preocupa "perder" una llamada a un método de creación. En la práctica, esto se capta fácilmente con un caso de prueba. Tal vez comience con un generador de vanilla y luego presente esto si sigue recibiendo mordiscos por las llamadas a métodos faltantes.
public class Complex {
private final String first;
private final String second;
private final String third;
public static class False {}
public static class True {}
public static class Builder<Has1,Has2,Has3> {
private String first;
private String second;
private String third;
private Builder() {}
public static Builder<False,False,False> create() {
return new Builder<>();
}
public Builder<True,Has2,Has3> setFirst(String first) {
this.first = first;
return (Builder<True,Has2,Has3>)this;
}
public Builder<Has1,True,Has3> setSecond(String second) {
this.second = second;
return (Builder<Has1,True,Has3>)this;
}
public Builder<Has1,Has2,True> setThird(String third) {
this.third = third;
return (Builder<Has1,Has2,True>)this;
}
}
public Complex(Builder<True,True,True> builder) {
first = builder.first;
second = builder.second;
third = builder.third;
}
public static void test() {
// Compile Error!
Complex c1 = new Complex(Complex.Builder.create().setFirst("1").setSecond("2"));
// Compile Error!
Complex c2 = new Complex(Complex.Builder.create().setFirst("1").setThird("3"));
// Works!, all params supplied.
Complex c3 = new Complex(Complex.Builder.create().setFirst("1").setSecond("2").setThird("3"));
}
}