类似事务的编程风格或明智的异常处理

时间:2009-06-16 09:32:36

标签: language-agnostic architecture exception-handling

抽象例子的问题

假设您有两种方法:DoJob1()DoJob2()。 它们中的每一个都具有类似事务的行为,即执行其工作或报告错误。

我应该如何编写一个执行DoJob1()然后DoJob2()的方法,但它本身就像事务一样,也就是说,保证DoJob1()执行的操作的回滚以防万一处理DoJob2()时出错?

当然,您可以自由选择错误处理方式(bool返回值,实例异常,全局错误变量 - 您可以命名)。

背景

这个想法是写(某些)类似事务的方法。当发生异常时,建议用户重复“交易”。

我想到了问题的可能解决方法,我将在一段时间内发布; (为了不限制你的想象力)

4 个答案:

答案 0 :(得分:1)

一般来说,我这样做:

transactionalJob1()
    transaction_begin()
    doJob1()
    transaction_end()
    exception:
        log
        transaction_rollback()

transactionalJob2()
    transaction_begin()
    doJob2()
    transaction_end()
    exception:
        log
        transaction_rollback()

transactionalJob1And2()
    transaction_begin()
    doJob1()
    doJob2()
    transaction_end()
    exception:
        transaction_rollback()

如果您选择的语言支持模板方法,您可以将其全部包装起来。

答案 1 :(得分:0)

我的想法

我有同样的想法。

这是一种beta方法(C#):

class Program
{
    Random rnd = new Random();

    void DoJob1()
    {
        if (rnd.NextDouble() <= 0.5)
            throw new ArgumentException("First case");

        Console.WriteLine("First job done.");
    }

    void DoJob1Rollback()
    {
        Console.WriteLine("First job rollback.");
    }

    void DoJob2()
    {
        if (rnd.NextDouble() <= 0.5)
            throw new ArgumentException("Second case");

        Console.WriteLine("Second job done.");
    }

    void DoJob2Rollback()
    {
        Console.WriteLine("Second job rollback.");
    }

    void Run()
    {
        bool success = false;

        while (true)
        {
            try
            {
                TransactHelper.DoTransaction(
                    DoJob1,
                    DoJob2
                );
                success = true;
            }
            catch (Exception ex)
            {
                Console.WriteLine("--------------------------------------");
                Console.WriteLine("Exception: " + ex.Message);
                // Console.WriteLine("Stack trace:");
                // Console.WriteLine(ex.StackTrace);
                // Console.WriteLine();
            }

            if (!success)
            {
                // ask the user for another chance
                Console.Write("Retry? ");
                if (Console.ReadLine().ToLower() != "yes")
                    break;
            }
            else
                break;
        }

        Console.WriteLine("Batch job {0}", success ? "succeeded." : "did not succeed.");

        Console.WriteLine("Press Enter to exit.");
        Console.ReadLine();
    }

    static void Main(string[] args)
    {
        (new Program()).Run();
    }
}

这看起来很不错,就像运行TransactHelper.DoTransaction

一样
class TransactHelper
{
    public static void DoTransaction(params ThreadStart[] actions)
    {
        int i = 0;
        int n = actions.Length;

        // exception to pass on
        Exception ret_ex = null;

        // do the list of jobs
        for (; i < n; ++i)
        {
            try
            {
                ThreadStart ts = actions[i];
                ts();
            }
            catch (Exception ex)    // register exception
            {
                ret_ex = ex;
                break;
            }
        }

        if (ret_ex != null)         // exception registered, rollback what's done
        {
            int k = i;              // method which failed
            for (; i >= 0; --i)
            {
                MethodInfo mi = actions[i].Method;
                string RollbackName = mi.Name + "Rollback";

                // set binding flags - the same as the method being called
                BindingFlags flags = (mi.IsStatic) ? BindingFlags.Static : BindingFlags.Instance;
                if (mi.IsPrivate)
                    flags |= BindingFlags.NonPublic;
                if (mi.IsPublic)
                    flags |= BindingFlags.Public;

                // call rollback
                MethodInfo miRollback = mi.DeclaringType.GetMethod(RollbackName, flags);
                miRollback.Invoke(actions[i].Target, new object[] { });
            }

            throw new TransactionException("Transaction failed on method " + actions[k].Method.Name, ret_ex);
        }
    }
}

[global::System.Serializable]
public class TransactionException : Exception
{
    public TransactionException() { }
    public TransactionException(string message) : base(message) { }
    public TransactionException(string message, Exception inner) : base(message, inner) { }
    protected TransactionException(
      System.Runtime.Serialization.SerializationInfo info,
      System.Runtime.Serialization.StreamingContext context)
        : base(info, context) { }
}

然而,存在陷阱。

  • 通过维持两种方法而不是一种方法来增加疼痛
  • 如果xRollback方法中发生异常怎么办? (但是,它与catch {}块中出现的情况相同)
  • (当前设计只运行没有参数的方法 - 这很容易扩展)

我最初考虑过这个问题,因为我在一个产生大量代码的团队中工作,如:

_field = new Field();
_field.A = 1;
_filed.B = "xxx";

而不是

Field tmp = new Field();
tmp.A = 1;
tmp.B = "xxx";

_field = tmp;

交易的美妙之处在于:

  • 数据完整性(!)
  • 您可以建议用户重复操作

P.S。

也许我正试图重新发明轮子? 是否有一个更明智的方法库? 我没有预见到这种设计的缺陷是什么?

答案 2 :(得分:0)

我采用的第二种方法是在单独的类中记录回滚操作。

class Transaction
{
    IList<Action> _rollBacks = new List<Action>();

    public void AddRollBack(Action action)
    {
        _rollBacks.Add(action);
    }

    public void Clear()
    {
        _rollBacks.Clear();
    }
    public void RollBack()
    {
        for (int i = _rollBacks.Count - 1; i >= 0; --i)
        {
            _rollBacks[i]();
            _rollBacks.RemoveAt(i);
        }
    }
}

实际代码是成对编写的,例如:

File.Move(file, file + "__");
string s = (string) file.Clone();
tr.AddRollBack(() => File.Move(s + "__", s));

当我需要做某事事务性时,我创建了一个事务对象,并将敏感代码覆盖到try ... catch ... finally

Transaction tr = new Transaction();

UpdateLogic ul = new UpdateLogic();
ul.Transaction = tr;

// ........ more initialization (of classes)

try
{
    // .... more initialization, which is a part of transaction
    ul.DoPreparation();
    ul.DoCopying();

    tr.Clear();                         // at this point most of update is ok

    ul.DoCleanup();

    ShowMessage("Update completed", "Update completed successfully.", false);
}
catch (Exception ex)
{
    // handel error
}
finally
{
    // show message in UI
    try
    {
        tr.RollBack();
    }
    catch (Exception ex)
    {
        ShowMessage("Error while performing rollback of actions", ex.Message, true);
    }

    // ... update is done        
}

非常方便,因为这可以保证文件的完整性。通过明智的编码,即使进程在执行操作或回滚期间被终止(但在文件系统中留下一些垃圾),也可以保持完整性。

答案 3 :(得分:0)

这取决于该方法应该做什么样的工作。通常,首先将事务的副作用存储到临时位置,然后当事务提交时,将副作用存储到一个原子操作中的永久位置。根据您是否正在修改文件系统中的某些内容(从一些书中读取数据库事务日志如何工作),一些内存数据结构,网络上的内容或其他内容,有关如何执行此操作的详细信息非常不同。 / p>

例如,我曾经编写了一个事务性内存中键值数据库(http://dimdwarf.sourceforge.net/的一部分)。它保留了在事务期间完成的所有修改的临时列表。然后,当事务提交时,修改的密钥被锁定在数据库中,修改存储在数据库中(此操作不得失败),之后密钥被解锁。一次只能提交一个事务,在事务完全提交之前,没有其他事务可以看到更改。