c# - org - ¿Cómo se valida el estado interno de un objeto?
mapwindows 5 (4)
Me interesa saber qué técnica (s) está utilizando para validar el estado interno de un objeto durante una operación que, desde su propio punto de vista, solo puede fallar debido a un mal estado interno o una violación invariable.
Mi enfoque principal está en C ++, ya que en C # la forma oficial y prevalente es lanzar una excepción, y en C ++ no hay una sola forma de hacerlo (vale, no realmente en C # tampoco, lo sé).
Tenga en cuenta que no estoy hablando de la validación de parámetros de función, sino más bien como las verificaciones de integridad invariante de clase.
Por ejemplo, supongamos que queremos que un objeto de Printer
coloque en Queue
un trabajo de impresión de forma asíncrona. Para el usuario de la Printer
, esa operación solo puede tener éxito, porque un resultado de cola asíncrona llega en otro momento. Por lo tanto, no hay un código de error relevante para transmitir a la persona que llama.
Pero para el objeto Printer
, esta operación puede fallar si el estado interno es malo, es decir, la invariante de clase está rota, lo que básicamente significa: un error. Esta condición no es necesariamente de interés para el usuario del objeto Printer
.
Personalmente, tiendo a mezclar tres estilos de validación de estado interno y realmente no puedo decidir cuál es el mejor, si es que hay alguno, cuál es el peor. Me gustaría escuchar su opinión sobre esto y también que comparta sus propias experiencias y pensamientos sobre este asunto.
El primer estilo que uso: mejor falla de una manera controlable que datos corruptos:
void Printer::Queue(const PrintJob& job)
{
// Validate the state in both release and debug builds.
// Never proceed with the queuing in a bad state.
if(!IsValidState())
{
throw InvalidOperationException();
}
// Continue with queuing, parameter checking, etc.
// Internal state is guaranteed to be good.
}
El segundo estilo que uso: mejor bloqueo incontrolable que datos corruptos:
void Printer::Queue(const PrintJob& job)
{
// Validate the state in debug builds only.
// Break into the debugger in debug builds.
// Always proceed with the queuing, also in a bad state.
DebugAssert(IsValidState());
// Continue with queuing, parameter checking, etc.
// Generally, behavior is now undefined, because of bad internal state.
// But, specifically, this often means an access violation when
// a NULL pointer is dereferenced, or something similar, and that crash will
// generate a dump file that can be used to find the error cause during
// testing before shipping the product.
}
El tercer estilo que utilizo es el mejor rescate en silencio y defensa que los datos corruptos:
void Printer::Queue(const PrintJob& job)
{
// Validate the state in both release and debug builds.
// Break into the debugger in debug builds.
// Never proceed with the queuing in a bad state.
// This object will likely never again succeed in queuing anything.
if(!IsValidState())
{
DebugBreak();
return;
}
// Continue with defenestration.
// Internal state is guaranteed to be good.
}
Mis comentarios a los estilos:
- Creo que prefiero el segundo estilo, donde la falla no está oculta, siempre que una infracción de acceso cause un bloqueo.
- Si no es un puntero NULL involucrado en el invariante, entonces tiendo a inclinarme hacia el primer estilo.
- Realmente no me gusta el tercer estilo, ya que ocultará muchos errores, pero conozco a personas que lo prefieren en el código de producción, porque crea la ilusión de un software robusto que no se cuelga (las funciones simplemente se detienen para funcionar, como en la cola en el objeto
Printer
roto).
¿Prefieres alguno de estos o tienes otras formas de lograrlo?
La pregunta se considera mejor en combinación con la forma en que prueba su software.
Es importante que golpear una invariante rota durante la prueba se presente como un error de alta gravedad, al igual que un choque sería. Las compilaciones para las pruebas durante el desarrollo se pueden hacer para detenerse y emitir diagnósticos.
Puede ser apropiado agregar código defensivo, más bien como su estilo 3: su DebugBreak
arrojaría diagnósticos en compilaciones de prueba, pero sería un punto de quiebre para los desarrolladores. Esto hace menos probable que un error en un código no relacionado le impida trabajar a un desarrollador.
Tristemente, a menudo lo he visto al revés, donde los desarrolladores tienen todos los inconvenientes, pero las compilaciones de prueba navegan a través de invariantes rotos. Muchos errores de comportamiento extraños se archivan, donde de hecho un solo error es la causa.
Puede usar una técnica llamada NVI ( Non-Virtual-Interface ) junto con el template method
la template method
. Probablemente así es como lo haría (por supuesto, es solo mi opinión personal, lo que de hecho es debatible):
class Printer {
public:
// checks invariant, and calls the actual queuing
void Queue(const PrintJob&);
private:
virtual void DoQueue(const PringJob&);
};
void Printer::Queue(const PrintJob& job) // not virtual
{
// Validate the state in both release and debug builds.
// Never proceed with the queuing in a bad state.
if(!IsValidState()) {
throw std::logic_error("Printer not ready");
}
// call virtual method DoQueue which does the job
DoQueue(job);
}
void Printer::DoQueue(const PrintJob& job) // virtual
{
// Do the actual Queuing. State is guaranteed to be valid.
}
Como Queue
no es virtual, el invariante aún se verifica si una clase derivada anula DoQueue
para un manejo especial.
A sus opciones: creo que depende de la condición que desea verificar.
Si es una invariante interna
Si es un invariante, no debería ser posible que un usuario de su clase lo viole. La clase debería preocuparse por su invariante en sí misma. Por lo tanto,
assert(CheckInvariant());
En ese caso.
Es simplemente una precondición de un método
Si es simplemente una condición previa que el usuario de la clase debería garantizar (digamos, solo imprimiendo después de que la impresora esté lista), lanzaría
std::logic_error
como se muestra arriba.
Realmente desalentaría comprobar una condición, pero luego no hacer nada.
El usuario de la clase podría afirmar antes de llamar a un método que se cumplen las condiciones previas. Por lo tanto, en general, si una clase es responsable de algún estado y encuentra que un estado no es válido, debe afirmarlo. Si la clase encuentra una condición para ser violada que no cae en su responsabilidad, debe arrojar.
Difícil pregunta esto :)
Personalmente, tiendo a arrojar una excepción ya que usualmente estoy demasiado metido en lo que estoy haciendo cuando implemento cosas para encargarme de lo que su diseño debe cuidar. Usualmente esto regresa y me muerde más tarde ...
Mi experiencia personal con la estrategia "Do-some-logging-and-then-dont-do-anything-more" es que también vuelve para morderte, especialmente si se implementa como en tu caso (sin una estrategia global, cada clase podría potencialmente hacerlo de diferentes maneras).
Lo que haría, tan pronto como descubriera un problema como este, sería hablar con el resto de mi equipo y decirles que necesitamos algún tipo de manejo de error global. Lo que hará el manejo dependerá de su producto (no desea simplemente no hacer nada y registrar algo en un sutil archivo con mentalidad de desarrollador en un sistema de controlador de tráfico aéreo, pero funcionaría bien si estuviera haciendo un controlador para, por ejemplo, una impresora :)).
Supongo que lo que estoy diciendo es que, en verdad, esta pregunta es algo que debes resolver sobre el nivel de diseño de tu aplicación en lugar de a nivel de implementación. - Y lamentablemente no hay soluciones mágicas :(
Es una pregunta buena y muy relevante. En mi humilde opinión, cualquier arquitectura de aplicación debe proporcionar una estrategia para informar invariantes rotos. Uno puede decidir usar excepciones, usar un objeto ''registro de errores'' o comprobar explícitamente el resultado de cualquier acción. Tal vez haya incluso otras estrategias: ese no es el punto.
Dependiendo de un choque posiblemente alto es una mala idea: no puede garantizar que la aplicación se bloqueará si no conoce la causa de la violación invariante. En caso de que no lo haga, aún tiene datos corruptos.
La solución NonVirtual Interface de litb es una manera sencilla de verificar invariantes.