2012-06-11 14 views
9

Supongamos que tengo dos funciones DoTaskA y DoTaskB — ambos capaces de lanzar TaskException — con sus correspondientes funciones "rollback" UndoTaskA y UndoTaskB. ¿Cuál es el mejor patrón para usar para que ambos tengan éxito o ambos fallen?C++ patrón de transacción similar de Todo o Nada Trabajo

Lo mejor que tenemos ahora es

bool is_task_a_done = false, 
    is_task_b_done = false; 

try { 
    DoTaskA(); 
    is_task_a_done = true; 

    DoTaskB(); 
    is_task_b_done = true; 
} catch (TaskException &e) { 
    // Before rethrowing, undo any partial work. 
    if (is_task_b_done) { 
     UndoTaskB(); 
    } 
    if (is_task_a_done) { 
     UndoTaskA(); 
    } 
    throw; 
} 

Sé que is_task_b_done es innecesaria, pero tal vez bueno para mostrar código de simetría en caso añadimos una tercera o una cuarta tarea más adelante.

No me gusta este código debido a las variables booleanas auxiliares. Quizás hay algo en el nuevo C++ 11 del que no tengo conocimiento, ¿cuál puede codificarlo mejor?

+0

Si está dentro del bloque catch, 'is_task_b_done' siempre es' falso' – SuperSaiyan

+0

Lo sé, he agregado el comentario de que solo está ahí para la simetría del código en caso de que agreguemos una tercera o cuarta tarea. – kirakun

+0

¿Su aplicación tiene varios subprocesos? ¿Has echado un vistazo a [RAII] (http://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization)? – dirkgently

Respuesta

12

Un poco RAII cometer guardia de alcance/rollback podría tener este aspecto:

#include <utility> 
#include <functional> 

class CommitOrRollback 
{ 
    bool committed; 
    std::function<void()> rollback; 

public: 
    CommitOrRollback(std::function<void()> &&fail_handler) 
     : committed(false), 
      rollback(std::move(fail_handler)) 
    { 
    } 

    void commit() noexcept { committed = true; } 

    ~CommitOrRollback() 
    { 
     if (!committed) 
      rollback(); 
    } 
}; 

Por lo tanto, estamos asumiendo siempre vamos a crear el objeto de guardia después de la operación tiene éxito, y llamar a commit sólo después de todo las transacciones tuvieron éxito.

void complicated_task_a(); 
void complicated_task_b(); 

void rollback_a(); 
void rollback_b(); 

int main() 
{ 
    try { 
     complicated_task_a(); 
     // if this^throws, assume there is nothing to roll back 
     // ie, complicated_task_a is internally exception safe 
     CommitOrRollback taskA(rollback_a); 

     complicated_task_b(); 
     // if this^throws however, taskA will be destroyed and the 
     // destructor will invoke rollback_a 
     CommitOrRollback taskB(rollback_b); 


     // now we're done with everything that could throw, commit all 
     taskA.commit(); 
     taskB.commit(); 

     // when taskA and taskB go out of scope now, they won't roll back 
     return 0; 
    } catch(...) { 
     return 1; 
    } 
} 

PS.Como Anon Mail dice, es mejor insertar todos esos objetos taskX en un contenedor si tiene muchos de ellos, dándole al contenedor la misma semántica (confirmación de llamada en el contenedor para que confíe cada objeto guardián propiedad).


PPS. En principio, puede usar std::uncaught_exception en el dtor RAII en lugar de comprometerlo explícitamente. Prefiero comprometerme explícitamente aquí porque creo que es más claro, y también funciona correctamente si sale del alcance anticipadamente con un return FAILURE_CODE en lugar de una excepción.

+0

Respuesta aseada. Me gusta el comentario de que 'std :: uncaught_exception' se podría usar para evitar el' commit' que está algo separado del flujo del cuerpo principal, por lo que es fácil de olvidar. Nunca debemos usar paradigmas mixtos de manejo de errores de todos modos. – kirakun

0

Para la escalabilidad, desea guardar el hecho de que debe deshacer una tarea en un contenedor. Luego, en el bloque catch, solo invocará todos los inserciones que están registradas en el contenedor.

El contenedor puede, por ejemplo, contener objetos de función para deshacer la tarea que se ha completado con éxito.

0

¿Has pensado en CommandPattern? Command Pattern description

encapsular todos los datos que se necesita para hacer lo que DoTaskA() hace en un objeto de una clase de comando, con la ventaja, que puede revertir todo esto, si es necesario (por lo tanto no hay necesidad de tener una Deshacer especial si no se pudo ejecutar ). El patrón de comando es especialmente bueno para manejar situaciones de "todo o nada" .

Si tiene varios comandos que se acumulan unos sobre otros, como su ejemplo se puede leer, entonces usted debe investigar chain of responsibility

tal vez un patrón de reactor puede ser útil (reactor description here) esto invertirá el flujo de control, pero se siente natural y tiene el beneficio de de convertir su sistema en un fuerte diseño multiproceso y multicomponente. pero puede ser exagerado aquí, difícil de diferenciar del ejemplo.

7

Es difícil lograr consistencia de transacción en C++. Hay un buen método descrito usando el patrón ScopeGuard en el diario del Dr. Dobb. La belleza del enfoque es que esto requiere limpieza tanto en situaciones normales como en escenarios de excepción. Utiliza el hecho de que los destructores de objeto están garantizados para invocar cualquier salida de alcance y el caso de excepción es simplemente otra salida de ámbito.

+0

¿Quizás podría reproducir (o dibujar) el código en línea, por si acaso el Dr. Dobbs está inactivo cuando alguien está leyendo esta respuesta? – Useless

1

La mejor manera de lograr esto es con protectores de alcance, básicamente un pequeño modismo de RAII que invocará un controlador de reversión si se produce una excepción.

Hace un momento pregunté por una implementación simple de ScopeGuard y la pregunta evolucionó hacia una implementación agradable que estoy usando en mis proyectos de producción. Funciona con C++ 11 y lambdas como los manejadores de retrotracción.

mi fuente tiene en realidad dos versiones: una que invocará el controlador de retrotracción si el controlador del constructor lanza, y otra que no se lanzará si eso sucede.

verifique la fuente y ejemplos de uso in here.

Cuestiones relacionadas