对于多条消息和 success/failure 是否有通知模式的替代方案?

Is there an alternative to the Notification pattern for multiple messages and success/failure?

对于多条消息和 success/failure 是否有通知模式的替代方案?

我有一个 class、OperationResult,我用它来 return 一个 Success 布尔值和一个 "error" 消息列表。这些消息有时是意想不到的错误,但更多的是经常发生的普通情况。有时我们 return 一条错误消息,但有时我们 return 多条错误消息。我希望找到更好的方法。

这好像差不多the Notification pattern advocated by Fowler。然后消费者对成功状态和错误做一些合理的事情,最常见的是向用户显示错误,但有时在非致命错误的情况下继续。

因此我有很多看起来像这样的服务方法(不是网络服务方法):

private ThingRepository _repository;
public OperationResult Update(MyThing thing)
{
    var result = new OperationResult() { Success = true };

    if (thing.Id == null) {
        result.AddError("Could not find ID of the thing update.");
        return result;
    }

    OtherThing original = _repository.GetOtherThing(thing.Id);
    if (original == null) return result;

    if (AnyPropertyDiffers(thing, original))
    {
        result.Merge(UpdateThing(thing, original));
    }

    if (result.Success) result.Merge(UpdateThingChildren(thing));
    if (!result.HasChanges) return result;

    OperationResult recalcResult = _repository.Recalculate(thing);

    if (recalcResult.Success) return result;

    result.AddErrors(recalcResult.Errors);
    return result;
}
private OperationResult UpdateThing(MyThing ) {...}
private OperationResult UpdateThingChildren(MyThing) {...}
private bool AnyPropertyDiffers(MyThing, OtherThing) {...}

如您所想,UpdateThingUpdateThingChildrenThingRepository.Recalculate 都有类似的 OperationResult merging/manipulating 代码与它们的业务逻辑交错。

有没有其他方法可以替代我的 returned 对象周围的大量代码?我希望我的代码只关注业务逻辑,而不必特别注意如何操作 OperationResult.

我希望代之以看起来像下面这样的代码,它能更好地表达其业务逻辑并减少消息处理:

public ??? Update(MyThing thing, ???)
{
    if (thing.Id == null) return ???;
    OtherThing original = _repository.GetOtherThing(thing.originalId);
    if (original == null) return ???;

    if (AnyPropertyDiffers(thing, original))
    {
        UpdateThing(thing, original));
    }

    UpdateThingChildren(thing);
    _repository.Recalculate(thing); 
    return ???;  
}

有什么想法吗?

注意:抛出异常在这里并不合适,因为消息并非异常。

有些人明白规律是不能被打破的。但是模式有很多批评者。而且真的有即兴创作的空间。即使您修改了一些细节,您仍然可以认为您已经实现了一个模式。模式是通用的东西,可以有具体的实现。

话虽如此,您可以选择不进行这种细粒度的操作结果解析。我一直提倡像这样的伪代码

class OperationResult<T>
{
    List<T> ResultData {get; set;}
    string Error {get; set;}
    bool Success {get; set;}
}

class Consumer<T>
{
    void Load(id)
    {
        OperationResult<T> res = _repository.GetSomeData<T>(id);
        if (!res.Success)
        {
            MessageBox.Show(DecorateMsg(res.Error));
            return;
        }
    }
}

如您所见,服务器代码 return 数据或错误消息。简单的。服务器代码执行所有日志记录,您可以将错误写入数据库,无论如何。但我认为将复杂的操作结果传递给消费者没有任何价值。消费者只需要知道,成功与否。另外,如果你在同一个操作中得到了多个东西,如果你首先得到的东西失败了,继续操作有什么意义?可能是您尝试一次做太多事情的问题?这是有争议的。你可以这样做

class OperationResult
{
    List<type1> ResultData1 {get; set;}
    List<type2> ResultData2 {get; set;}
    List<type3> ResultData3 {get; set;}
    string[] Error {get; set;} // string[3]
    bool Success {get; set;}
}

在这种情况下,您可以填充 2 个网格,但不能填充第三个网格。如果客户端因此发生任何错误,您将需要使用客户端错误处理来处理它。

您绝对应该根据自己的具体需要随意调整任何模式。

我认为您的服务不是只做一件事。它负责验证输入,然后如果验证成功则更新内容。是的,我同意用户需要尽可能多的关于错误的信息(违规、未提供姓名、描述过长、日期结束在日期开始之前等),因为您可以根据单个请求提供尽可能多的信息,因此例外情况不是解决办法。

在我的项目中,我倾向于将验证和更新的关注点分开,因此执行更新的服务几乎没有 none 失败的机会。我也喜欢策略模式来进行验证和更新——用户请求更改,通用服务接受 validation/update 的请求,调用特定的 validator/updater,后者又将通用服务调用到 validate/update 一些依赖项.通用服务将合并结果并决定操作的成功或失败。明显的好处是违规消息合并在一些通用 class 和特定 validator/updater 中完成一次可以专注于单个实体。另一方面,您可能想要验证某些数据库的唯一性或 objects 在数据库上的存在,这暴露了两个问题:对数据库的额外查询(使用 Exist 来最小化输出的轻型查询,但它需要访问数据库)以及验证和更新之间的延迟(在那个时候数据库可以改变并且你的唯一性或存在验证可以改变(这个时间相对较小但它可能发生)。这种模式也最大限度地减少了 UpdateThingChildren 的重复 - 当你有简单的多对多关系 child 可以从任何一个连接的实体更新。

我看到您可以使用两个可能的选项来避免抛出异常,但最终我们只是减少了消息所需的代码量:

  1. 我会稍微重构代码,以使其在所有调用中更加标准。像下面这样(见代码注释中括号内的注释):

    public OperationResult Update2(MyThing thing)
    {
        var original = _repository.GetOtherThing(thing.Id);
        if (original == null)
        {
            return OperationResult.FromError("Invalid or ID not found of the thing update.");
        }
    
        var result = new OperationResult() { Success = true };
        if (AnyPropertyDiffers(thing, original))
        {
            result.Merge(UpdateThing(thing, original));
            if (!result.HasChanges) return result;
        }
    
        result.Merge(UpdateThingChildren(thing));
        if (!result.HasChanges) return result;
    
        result.Merge(_repository.Recalculate(thing));
        return result;
    }
    
    • _repository.GetOtherThing - 将 Id 检查委托给存储库,以简化代码并确保我们 return 如果什么都没发生就会出错
    • UpdateThing - 没有更改后退出
    • _repository.Recalculate - 我们现在合并结果 return 他们
  2. 在构建服务时使用所有服务共享的作用域class。

    // We a scope class shared by all services, 
    // we don't need to create a result or decide what result to use. 
    // It is more whether it worked or didn't 
    public void UpdateWithScope(MyThing thing)
    { 
        var original = _repository.GetOtherThing(thing.Id);
        if (_workScope.HasErrors) return;
        if (original == null)
        {
            _workScope.AddError("Invalid or ID not found of the thing update.");
            return;
        }
    
        if (AnyPropertyDiffers(thing, original))
        {
            UpdateThing(thing, original);
            if (_workScope.HasErrors) return;
        }
    
        UpdateThingChildren(thing);
        if (_workScope.HasErrors) return;
    
        _repository.Recalculate(thing);
    }
    
    • _repository.GetOtherThing 必须将任何错误添加到 _workScope
    • UpdateThing 必须将任何错误添加到 _workScope
    • UpdateThingChildren 必须将任何错误添加到 _workScope
    • _repository.Recalculate 必须将任何错误添加到 _workScope

在最后一个示例中,我们不需要 return 任何东西,因为调用者必须验证作用域是否仍然有效。 现在,如果要执行多个验证,我建议再做一个 class,比如提到的 Rafal

  • 最后一点:对于应该在 GUI 中处理的任何事情,我都会抛出异常,例如 'thing.Id' 发送时没有值或根本没有发送,这对我来说就像界面的设计者没有正确处理这种情况,或者当前接口过时或不受支持,这通常足以引发异常。

您可以在内部使用异常而不将它们抛给调用者。这使您可以轻松地摆脱失败的操作,并将您的业务逻辑集中在一个地方。仍然有一些 'cruft',但它与包含在 *Internal 实现中的业务逻辑分离(并且可以在其自己的 class 中)。并不是说这是解决此问题的最佳或唯一方法,但我可能会这样做:

public class OperationResult
{
    public bool Success { get; set; }
    public List<string> Errors { get; set;  } = new List<string>();
}
public class Thing { public string Id { get; set; } }
public class OperationException : Exception
{
    public OperationException(string error = null)
    {
        if (error != null)
            Errors.Add(error);
    }

    public List<string> Errors { get; set; } = new List<string>();
}

public class Operation
{
    public OperationResult Update(Thing thing)
    {
        var result = new OperationResult { Success = true };
        try
        {
            UpdateInternal(thing);
        }
        catch(OperationException e)
        {
            result.Success = false;
            result.Errors = e.Errors;
        }

        return result;
    }

    private void UpdateInternal(Thing thing)
    {
        if (thing.Id == null)
            throw new OperationException("Could not find ID of the thing update.");

        var original = _repository.GetOtherThing(thing.Id);
        if (original == null)
            return;

        if (AnyPropertyDiffers(thing, original))
            result.Merge(UpdateThing(thing, original));

        result.Merge(UpdateThingChildren(thing));

        if (result.HasChanges)
            _repository.Recalculate(thing);
    }
}

我认为这是函数式编程可以提供帮助的情况,所以我会尝试 package 将一些 F# 功能移植到 C#

using Optional;

并且因为我们要管理异常

using Optional.Unsafe;

此时我们可以引入一个助手,来完成典型的功能性“monad chaining

public static class Wrap<Tin, Tout>
{
    public static Option<Tout, Exception> Chain(Tin input, Func<Tin, Tout> f)
    {
        try
        {
            return Option.Some<Tout,Exception>(f(input));
        }
        catch (Exception exc)
        {
            return Option.None<Tout, Exception>(exc);
        }
    }
    public static Option<Tout, Exception> TryChain(Option<Tin, Exception> input, Func<Tin, Tout> f)
    {
        return input.Match(
                some: value => Chain(value, f),
                none: exc => Option.None<Tout, Exception>(exc)
            );
    }
}

现在,假设我们有以下可能引发异常的更新:

    Type2 Update1 (Type1 t)
    {
        var r = new Type2();
        // can throw exceptions
        return r;
    }
    Type3 Update2(Type2 t)
    {
        var r = new Type3();
        // can throw exceptions
        return r;
    }
    Type4 Update3(Type3 t)
    {
        var r = new Type4();
        // can throw exceptions
        return r;
    }

我们将能够在 Happy Path

之后编写一个逻辑流程
    Option<Type4, Exception> HappyPath(Option<Type1, Exception> t1)
    {
        var t2 = Wrap<Type1,Type2>.TryChain(t1, Update1);
        var t3 = Wrap<Type2, Type3>.TryChain(t2, Update2);
        return Wrap<Type3, Type4>.TryChain(t3, Update3);
    }

最后,扩展 class 喜欢

public static class Extensions {
    public static Option<Type2, Exception> TryChain(this Option<Type1, Exception> input, Func<Type1, Type2> f)
    {
        return Wrap<Type1, Type2>.TryChain(input, f);
    }
    public static Option<Type3, Exception> TryChain(this Option<Type2, Exception> input, Func<Type2, Type3> f)
    {
        return Wrap<Type2, Type3>.TryChain(input, f);
    }
    public static Option<Type4, Exception> TryChain(this Option<Type3, Exception> input, Func<Type3, Type4> f)
    {
        return Wrap<Type3, Type4>.TryChain(input, f);
    }
}

快乐之路可以写得很美

    Option<Type4, Exception> HappyPath(Option<Type1, Exception> t1)
    {
        var t2 = t1.TryChain(Update1);
        var t3 = t2.TryChain(Update2);
        return t3.TryChain(Update3);
    }

我会使用状态模式和内部集合来保存信息。您可以开始应用更改状态的事件并存储与应用的事件相关的信息。最后调用get information将它们包裹在operationresult中。

伪代码

public OperationResult Update(MyThing thing)
{
     return new OperationResult
            {
               Errors = thing.ApplyEvent(Event.NullCheck)
                             .ApplyEvent(Event.OriginalNullCheck)
                             .ApplyEvent(Event.OriginalPropertyDiffersCheck)
                             .CollectInfo(),
               Success = true
            };
}
public class MyThing
{
   private List<string> _errors = new List<string>();
   private MyThing _original;

   public MyThingState ThingState {get;set;}
   public MyThing ApplyEvent(Event eventInfo)
   {
       MyThingState.ApplyEvent(this, eventInfo)
   }        
}

public class NullState : MyThingState
{
    public MyThing ApplyEvent(MyThing myThing, Event eventInfo)
    { 
         if(mything == null)
         {
           // use null object pattern
           mything.AddErrors("Null value")
           // Based on the event, you select which state to instantiate
           // and inject dependencies
           mything.State = new OriginalNullState();
         }
    }
}
public class OriginalNullState : MyThingState
{
      public void ApplyEvent(MyThing myThing, Event eventInfo)
      {
           // Get original from database or repository
           // Save and validate null 
           // Store relevant information in _errors;
           // Change state
      }
}

尽管我很想抛出异常,但在这里不合适,因为你不在 fail-fast 中。您正在对非致命案例采取纠正措施。你只是想让更高层知道它。

public OperationResult<DataSet> Update(MyThing thing, OperationResult<DataSet> papa)
{
    // Either you have a result object from enclosing block or you have null.
    var result = OperationResult<DataSet>.Create(papa);

    if (thing.Id == null) return result.Fail("id is null");

    OtherThing original = _repository.GetOtherThing(thing.originalId);
    if (original == null) return result.warn("Item already deleted");

    if (AnyPropertyDiffers(thing, original))
    {
        UpdateThing(thing, original, result));
        // Inside UpdateThing, take result in papa and do this dance once:
        // var result = OperationResult<DataSet>.Create(papa);
    }

    UpdateThingChildren(thing, result);
    // same dance. This adds one line per method of overhead. Eliminates Merge thingy

    _repository.Recalculate(thing, result); 

    return result.ok();
}

您可以使用来自@BigShot 的 Scope 模式消除到处传递的结果,但我个人不喜欢环境上下文。这可能是您可能需要 return 返回的任何内容。

class OperationResult<T> {
    enum SuccessLevel { OK, WARN, FAIL }

    private SuccessLevel _level = SuccessLevel.OK;
    private List<String> _msgs = new ...

    public T value {get; set};

    public static OperationResult<T> Create(OperationResult<T> papa) {
        return papa==null ? new OperationResult<T>() : papa;
    }

    public OperationResult<T> Fail(string msg) {
        _level = SuccessLevel.Fail;
        _msgs.add(msg);
        return this; // this return self trick will help in reducing many extra lines in main code.
    }

    // similarly do for Ok() and Warn()

}

首先,为了简短地回答您的问题,除了通知模式之外,没有其他方法可以将多个响应合并为一个。即使您可以抛出异常,您也会有 AggregateException,它只不过是用于将多个异常收集到一个中的通知模式(异常只是该方法可以具有的一种输出)。

通知模式是一个很好的模式,我看不出有什么理由要避免它。是的,您的服务层方法看起来确实有些冗长,但这些方法可以重构。虽然您还没有真正征求过如何重构的建议,但您主要需要考虑这一部分。

通过查看您的代码和总体建议的几个建议:

如果适用,将通知模式作为代码库中的主要模式是正常的。不仅在服务层,而且在其他任何地方。如果一个方法 return 的结果比原始值多,我看不出你还能怎么做。因此,每个方法都可以 return OperationResult{TResult}(通用),它指示 success/failure,以及操作的结果 - 如果失败则为错误列表,如果成功则为 TResult 对象。每个调用方方法将决定如何处理结果 - 丢弃部分或全部结果,或者 return 将其返回给调用堆栈中的调用方。

在您的代码中,您有 UpdateThingChildren 私有方法。我不确定那是做什么的,但是如果你对事物本身进行 thing.UpdateChildren() 调用会更好地表达意图。

要减少服务方法的繁琐,您可以使用流畅的 interface-like 方法链接。实现起来应该不难,假设你调用的每个操作都是returns OperationResult。我希望您的代码至少看起来像这样:

private ThingRepository _repository;
public OperationResult Update(MyThing thing)
{
    return new OperationResult() //removed Success = true, just make that a default value.
        .Then(() => thing.ValidateId()) //moved id validation into thing
        .Then(() => GetOtherThing(thing.Id)) //GetOtherThing validates original is null or not
        .Then(() => thing.AnyPropertyDiffersFrom(original)) //moved AnyPropertyDiffers into thing
        .Then(() => thing.UpdateChildren())
        .Then(() => _repository.Recalculate(thing));
}
private OperationResult GetOtherThing(MyThing ) {...}

如你所想,实现Then方法一点都不难。它在 OperationResult 上定义,并将 Func{OperationResult} (通用)作为参数。如果 success == false,它不会执行 func。否则,它执行 func 并将运算结果与自身合并。最后,它总是 return 自己 (this)。