是否存在用于多个消息和成功/失败的通知模式的替代方案
本文关键字:通知 失败 模式 方案 成功 用于 存在 消息 是否 | 更新日期: 2023-09-27 18:16:21
对于多个消息和成功/失败是否有替代通知模式?
我有一个类OperationResult,我用它来返回一个Success
布尔值和一个"错误"消息列表。这些消息有时是意想不到的错误,但更常见的是经常发生的普通情况。有时返回单个错误消息,但有时返回多个错误消息。我希望能找到一个更好的方法。
这似乎或多或少是Fowler提倡的通知模式。消费者然后对成功状态和错误做一些合理的处理,大多数情况下向用户显示错误,但有时在非致命错误的情况下继续。
因此,我有很多服务方法(不是web服务方法)看起来像这样:
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) {...}
可以想象,UpdateThing
、UpdateThingChildren
和ThingRepository.Recalculate
都有类似的OperationResult
合并/操作代码,它们的业务逻辑交织在一起。
是否有一个替代这么多的代码围绕我返回的对象?我希望我的代码只关注业务逻辑,而不必特别注意操作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 ???;
}
任何想法?
注意:抛出异常在这里并不合适,因为消息不是异常的。
我认为这是函数式编程可以帮助的情况,所以我尝试用一个包将F#
的一些特性移植到C#
using Optional;
,因为我们想要管理异常
using Optional.Unsafe;
在这里,我们可以引入一个助手,来执行典型的函数式"单链接"
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);
}
最后,使用像
这样的扩展类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);
}
我认为你的服务没有做一件事。它负责验证输入,如果验证成功,则更新内容。是的,我同意用户需要尽可能多的关于错误的信息(违规,未提供名称,描述过长,日期结束在日期开始之前等),因为你可以在单个请求中产生异常。
在我的项目中,我倾向于将验证和更新的关注点分开,以便进行更新的服务几乎没有失败的机会。我还喜欢同时进行验证和更新的策略模式——用户请求更改通用服务接受验证/更新请求,调用特定的验证器/更新器,后者反过来调用通用服务来验证/更新一些依赖项。通用服务将合并结果并决定操作的成功或失败。明显的好处是,冲突消息合并在某些泛型类中完成一次,特定的验证器/更新器可以专注于单个实体。另一方面,您可能想要验证数据库上的某些数据库唯一性或对象的存在性,这暴露了两个问题:对数据库的额外查询(使用Exist
最小化输出的轻查询,但这是对数据库的访问)以及验证和更新之间的延迟(在此时间内数据库可能会更改,您的唯一性或存在性验证可能会更改(此时间相对较小,但可能发生)。此模式还最大限度地减少了UpdateThingChildren
的重复-当您有简单的多对多关系时,子实体可以从连接的实体之一更新。
首先,简单回答您的问题,要将多个响应合并为一个响应,除了Notification模式之外没有其他选择。即使你可以抛出一个异常,你也会有AggregateException,它只不过是一个通知模式,用于将几个异常收集到一个(异常只是方法可以拥有的一种输出)。
通知模式是一个很好的模式,我看不出有什么理由要避免它。是的,您的服务层方法看起来确实有些冗长,但这些方法可以重构。虽然您还没有真正询问如何重构,但您需要主要考虑这部分内容。
通过查看您的代码和一般的几个建议:
如果适用的话,将Notification模式作为代码库中的主要模式是正常的。不仅在服务层,在其他任何地方也一样。如果一个方法返回的结果不止一个原始值,我不知道你还能怎么做。因此,每个方法都可以返回OperationResult{TResult}(泛型),它表示成功/失败,以及操作的结果——如果失败则返回错误列表,如果成功则返回result对象。每个调用方方法将决定如何处理结果——要么丢弃部分或全部结果,要么将其返回给调用堆栈中的调用方。
在你的代码中,你有UpdateThingChildren私有方法。我不确定这是做什么,但它会更好地表达的意图,如果你做thing. updatechildren()调用的东西本身。
要减少服务方法的闲谈性,可以使用类似于接口的流畅方法链接。它应该不难实现,假设您调用的每个操作都返回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。否则,它执行函数并将操作结果与自身合并。最后,它总是返回自己(this)。
有些人明白模式是不能被打破的。但是模式也有很多批评者。而且确实有即兴发挥的空间。即使修改了一些细节,您仍然可以认为已经实现了一个模式。模式是一般的东西,可以有特定的实现。
也就是说,您可以选择不使用这种细粒度的操作结果解析。我一直提倡像这样的伪代码
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;
}
}
}
如您所见,服务器代码返回数据或错误消息。简单。服务器代码完成所有的日志记录,您可以将错误写入数据库,等等。但是我认为将复杂的操作结果传递给消费者没有任何价值。消费者只需要知道,成功与否。同样,如果你在同一个操作中得到了很多东西,如果你没有得到第一件东西,继续操作有什么意义?可能是你试图同时做太多事情的问题?这是有争议的。你可以这样做
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个网格,但不能填写第三个。如果因此在客户端发生任何错误,您将需要使用客户端错误处理来处理它。
在避免抛出异常的同时,我看到了两个可能的选项,但最终我们只是减少了消息所需的代码量:
-
我会稍微重构一下代码,以使它在所有调用中更标准。如下所示(参见代码注释中括号内的注释):
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检查到存储库,为了更简单的代码,并确保我们返回一个错误,如果没有发生
- UpdateThing -没有更改后退出
- _repository。重新计算-我们现在合并结果并返回它们
-
在构建服务时使用所有服务共享的作用域类。
// 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。realculate必须将任何错误添加到_workScope
在最后一个例子中,我们不需要返回任何东西,因为调用者必须验证作用域是否仍然有效。现在,如果有多个验证要执行,我建议做另一个类,如Rafal提到的。
- 最后一点:我会抛出异常,任何应该在GUI中处理的东西,比如'thing。Id'发送没有值或根本不发送,这听起来像谁做接口没有正确处理这种情况或当前接口是旧的或不支持,这通常是足够的理由抛出异常。
可以在内部使用异常,而不必将异常抛出给调用者。这使您可以轻松地摆脱失败的操作,并将业务逻辑全部分组在一个位置。仍然有一些"cruft",但它是从业务逻辑中分离出来的(并且可以在自己的类中),业务逻辑包含在*Internal
实现中。我不是说这是解决这个问题的最好或唯一的方法,但我可能会这样做:
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);
}
}
我将使用状态模式和内部收集来保存信息。您可以开始应用更改状态的事件,并存储与应用的事件相关的信息。最后调用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
}
}
尽管我喜欢抛出异常,但在这里它是不合适的,因为你不是在快速失败。你们正在对非致命病例采取纠正措施。你只是想让更高层的人知道。
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模式消除到处传递结果,但我个人不喜欢环境上下文。可以是你需要返回的任何东西。
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()
}