¿Debe un getter lanzar una excepción si su objeto tiene un estado no válido?

55

A menudo me encuentro con este problema, especialmente en Java, incluso si creo que es un problema general de OOP. Es decir: plantear una excepción revela un problema de diseño.

Supongamos que tengo una clase que tiene un String namecampo y un String surnamecampo.

Luego usa esos campos para componer el nombre completo de una persona para mostrarlo en algún tipo de documento, digamos una factura.

public void String name;
public void String surname;

public String getCompleteName() {return name + " " + surname;}

public void displayCompleteNameOnInvoice() {
    String completeName = getCompleteName();
    //do something with it....
}

Ahora quiero fortalecer el comportamiento de mi clase arrojando un error si displayCompleteNameOnInvoicese llama antes de que se haya asignado el nombre. Parece una buena idea, ¿no?

Puedo agregar un código de excepción al getCompleteNamemétodo. Pero de esta manera estoy violando un contrato 'implícito' con el usuario de la clase; en general, los getters no deben lanzar excepciones si no se establecen sus valores. Ok, este no es un captador estándar ya que no devuelve un solo campo, pero desde el punto de vista del usuario, la distinción puede ser demasiado sutil para pensarlo.

O puedo lanzar la excepción desde adentro del displayCompleteNameOnInvoice. Pero para hacerlo debería probar directamente nameo surnamecampos y al hacerlo violaría la abstracción representada por getCompleteName. Es responsabilidad de este método verificar y crear el nombre completo. Incluso podría decidir, basando la decisión en otros datos, que en algunos casos es suficiente surname.

Así que la única posibilidad parece cambiar la semántica del método getCompleteNamea composeCompleteName, que sugiere un comportamiento más 'activa' y, con ella, la capacidad de lanzar una excepción.

¿Es esta la mejor solución de diseño? Siempre estoy buscando el mejor equilibrio entre simplicidad y corrección. ¿Hay una referencia de diseño para este problema?

AgostinoX
fuente
8
"en general, los captadores no deben lanzar excepciones si no se establecen sus valores". - ¿Qué se supone que deben hacer entonces?
Rotem
2
@scriptin Luego, siguiendo esa lógica, displayCompleteNameOnInvoicepuede lanzar una excepción si getCompleteNameregresa null, ¿no?
Rotem
2
@Rotem puede. La pregunta es sobre si debería.
scriptin
22
Sobre el tema, Falsehoods Programmers Believe About Names es una muy buena lectura de por qué "nombre" y "apellido" son conceptos muy limitados.
Lars Viklund el
99
Si su objeto puede tener un estado no válido, ya lo ha arruinado.
user253751

Respuestas:

151

No permita que su clase se construya sin asignar un nombre.

DeadMG
fuente
99
Es interesante pero una solución bastante radical. Muchas veces sus clases tienen que ser más fluidas, lo que permite estados que se convierten en un problema solo si se llaman ciertos métodos y de lo contrario terminará con una proliferación de clases pequeñas que requieren demasiada explicación para ser utilizadas.
AgostinoX
71
Esto no es un comentario. Si aplica el consejo en la respuesta, ya no tendrá el problema descrito. Es una respuesta.
DeadMG
14
@AgostinoX: solo debe hacer que sus clases sean fluidas si es absolutamente necesario. De lo contrario, deberían ser lo más invariables posible.
DeadMG
25
@gnat: haces un gran trabajo aquí cuestionando la calidad de cada pregunta y cada respuesta en PSE, pero a veces eres demasiado en mi humilde opinión.
Doc Brown
99
Si pueden ser válidamente opcionales, no hay razón para que él lance en primer lugar.
DeadMG
75

La respuesta proporcionada por DeadMG prácticamente lo clava, pero permítanme expresarlo de manera un poco diferente: evite tener objetos con un estado no válido. Un objeto debe ser "completo" en el contexto de la tarea que cumple. O no debería existir.

Entonces, si quieres fluidez, usa algo como el Patrón de construcción . O cualquier otra cosa que sea una reificación separada de un objeto en construcción en oposición a la "cosa real", donde se garantiza que este último tiene un estado válido y es el que realmente expone las operaciones definidas en ese estado (por ejemplo getCompleteName).

back2dos
fuente
77
So if you want fluidity, use something like the builder pattern- fluidez o inmutabilidad (a menos que sea de alguna manera). Si uno quiere crear objetos inmutables, pero necesita diferir la configuración de algunos valores, entonces el patrón a seguir es establecerlos uno por uno en un objeto generador y solo crear uno inmutable una vez que hayamos reunido todos los valores necesarios para el correcto estado ...
Konrad Morawski
1
@ KonradMorawski: Estoy confundido. ¿Estás aclarando o contradiciendo mi declaración? ;)
back2dos
3
Traté de complementarlo ...
Konrad Morawski
40

No tenga un objeto con un estado no válido.

El propósito de un constructor o constructor es establecer el estado del objeto en algo que sea consistente y utilizable. Sin esta garantía, surgen problemas con un estado inicializado incorrectamente en los objetos. Este problema se agrava si se trata de concurrencia y algo puede acceder al objeto antes de que haya terminado de configurarlo.

Entonces, la pregunta para esta parte es "¿por qué estás permitiendo que el nombre o apellido sea nulo?" Si eso no es algo válido en la clase para que funcione correctamente, no lo permita. Tenga un constructor o constructor que valide adecuadamente la creación del objeto y, si no es correcto, plantee el problema en ese punto. También puede utilizar una de las @NotNullanotaciones que existen para ayudar a comunicar que "esto no puede ser nulo" y aplicarla en la codificación y el análisis.

Con esta garantía, es mucho más fácil razonar sobre el código, lo que hace, y no tener que lanzar excepciones en lugares extraños o poner controles excesivos alrededor de las funciones getter.

Getters que hacen más.

Hay bastante por ahí sobre este tema. ¿Tienes cuánta lógica en Getters y qué debería permitirse dentro de getters y setters? desde aquí y getters y setters que realizan lógica adicional en Stack Overflow. Este es un problema que surge una y otra vez en el diseño de clase.

El núcleo de esto proviene de:

en general, los captadores no deben lanzar excepciones si no se establecen sus valores

Y usted tiene razón. No deberían. Deberían parecer "tontos" al resto del mundo y hacer lo que se espera. Poner demasiada lógica allí conduce a problemas en los que se viola la ley del menor asombro. Y admitámoslo, realmente no quieres envolver a los captadores con un try catchporque sabemos cuánto nos encanta hacer eso en general.

También hay situaciones en las que debe usar un getFoométodo como el marco JavaBeans y cuando tiene algo de llamadas EL que espera obtener un bean (para que <% bar.foo %>realmente llame getFoo()a la clase, dejando a un lado el 'debería el bean hacer la composición o debería que se deje a la vista? 'porque uno puede llegar fácilmente a situaciones en las que uno u otro claramente puede ser la respuesta correcta)

Tenga en cuenta también que es posible que un getter determinado se especifique en una interfaz o que haya sido parte de la API pública expuesta anteriormente para la clase que se está refactorizando (una versión anterior solo tenía 'completeName' que se devolvió y se rompió una refactorización en dos campos).

Al final del día...

Haz lo que sea más fácil de razonar. Pasará más tiempo manteniendo este código del que dedicará a diseñarlo (aunque cuanto menos tiempo dedique a diseñar, más tiempo dedicará a mantenerlo). La elección ideal es diseñarlo de tal manera que no lleve tanto tiempo mantenerlo, pero tampoco te quedes ahí pensando durante días, a menos que esta sea realmente una elección de diseño que tendrá días de implicaciones más adelante .

Intentar mantener la pureza semántica del ser captador private T foo; T getFoo() { return foo; }te meterá en problemas en algún momento. A veces, el código simplemente no se ajusta a ese modelo y las contorsiones que uno atraviesa para tratar de mantenerlo así simplemente no tienen sentido ... y, en última instancia, hacen que sea más difícil de diseñar.

Acepte a veces que los ideales del diseño no pueden realizarse de la forma en que los quiere en el código. Las pautas son pautas, no grilletes.

Comunidad
fuente
2
Obviamente, mi ejemplo fue solo una excusa para mejorar el estilo de codificación razonando, no un problema de bloqueo. Pero estoy bastante perplejo por la importancia que usted y otros parecen dar a los constructores. En teoría, mantienen su promesa. En la práctica, se vuelve engorroso mantener la herencia, no se puede usar cuando extrae una interfaz de una clase, no adoptada por JavaBeans y otros marcos como Spring si no me estoy equivocando. Bajo el paraguas de una idea de 'clase' viven muchas categorías diferentes de objetos. Un objeto de valor puede tener un constructor de validación, pero este es solo un tipo.
AgostinoX
2
Poder razonar sobre el código es clave para poder escribir código usando una API, o poder mantener el código. Si una clase tiene un constructor que le permite estar en un estado no válido, hace que sea mucho más difícil pensar "bueno, ¿qué hace esto?" porque ahora debe considerar más situaciones de las que el diseñador de la clase consideró o pretendió. Esta carga conceptual adicional viene con la penalización de más errores y un mayor tiempo para escribir código. Por otro lado, si puede mirar el código y decir "nombre y apellido nunca serán nulos", será mucho más fácil escribir código usándolos.
2
Incompleto no necesariamente significa inválido. Una factura sin recibo puede estar en progreso, y la API pública que presenta reflejaría que ese es un estado válido. Si surnameo nameser nulo es un estado válido para el objeto, trátelo en consecuencia. Pero si no es un estado válido, lo que hace que los métodos dentro de la clase arrojen excepciones, se debe evitar que se encuentre en ese estado desde el punto en que se inicializó. Puede pasar objetos incompletos, pero pasar los que no son válidos es problemático. Si un apellido nulo es excepcional, intente evitarlo más temprano que tarde.
2
Una publicación de blog relacionada para leer: Diseño de inicialización de objetos y tenga en cuenta los cuatro enfoques para inicializar una variable de instancia. Una de ellas es permitir que esté en un estado no válido, aunque tenga en cuenta que arroja una excepción. Si está tratando de evitar esto, ese enfoque no es válido y deberá trabajar con los otros enfoques, lo que implica garantizar un objeto válido en todo momento. Del blog: "Los objetos que pueden tener estados inválidos son más difíciles de usar y de entender que los que siempre son válidos". Y esa es la clave.
55
"Haz lo que sea más fácil de razonar". Eso
Ben
5

No soy un chico de Java, pero parece cumplir con las dos restricciones que presentaste.

getCompleteNameno lanza una excepción si los nombres no están inicializados, y lo displayCompleteNameOnInvoicehace.

public String getCompleteName()
{
    if (name == null || surname == null)
    {
        return null;
    }
    return name + " " + surname;
}

public void displayCompleteNameOnInvoice() 
{
    String completeName = getCompleteName();
    if (completeName == null)
    {
        //throw an exception.
    }
    //do something with it....
}
Rotem
fuente
Para ampliar por qué esta es una solución correcta: de esta manera, la persona que llama getCompleteName()no tiene que preocuparse si la propiedad está realmente almacenada o si se calcula sobre la marcha.
Bart van Ingen Schenau
18
Para ampliar por qué esta solución es una locura, debes verificar la nulidad en cada llamada a getCompleteName, lo cual es horrible.
DeadMG
No, @DeadMG, solo tiene que verificarlo en los casos en que se deba lanzar una excepción si getCompleteName devuelve nulo. Cuál es la forma en que debería ser. Esta solución es correcta.
Dawood dice que reinstale a Mónica el
1
@DeadMG Sí, pero no sabemos de qué otros usos hay getCompleteName()en el resto de la clase, ni siquiera en otras partes del programa. En algunos casos, regresar nullpuede ser exactamente lo que se requiere. Pero si bien hay UN método cuya especificación es "lanzar una excepción en este caso", eso es EXACTAMENTE lo que debemos codificar.
Dawood dice que reinstale a Mónica el
44
Puede haber situaciones en las que esta sea la mejor solución, pero no puedo imaginar ninguna. Para la mayoría de los casos, esto parece demasiado complicado: un método arroja datos incompletos, otro devuelve nulo. Me temo que es probable que las personas que llaman usen incorrectamente.
sleske
4

Parece que nadie está respondiendo tu pregunta.
A diferencia de lo que a la gente le gusta creer, "evitar objetos no válidos" no suele ser una solución práctica.

La respuesta es:

Si deberia.

Ejemplo fácil: FileStream.Lengthen C # /. NET , que arroja NotSupportedExceptionsi el archivo no es buscable.

Mehrdad
fuente
Los comentarios no son para discusión extendida; Esta conversación se ha movido al chat .
maple_shaft
1

Tienes todo el nombre del cheque al revés.

getCompleteName()no tiene que saber qué funciones lo utilizarán. Por lo tanto, no tener un namecuando llama displayCompleteNameOnInvoice()no getCompleteName()es responsabilidad de usted.

Pero, namees un requisito previo claro para displayCompleteNameOnInvoice(). Por lo tanto, debe asumir la responsabilidad de verificar el estado (o debe delegar la responsabilidad de verificar a otro método, si lo prefiere).

sampathsris
fuente