Pregunta

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 frecuente es lanzar una excepción, y en C ++ no hay una sola forma única de hacer esto (ok, no realmente en C # tampoco, lo sé).

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

Por ejemplo, supongamos que queremos un objeto Printer para Queue un trabajo de impresión de forma asincrónica. Para el usuario de Printer , esa operación solo puede tener éxito, porque el resultado de una cola asíncrona llega en otro momento. Por lo tanto, no hay 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 clase invariante está rota, lo que básicamente significa: un error. Esta condición no tiene necesariamente ningún 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, solo cuál es absolutamente el peor. Me gustaría escuchar sus puntos de vista sobre estos y también que comparte cualquiera de 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 uso: mejor rescate silencioso y defensivo 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.
}

Mis comentarios a los estilos:

  1. Creo que prefiero el segundo estilo, donde la falla no está oculta, siempre que una violación de acceso realmente cause un bloqueo.
  2. Si no es 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 personas que lo prefieren en el código de producción, porque crea la ilusión de un software robusto que no falla (las funciones simplemente dejarán de funcionar) , como en la cola del objeto roto Printer ).

¿Prefieres alguno de estos o tienes otras formas de lograrlo?

¿Fue útil?

Solución

La pregunta se considera mejor en combinación con la forma en que prueba su software.

Es importante que golpear a un invariante roto durante la prueba se presente como un error de alta gravedad, tal como lo sería un bloqueo. Se pueden hacer compilaciones para pruebas durante el desarrollo para detener los diagnósticos muertos y de salida.

Puede ser apropiado agregar un código defensivo, como su estilo 3: su DebugBreak volcaría los diagnósticos en las compilaciones de prueba, pero solo sería un punto de quiebre para los desarrolladores. Esto hace menos probable la situación en la que un desarrollador no puede trabajar por un error en un código no relacionado.

Lamentablemente, a menudo lo he visto al revés, donde los desarrolladores obtienen todos los inconvenientes, pero las construcciones de prueba navegan a través de invariantes rotos. Se archivan muchos errores de comportamiento extraños, donde de hecho un solo error es la causa.

Otros consejos

Puede usar una técnica llamada NVI ( Interfaz no virtual ) junto con el patrón template method . Probablemente así es como lo haría (por supuesto, es solo mi opinión personal, que es realmente 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 a que 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 desee verificar.

Si es una invariante interna

  

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

Es simplemente una precondición de un método

  

Si es simplemente una precondición que   el usuario de la clase tendría que   garantía (digamos, solo imprimir después   la impresora está lista), lanzaría    std :: logic_error como se muestra arriba.

Realmente desalentaría verificar una condición, pero luego no hacer nada.


El usuario de la clase podría afirmar antes de llamar a un método que sus condiciones previas se cumplen. Por lo tanto, en general, si una clase es responsable de algún estado y encuentra que un estado no es válido, debería afirmarse. Si la clase encuentra una condición para ser violada que no cae dentro de su responsabilidad, debe lanzar.

Es una buena pregunta 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 de 'registro de errores' o verificar explícitamente el resultado de cualquier acción. Tal vez hay incluso otras estrategias, ese no es el punto.

Dependiendo de un bloqueo 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, todavía tiene datos corruptos.

La solución Interfaz no virtual de litb es una forma ordenada de controlar invariantes.

Pregunta difícil esta :)

Personalmente, tiendo a lanzar una excepción, ya que generalmente estoy demasiado involucrado en lo que estoy haciendo al implementar cosas para encargarme de lo que debe ocuparse en su diseño. Por lo general, esto vuelve y me muerde más tarde ...

Mi experiencia personal con la estrategia "Hacer algo de registro y luego no hacer nada más" es que también vuelve a morderte, especialmente si se implementa como en tu caso (sin estrategia global, cada clase podría 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 global de errores. Lo que hará el manejo dependerá de su producto (no desea simplemente no hacer nada y registrar algo en un archivo sutil de mentalidad de desarrollador en un sistema de controlador de tráfico aéreo, pero funcionaría bien si estuviera haciendo un controlador para, digamos, una impresora :)).

Supongo que lo que estoy diciendo es que, en mi opinión, esta pregunta es algo que debe resolver a nivel de diseño de su aplicación en lugar de a nivel de implementación. - Y lamentablemente no hay soluciones mágicas :(

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top