Al implementar el Patrón de construcción, a menudo me confundo con cuándo dejar que falle el edificio e incluso me las arreglo para tomar diferentes posiciones sobre el asunto cada pocos días.
Primero alguna explicación:
- Si falla temprano, quiero decir que la construcción de un objeto debería fallar tan pronto como se pase un parámetro no válido. Entonces dentro del
SomeObjectBuilder
. - Al fallar tarde quiero decir que construir un objeto solo puede fallar en la
build()
llamada que implícitamente llama a un constructor del objeto a construir.
Entonces algunos argumentos:
- A favor de fallar tarde: una clase de constructor no debe ser más que una clase que simplemente contiene valores. Además, conduce a una menor duplicación de código.
- A favor de fallar temprano: un enfoque general en la programación de software es que desea detectar problemas lo antes posible y, por lo tanto, el lugar más lógico para verificar sería en la clase de constructor 'constructor', 'setters' y, en última instancia, en el método de compilación.
¿Cuál es el consenso general sobre esto?
java
design-patterns
skiwi
fuente
fuente
null
objeto cuando haya un problemabuild()
.Respuestas:
Veamos las opciones, donde podemos colocar el código de validación:
build()
método.build()
método cuando se cree la entidad.La opción 1 nos permite detectar problemas antes, pero puede haber casos complicados en los que podemos validar la entrada solo teniendo el contexto completo, por lo tanto, haciendo al menos parte de la validación en el
build()
método. Por lo tanto, elegir la opción 1 conducirá a un código inconsistente con parte de la validación realizada en un lugar y otra parte realizada en otro lugar.La opción 2 no es significativamente peor que la opción 1, porque, por lo general, los configuradores en el generador se invocan justo antes que
build()
, especialmente, en las interfaces fluidas. Por lo tanto, todavía es posible detectar un problema lo suficientemente temprano en la mayoría de los casos. Sin embargo, si el generador no es la única forma de crear un objeto, dará lugar a la duplicación del código de validación, ya que deberá tenerlo en todas partes donde cree un objeto. La solución más lógica en este caso será colocar la validación lo más cerca posible del objeto creado, es decir, dentro de él. Y esta es la opción 3 .Desde el punto de vista SÓLIDO, poner la validación en el generador también viola SRP: la clase del generador ya tiene la responsabilidad de agregar los datos para construir un objeto. La validación es establecer contratos en su propio estado interno, es una nueva responsabilidad verificar el estado de otro objeto.
Por lo tanto, desde mi punto de vista, no solo es mejor fallar tarde desde la perspectiva del diseño, sino que también es mejor fallar dentro de la entidad construida, en lugar de hacerlo en el propio constructor.
UPD: este comentario me recordó una posibilidad más, cuando la validación dentro del generador (opción 1 o 2) tiene sentido. Tiene sentido si el constructor tiene sus propios contratos en los objetos que está creando. Por ejemplo, suponga que tenemos un generador que construye una cadena con contenido específico, por ejemplo, una lista de rangos de números
1-2,3-4,5-6
. Este constructor puede tener un método comoaddRange(int min, int max)
. La cadena resultante no sabe nada sobre estos números, ni debería tener que saberlo. El propio constructor define el formato de la cadena y las restricciones en los números. Por lo tanto, el métodoaddRange(int,int)
debe validar los números de entrada y lanzar una excepción si max es menor que min.Dicho esto, la regla general será validar solo los contratos definidos por el propio constructor.
fuente
Dado que usa Java, considere la orientación autorizada y detallada proporcionada por Joshua Bloch en el artículo Creación y destrucción de objetos Java (la fuente en negrita en la cita a continuación es mía):
Nota según la explicación del editor en este artículo, los "elementos" en la cita anterior se refieren a las reglas presentadas en Effective Java, Second Edition .
El artículo no profundiza en explicar por qué esto se recomienda, pero si lo piensa, las razones son bastante evidentes. El consejo genérico para comprender esto se proporciona allí mismo en el artículo, en la explicación de cómo se conecta el concepto de constructor con el del constructor, y se espera que los invariantes de clase se verifiquen en el constructor, no en ningún otro código que pueda preceder / preparar su invocación.
Para una comprensión más concreta de por qué sería incorrecto verificar invariantes antes de invocar una construcción, considere un ejemplo popular de CarBuilder . Los métodos de construcción se pueden invocar en un orden arbitrario y, como resultado, uno no puede saber realmente si un parámetro en particular es válido hasta la construcción.
Considere que el auto deportivo no puede tener más de 2 asientos, ¿cómo podría uno saber si
setSeats(4)
está bien o no? Es solo en la construcción cuando uno puede saber con certeza sisetSportsCar()
se invocó o no, lo que significa si lanzarTooManySeatsException
o no.fuente
Los valores no válidos que no son válidos porque no se toleran deben darse a conocer de inmediato en mi opinión. En otras palabras, si acepta solo números positivos y se pasa un número negativo, no hay necesidad de esperar hasta que
build()
se llame. No consideraría estos los tipos de problemas que "esperarías" que sucedan, ya que es un requisito previo para comenzar con el método. En otras palabras, es probable que no dependa de la falla en establecer ciertos parámetros. Lo más probable es que presumas que los parámetros son correctos o que realices alguna comprobación tú mismo.Sin embargo, para problemas más complicados que no se validan tan fácilmente, es mejor que se den a conocer cuando llame
build()
. Un buen ejemplo de esto podría ser utilizar la información de conexión que proporciona para establecer una conexión a una base de datos. En este caso, aunque técnicamente podría verificar tales condiciones, ya no es intuitivo y solo complica su código. Desde mi punto de vista, estos también son los tipos de problemas que podrían suceder y que realmente no puedes anticipar hasta que lo pruebes. Es una especie de juego la diferencia entre una cadena con una expresión regular para ver si podía ser analizada como un int y simplemente tratando de analizarlo, el manejo de las excepciones posibles que pueden ocurrir como una consecuencia.En general, no me gusta lanzar excepciones al establecer parámetros, ya que significa tener que atrapar cualquier excepción lanzada, por lo que tiendo a favorecer la validación
build()
. Por esta razón, prefiero usar RuntimeException ya que nuevamente, los errores en los parámetros pasados generalmente no deberían suceder.Sin embargo, esta es más una mejor práctica que otra cosa. Espero haber respondido a su pregunta.
fuente
Hasta donde yo sé, la práctica general (no estoy seguro si hay consenso) es fallar tan pronto como sea posible descubrir un error. Esto también dificulta el mal uso involuntario de su API.
Si es un atributo trivial que se puede verificar en la entrada, como una capacidad o longitud que no debería ser negativa, entonces es mejor que falle inmediatamente. Retener el error aumenta la distancia entre el error y la retroalimentación, lo que hace que sea más difícil encontrar la fuente del problema.
Si tiene la desgracia de encontrarse en una situación en la que la validez de un atributo depende de otros, entonces tiene dos opciones:
build()
se llama más o menos.Como con la mayoría de las cosas, esta es una decisión tomada en un contexto. Si el contexto hace que resulte incómodo o complicado fallar temprano, se puede hacer una compensación para diferir los cheques a un momento posterior, pero la falla rápida debería ser la opción predeterminada.
fuente
unsigned
,@NonNull
etc.X
a un valor que no es válido dado el valor actual deY
, pero antes de llamar abuild()
setY
a un valor que seaX
válido.Shape
y el constructor tieneWithLeft
yWithRight
propiedades, y uno desea ajustar un constructor para construir un objeto en un lugar diferente, requiriendo queWithRight
se llame primero al mover un objeto a la derecha yWithLeft
al moverlo a la izquierda, agregaría una complejidad innecesaria en comparación con permitirWithLeft
establecer el borde izquierdo a la derecha del borde derecho anterior siempre que seWithRight
solucione el borde derecho antes de quebuild
se llame.La regla básica es "fallar temprano".
La regla un poco más avanzada es "fallar lo antes posible".
Si una propiedad es intrínsecamente inválida ...
... entonces lo rechazas de inmediato.
Otros casos pueden necesitar que los valores se verifiquen en combinación y podrían colocarse mejor en el método build ():
fuente