2008-12-05 10 views
7

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.¿Cómo se valida el estado interno de un objeto?

Mi enfoque principal está en C++, ya que en C# la manera oficial y predominante es lanzar una excepción, y en C++ no es sólo una sola manera de hacer esto (bueno, no realmente en C# o bien, sé ese).

Tenga en cuenta que estoy no hablando de validación de parámetros de función, pero más como verificaciones de integridad invariante de clase.

Por ejemplo, supongamos que queremos un objeto Printer para Queue un trabajo de impresión de forma asíncrona. Para el usuario de 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 incorrecto, 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 lo hay, 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 utilizo - mejor fallar de una manera controlable que los 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 utilizo - mejor accidente incontrolable que los 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 Uso - mejor en silencio y en defensa defensiva que 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. 
} 

Mi comentario ts a los estilos:

  1. Creo que prefiero el segundo estilo, donde la falla no está oculta, siempre que una infracción de acceso realmente cause un bloqueo.
  2. Si no se trata de un puntero NULL involucrado en el invariante, entonces tiendo a inclinarme hacia el primer estilo.
  3. Realmente no me gusta el tercer estilo, ya que ocultará muchos errores, pero conozco a gente que lo prefiere 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 del objeto Printer roto).

¿Prefiere alguno de estos o tiene otras formas de lograrlo?

Respuesta

2

La pregunta se considera mejor en combinación con la forma de probar el software.

Es importante que golpear una invariante rota durante las pruebas se archive 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 un 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.

6

Puede utilizar una técnica llamada NVI (Interfaz no virtual) junto con el patrón template method.Esto probablemente es la forma en que lo haría (por supuesto, es sólo mi opinión personal, que de hecho es discutible):

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. 
} 

Debido Queue es no virtual, el invariante está siendo verificado si una clase derivada anula DoQueue de manejo especial.


Para sus opciones: Creo que depende de la condición que desea comprobar.

Si es una invariante interna

Si es un invariante, no debería ser posible para un usuario de la clase violarlo. A la clase le debe importar sobre su invariante. Por lo tanto, sería assert(CheckInvariant()); en tal caso.

Es simplemente una condición previa de un método

Si es simplemente una condición previa que el usuario de la clase tendría que garantía (por ejemplo, la impresión sólo después de la 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.

+0

En realidad, no estoy de acuerdo en que NVI sea una buena solución en el caso específico que expuse. Sería bueno si Printer fuera una clase base, pero añadiendo ese cableado antes la necesidad es obvia a menudo es en vano. Si veía la necesidad de derivar de la impresora, entonces me refactorizaría en ese momento. –

1

Es una pregunta excelente 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 fuerte es una mala idea: no se puede garantizar que la aplicación falle si no se conoce la causa de la violación invariable. En caso de que no lo haga, aún tiene datos corruptos.

La solución NonVirtual Interface de litb es una manera ordenada de comprobar invariantes.

1

Difícil pregunta éste :)

Personalmente, tiendo a lanzar una excepción ya que estoy por lo general demasiado en lo que estoy haciendo en la aplicación de cosas para cuidar de lo que debe ser atendido por tu diseño. Por lo general, esto vuelve y me muerde más adelante ...

Mi experiencia personal con la estrategia "Do-some-logging-and-then-don-t-do-anything-more" es que también regresa para morderte, especialmente si se implementa como en tu caso (sin una estrategia global, cada clase podría hacerlo de diferentes maneras).

Lo que haría, en cuanto descubra un problema como este, sería hablar con el resto de mi equipo y decirles que necesitamos algún tipo de gestión 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 debe resolver en un nivel de diseño de su aplicación en lugar de a nivel de implementación. - Y, por desgracia, no hay soluciones mágicas :(

Cuestiones relacionadas