Использование событий, а не исключений для реализации обработки ошибок

Я работаю над некоторым кодом, который использует шаблон в своей области бизнеса и данных, который использует события для сигнализации ошибок, например.

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

Взгляните на этот пост Уди Дахана. Его элегантный подход для отправки доменных событий. В предыдущем плакате указано, что вы не должны использовать механизм событий для восстановления от фатальных ошибок, но это очень полезный шаблон для уведомления в слабосвязанных системах:

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
}

Ответ 2

Мне кажется странным. Есть несколько преимуществ - например, использование нескольких "обработчиков", но семантика существенно отличается от обычной обработки ошибок. В частности, тот факт, что он автоматически не распространяется на стек, касается меня - если сами обработчики ошибок не генерируют исключение, логика будет продолжаться, как если бы все было в порядке, когда это, вероятно, должно было прервать текущую операцию.

Еще один способ подумать об этом: предположим, что метод предназначен для возврата значения, но вы обнаружили ошибку раньше. Какую ценность вы возвращаете? Исключения сообщают, что нет подходящего значения для возврата...

Ответ 3

Если вы думаете в терминах "Ошибки" и "Предупреждения", мне было очень повезло при резервировании событий для категории "Предупреждение" и "Исключения для категории" Ошибки ".

Обоснование здесь состоит в том, что события являются необязательными. Никто не держит пистолет в голову, заставляя вас обращаться с ними. Вероятно, это хорошо для предупреждений, но когда у вас есть настоящие ошибки, вы хотите убедиться, что они воспринимаются более серьезно. Исключения должны быть обработаны, или они будут пузыриться и создать неприятное сообщение для пользователя.

Что касается вашего вопроса Big Object: вы определенно не проходите мимо больших объектов, но это не значит, что вы не можете передавать ссылки на большие объекты. Там есть много возможностей в этом.

Как добавление, ничего не останавливается от создания события в дополнение к исключению, но опять же: если у вас есть подлинная ошибка, вы хотите что-то заставить заставить клиентский разработчик обработать его.

Ответ 4

Это выглядит странно для меня, во-первых, IDisposable - ваш друг, используйте его.

Если вы имеете дело с ошибками и исключительными ситуациями, вы должны использовать исключения, а не события, поскольку их гораздо проще понять, отлаживать и кодировать.

Так что это должно быть

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

Ответ 5

1), это необходимо? нет рисунка абсолютно необходимо

2) Windows Workflow Foundation делает это со всеми результатами из экземпляров Workflow, запущенных внутри размещенной среды выполнения. Просто помните, что исключения могут возникать при попытке поднять это событие, и вы можете захотеть сделать свой код очистки на блоке Dispose или finally в зависимости от ситуации, чтобы убедиться, что он работает.

Ответ 6

Если честно, события, сигнализирующие об ошибках, кажутся мне страшными.

Там есть несогласие между лагерями вокруг возвращающихся кодов состояния и исключениями броска. Чтобы упростить (значительно): лагерь состояния кода говорит, что бросание исключений места обнаружения и обработки ошибки слишком далеко от кода, вызывающего ошибку. Шаблон исключений исключений говорит, что пользователи забывают проверять коды состояния, а исключения обеспечивают выполнение обработки ошибок.

Ошибки как события кажутся худшими из обоих подходов. Очистка ошибок полностью отделена от кода, вызывающего ошибку, и уведомление об ошибке является полностью добровольным. Уч.

Для меня, если метод не выполнял его неявный или явный контракт (он не делал того, что он должен был делать), исключением является подходящий ответ. В этом случае разумная высылка информации, необходимой для исключения, представляется разумной.

Ответ 7

Первым фрагментом должен быть

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

иначе вы не будете освобождать свои ресурсы, когда обработчик события выдает исключение.

Как сказал Майк Браун, во втором фрагменте есть проблема, если resource.FreeLotsOfMemory() помешает с содержимым resource.StatusProperty вместо установки его на null.

Ответ 8

У нас есть базовый объект Error и ErrorEvent, который мы используем с шаблоном команды в нашей структуре для обработки некритических ошибок (например, ошибок проверки). Подобно исключениям, люди могут прослушивать базовый ErrorEvent или более конкретный ErrorEvent.

Также существует значительная разница между двумя вашими фрагментами.

если resource.FreeLotsOfMemory() очищает значение StatusProperty, а не просто устанавливает его значение null, ваша временная переменная будет содержать недопустимый объект при создании и броске OddException.

Эмпирическое правило состоит в том, что Исключения должны выполняться только в ситуациях, не требующих восстановления. Я действительно хочу, чтобы С# поддерживал предложение Throws, единственное, что я действительно пропустил от Java.

Ответ 9

Еще одна серьезная проблема с этим подходом - проблемы concurrency.

При традиционной обработке ошибок блокировки будут освобождены, так как управление перемещает стек вызовов в обработчик ошибок контролируемым образом. В этой схеме все блокировки будут сохраняться при вызове события. Любая блокировка, которая возникает в обработчике ошибок (и вы можете ожидать, что некоторые из них будут регистрироваться) будут потенциальным источником блокировок.