Patrón del generador: ¿Cuándo fallar?

44

Al implementar el patrón de creación, a menudo me encuentro confundido con cuándo dejar que falle la construcción e incluso logro tomar posiciones diferentes al respecto cada pocos días.

Primero alguna explicación:

  • Con fallar antes quiero decir que la creación de un objeto debería fallar tan pronto como se pase un parámetro no válido. Por lo tanto, dentro de SomeObjectBuilder .
  • Con fallar tarde quiero decir que la creación de un objeto solo puede fallar en la llamada build() que llama implícitamente 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, lleva a menos duplicación de código.
  • A favor de fallar antes de tiempo: un enfoque general en la programación de software es que usted desea detectar los problemas lo antes posible y, por lo tanto, el lugar más lógico para verificar sería en la clase de constructor 'constructores', 'definidores' y finalmente en el método de construcción.

¿Cuál es el consenso general sobre esto?

    
pregunta skiwi 28.05.2014 - 13:43

5 respuestas

34

Veamos las opciones, donde podemos colocar el código de validación:

  1. Dentro de los setters en el constructor.
  2. Dentro del método build() .
  3. Dentro de la entidad construida: se invocará en el método build() 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 con el contexto completo, por lo tanto, realizar al menos parte de la validación en el método build() . Por lo tanto, la elección de la opción 1 conducirá a un código inconsistente con parte de la validación que se realiza en un lugar y otra parte que se realiza en otro lugar.

La opción 2 no es significativamente peor que la opción 1 porque, generalmente, los configuradores en el constructor se invocan justo antes del build() , especialmente en las interfaces fluidas. Por lo tanto, aún 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, porque 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 de SOLID, poner la validación en el constructor también viola el SRP: la clase de constructor 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 constructor (opción 1 o 2) tiene sentido. Tiene sentido si el constructor tiene sus propios contratos sobre los objetos que está creando. Por ejemplo, supongamos que tenemos un constructor que construye una cadena con contenido específico, por ejemplo, la lista de rangos de números 1-2,3-4,5-6 . Este constructor puede tener un método como addRange(int min, int max) . La cadena resultante no sabe nada acerca de 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étodo addRange(int,int) debe validar los números de entrada y lanzar una excepción si el máximo es menor que el mínimo.

Dicho esto, la regla general será validar solo los contratos definidos por el propio constructor.

    
respondido por el Ivan Gammel 28.05.2014 - 15:49
32

Dado que usa Java, tenga en cuenta la guía 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 siguiente cita es mía):

  

Como un constructor, un constructor puede imponer invariantes en sus parámetros. El método de construcción puede comprobar estos invariantes. Es fundamental que se verifiquen después de copiar los parámetros del constructor al objeto, y que se verifiquen en los campos del objeto en lugar de en los campos del constructor (Elemento 39). Si se infringe cualquier invariante, el método de construcción debe lanzar un IllegalStateException (Elemento 60). El método de detalle de la excepción debe indicar qué invariante se viola (Artículo 63).

     

Otra forma de imponer invariantes que involucran múltiples parámetros es hacer que los métodos de establecimiento tomen grupos enteros de parámetros en los que deben mantenerse algunos invariantes. Si el invariante no está satisfecho, el método de establecimiento lanza un IllegalArgumentException . Esto tiene la ventaja de detectar la falla invariante tan pronto como se pasan los parámetros no válidos, en lugar de esperar a que se invoque la compilación.

Nota de acuerdo con explicación del editor en este artículo, los "elementos" en la cita anterior se refieren a las reglas presentadas en Java efectivo, Segunda edición .

El artículo no profundiza en explicar por qué se recomienda esto, pero si lo piensas, las razones son bastante evidentes. En el artículo se proporciona un consejo genérico sobre cómo comprender esto, en la explicación de cómo el concepto de constructor está conectado 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é es incorrecto verificar invariantes antes de invocar una construcción, considere un ejemplo popular de CarBuilder . Los métodos del generador pueden invocarse 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 los autos deportivos no pueden tener más de 2 asientos, ¿cómo se puede saber si setSeats(4) está bien o no? Es solo en la compilación cuando se puede saber con certeza si se invocó o no setSportsCar() , lo que significa si lanzar o no TooManySeatsException .

    
respondido por el gnat 28.05.2014 - 16:04
19

En mi opinión, los valores no válidos que no son válidos porque no se toleran deben darse a conocer inmediatamente. En otras palabras, si solo acepta números positivos y se pasa un número negativo, no es necesario que tenga que esperar hasta que se llame a build() . No consideraría que estos son los tipos de problemas que "espera" que ocurran, ya que es un requisito previo para llamar al método para comenzar. En otras palabras, no es probable que dependa de la falla en la configuración de ciertos parámetros. Es más probable que suponga que los parámetros son correctos o que usted mismo realizaría algunas comprobaciones.

Sin embargo, para problemas más complicados que no son tan fáciles de validar es mejor que se den a conocer al llamar a 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 dichas condiciones, ya no es intuitivo y solo complica su código. A mi modo de ver, estos son también los tipos de problemas que pueden suceder y que no puedes anticipar hasta que lo intentas. Es una especie de diferencia entre hacer coincidir una cadena con una expresión regular para ver si podría analizarse como un int y simplemente intentar analizarla, manejando cualquier posible excepción que pueda ocurrir como consecuencia.

Por lo general, no me gusta lanzar excepciones cuando configuro parámetros, ya que significa tener que capturar cualquier excepción, por lo que tiendo a favorecer la validación en build() . Por esta razón, prefiero utilizar RuntimeException, ya que, de nuevo, los errores en los parámetros pasados no deberían ocurrir en general.

Sin embargo, esto es más una buena práctica que cualquier otra cosa. Espero que responda a tu pregunta.

    
respondido por el Neil 28.05.2014 - 14:30
11

Por lo que sé, la práctica general (no estoy seguro de si hay consenso) es fallar tan pronto como sea posible descubrir un error. Esto también hace que sea más difícil el mal uso involuntario de su API.

Si es un atributo trivial que puede verificarse en la entrada, como una capacidad o longitud que debería ser no negativa, entonces es mejor fallar inmediatamente. Mantener 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 tienes la desgracia de estar en una situación en la que la validez de un atributo depende de otros, tienes dos opciones:

  • Requiere que ambos atributos (o más) se suministren simultáneamente (es decir, invocación de un solo método).
  • Valide la prueba tan pronto como sepa que no hay más cambios entrantes: cuando se llama a build() o menos.

Como con la mayoría de las cosas, esta es una decisión tomada en un contexto. Si el contexto hace que sea difícil o complicado fallar antes, se puede hacer una compensación para diferir los cheques a un momento posterior, pero el fallo debería ser el predeterminado.

    
respondido por el JvR 28.05.2014 - 14:43
0

La regla básica es "fallar antes".

La regla ligeramente más avanzada es "fallar lo antes posible".

Si una propiedad es intrínsecamente inválida ...

CarBuilder.numberOfWheels( -1 ). ...  

... entonces lo rechazas inmediatamente.

Otros casos podrían necesitar que se verifiquen los valores en combinación y podrían ubicarse mejor en el método build ():

CarBuilder.numberOfWheels( 0 ).type( 'Hovercraft' ). ...  
    
respondido por el Phill W. 19.07.2017 - 15:20

Lea otras preguntas en las etiquetas