使用事件而不是异常来实现错误处理

时间:2020-03-06 14:52:36  来源:igfitidea点击:

我正在研究一些在其业务和数据层中使用模式的代码,该模式使用事件来表示错误,例如

resource = AllocateLotsOfMemory();
if (SomeCondition())
{    
    OnOddError(new OddErrorEventArgs(resource.StatusProperty));
    resource.FreeLotsOfMemory();    
    return;
}

从表面上看这很奇怪,特别是因为调用此代码的代码需要挂接到事件中(有四个或者五个不同的事件!)。

开发人员告诉我,通过这种方式,他们可以在错误处理代码中引用分配的资源的属性,并负责在该层保留错误后进行清理。

这是有道理的。

另一种可能是这样的

resource = AllocateLotsOfMemory();
if (SomeCondition())
{   
    BigObject temporary = resource.StatusProperty;
    resource.FreeLotsOfMemory();
    throw new OddException(temporary);
}

我的问题是:

  • 由于在释放异常对象时会释放此" BigObject",因此我们需要这种模式吗?
  • 其他人有没有经历过这种模式?如果是这样,我们发现了什么陷阱?有什么优势?

谢谢!

解决方案

如果我们从"错误"和"警告"的角度来考虑,那么在为"警告"类别保留事件和为"错误"类别例外时,我很幸运。

这里的理由是事件是可选的。没有人拿着枪顶你的头。警告可能没关系,但是当我们遇到真正的错误时,我们想要确保对它们的重视程度更高。必须处理异常,否则异常会冒泡并为用户创建令人讨厌的消息。

关于大对象问题:我们绝对不会在周围传递大对象,但这并不意味着我们不能在周围传递对大对象的引用。这样做的能力非常强大。

作为附录,除了引发异常外,没有其他任何事情可以引发事件,但是如果发生真正的错误,我们希望通过某种方式来迫使客户端开发人员进行处理。

1)需要吗?没有图案是绝对必要的

2)Windows Workflow Foundation使用托管运行时内部运行的Workflow实例的所有结果来执行此操作。请记住,尝试引发该事件时可能会发生异常,并且我们可能希望根据情况在Dispose或者finally块上执行清理代码,以确保其运行。

我也觉得很奇怪。有一些优点,例如允许使用多个"处理程序",但其语义与常规错误处理显着不同。特别是,除非错误处理程序本身抛出异常,否则不会自动传播到堆栈上这一事实使我感到担忧,逻辑将继续进行下去,好像它应该中止当前操作时一切仍然正常。

考虑这种情况的另一种方法:假设该方法旨在返回一个值,但是我们已及早发现了错误。我们返回什么价值?异常传达了一个事实,即没有适当的值可以返回...

这对我来说真的很奇怪,首先IDisposable是朋友,请使用它。

如果我们要处理错误和特殊情况,则应该使用异常,而不是事件,因为异常要容易掌握,调试和编码。

所以应该是

using(var resource = AllocateLotsOfMemory())
{
   if(something_bad_happened) 
   {
     throw new SomeThingBadException();
   }
}

我们有一个基本的Error对象和ErrorEvent,我们将其与框架中的命令模式一起使用来处理非关键错误(例如验证错误)。像例外一样,人们可以侦听基本的ErrorEvent或者更具体的ErrorEvent。

两个摘要之间也存在显着差异。

如果resource.FreeLotsOfMemory()清除了StatusProperty值,而不仅仅是将其设置为null,则在创建并抛出OddException时,临时变量将持有无效的对象。

经验法则是,仅应在不可恢复的情况下引发异常。我真的希望C支持一个Throws子句,这是我真正想念的唯一的Java东西。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。

老实说,发生错误的事件使我感到震惊。

关于返回状态代码和引发异常,阵营之间存在分歧。为了简化(极大地):状态代码阵营说抛出异常会使检测和处理错误与导致错误的代码相距太远。抛出异常的上限表示用户忘记检查状态码,而异常会强制执行错误处理。

将错误作为事件似乎是两种方法中最糟糕的一种。错误清除与导致错误的代码完全分开,并且错误通知是完全自愿的。哎哟。

对我来说,如果该方法不能满足它的隐式或者显式约定(它没有执行应做的事情),则例外是适当的响应。在这种情况下,抛出异常所需的信息似乎是合理的。

看看Udi Dahan的这篇文章。它是调度域事件的一种优雅方法。上一个张贴者的说法是正确的,我们不应使用事件机制来从致命错误中恢复,但是对于松散耦合的系统中的通知而言,这是非常有用的模式:

public class DomainEventStorage<ActionType>
{
    public List<ActionType> Actions
    {
        get
        {
            var k = string.Format("Domain.Event.DomainEvent.{0}.{1}",
                                  GetType().Name,
                                  GetType().GetGenericArguments()[0]);
            if (Local.Data[k] == null)
                Local.Data[k] = new List<ActionType>();

            return (List<ActionType>) Local.Data[k];
        }
    }

    public IDisposable Register(ActionType callback)
    {
        Actions.Add(callback);
        return new DomainEventRegistrationRemover(() => Actions.Remove(callback)
            );
    }
}

public class DomainEvent<T1> : IDomainEvent where T1 : class
{
    private readonly DomainEventStorage<Action<T1>> _impl = new DomainEventStorage<Action<T1>>();

    internal List<Action<T1>> Actions { get { return _impl.Actions; } }

    public IDisposable Register(Action<T1> callback)
    {
        return _impl.Register(callback);
    }

    public void Raise(T1 args)
    {
        foreach (var action in Actions)
        {
            action.Invoke(args);
        }
    }
}

并消耗:

var fail = false;
using(var ev = DomainErrors.SomethingHappened.Register(c => fail = true) 
{
   //Do something with your domain here
}

这种方法的另一个主要问题是并发性问题。

使用传统的错误处理,当控件以受控方式将调用堆栈移至错误处理程序时,将释放锁定。在此方案中,在调用事件时,所有锁仍将保留。错误处理程序中发生的任何阻塞(如果有日志记录,我们可能会希望得到一些阻塞)将是潜在的死锁源。

第一个片段可能应该是

resource = AllocateLotsOfMemory();
if (SomeCondition())
{
    try
    {
        OnOddError(new OddErrorEventArgs(resource.StatusProperty));
        return;
    }
    finally
    {
        resource.FreeLotsOfMemory();
    }
}

否则,当事件处理程序引发异常时,我们将不会释放资源。

正如Mike Brown所说,如果resource.FreeLotsOfMemory()resource.StatusProperty的内容混淆而不是将其设置为'null',则第二个片段也有问题。