十一、了解操作结果设计模式
在本章中,我们将从简单到更复杂的案例探索操作结果模式。操作结果旨在向调用者传达操作的成功或失败。它还允许该操作向调用者返回一个值和一条或多条消息。
想象一下,在任何系统中,您都希望显示用户友好的错误消息,获得一些小的速度增益,甚至可以轻松明确地处理故障。操作结果设计模式可以帮助您实现这些目标。使用它的一种方法是作为远程操作的结果,例如在查询远程 web 服务之后。
本章将介绍以下主题:
- 操作结果设计模式基础
- 返回值的操作结果设计模式
- 返回错误消息的操作结果设计模式
- 返回具有严重性级别的消息的操作结果设计模式
- 使用子类和静态工厂方法更好地隔离成功和失败
目标
操作结果模式的作用是为操作(方法)提供返回复杂结果(对象)的可能性,这允许消费者:
- [必须]访问操作的成功指示器(即操作是否成功)。
- [可选]如果存在操作结果(方法的返回值),则访问操作结果。
- [可选]在操作未成功的情况下,访问故障原因(错误消息)。
- [可选]访问记录操作结果的其他信息。这可以是简单的消息列表,也可以是复杂的多个属性。
这可以更进一步,例如返回故障的严重性或为特定用例添加任何其他相关信息。成功指示器可以是二进制(true
或false
,也可以有两种以上的状态,如成功、部分成功和失败。你的想象力(和需求)是你的极限!
提示
首先关注你的需求,然后运用你的想象力找到最好的解决方案。软件工程不仅仅是应用别人告诉你的技术。这是一门艺术!唯一的区别是你正在制作软件,而不是绘画或木工。而且大多数人不会看到这些艺术(代码)。
设计
当操作失败时,很容易依赖于抛出异常。但是,当您不想或不能使用异常时,操作结果模式是组件之间通信成功或失败的另一种方式。
为了有效地使用,方法必须返回一个包含目标部分中显示的一个或多个元素的对象。根据经验,返回操作结果的方法不应引发异常。通过这种方式,用户不必处理操作结果本身以外的任何事情。对于特殊情况,您可以允许抛出异常,但在这一点上,这将是一个基于明确规范的判断调用或面临实际问题。
在看了描述此模式最简单形式的基本序列图(适用于所有示例)后,让我们跳入代码并探索多个较小的示例,而不是遍历所有可能的 UML 图:
图 11.1–运行结果设计模式的序列图
正如我们从图中看到的,一个操作返回一个结果(一个对象),然后调用方可以处理该结果。下面的示例介绍了结果对象中可以包含的内容。
项目-实施不同的操作结果模式
在这个项目中,使用者将 HTTP 请求路由到正确的处理程序。我们正在逐个访问这些处理程序,这将帮助我们实现从简单到更复杂的操作结果。这将向您展示实现操作结果模式的许多可选方法,以帮助您理解它,使其成为您自己的模式,并根据项目的需要实现它。
消费者
所有示例中的消费者都是Startup
类。以下代码将请求路由到处理程序:
app.UseRouter(builder =>
{
builder.MapGet("/simplest-form", SimplestFormHandler);
builder.MapGet("/single-error", SingleErrorHandler);
builder.MapGet("/single-error-with-value", SingleErrorWithValueHandler);
builder.MapGet("/multiple-errors-with-value", MultipleErrorsWithValueHandler);
builder.MapGet("/multiple-errors-with-value-and-severity", MultipleErrorsWithValueAndSeverityHandler);
builder.MapGet("/static-factory-methods", StaticFactoryMethodHandler);
});
接下来,我们逐一介绍每个处理程序。
最简单的形式
下图表示操作结果模式的最简单形式:
图 11.2–运行结果设计模式的类图
我们可以将该类图转换为以下代码块:
namespace OperationResult
{
public class Startup
{
// ...
private async Task SimplestFormHandler(HttpRequest request, HttpResponse response, RouteData data)
{
// Create an instance of the class that contains the operation
var executor = new SimplestForm.Executor();
// Execute the operation and handle its result
var result = executor.Operation();
if (result.Succeeded)
{
// Handle the success
await response.WriteAsync("Operation succeeded");
}
else
{
// Handle the failure
await response.WriteAsync("Operation failed");
}
}
}
}
前面的代码处理/simplest-form
HTTP 请求。它是操作的消费者。
namespace OperationResult.SimplestForm
{
public class Executor
{
public OperationResult Operation()
{
// Randomize the success indicator
// This should be real logic
var randomNumber = new Random().Next(100);
var success = randomNumber % 2 == 0;
// Return the operation result
return new OperationResult(success);
}
}
public class OperationResult
{
public OperationResult(bool succeeded)
{
Succeeded = succeeded;
}
public bool Succeeded { get; }
}
}
Executor
类使用Operation
方法实现要执行的操作。该方法返回OperationResult
类的一个实例。实现基于随机数。有时成功,有时失败。您通常会在该方法中编写一些应用逻辑。
OperationResult
类表示操作的结果。在本例中,它是一个简单的只读布尔值,存储在Succeeded
属性中。
在这种形式中,Operation()
方法返回bool
和OperationResult
实例之间的差异很小,但仍然存在。通过返回一个OperationResult
对象,您可以随着时间的推移扩展返回值,并将其添加到bool
对象中,这在不更新所有使用者的情况下是无法实现的。
单一错误消息
现在我们知道手术是否成功,我们想知道哪里出了问题。为此,我们可以向OperationResult
类添加一个ErrorMessage
属性。通过这样做,我们不再需要设置操作是否成功;我们可以用ErrorMessage
属性来计算。
这一改进背后的逻辑如下:
- 没有错误消息时,操作成功。
- 出现错误消息时,操作失败。
实现此逻辑的OperationResult
如下所示:
namespace OperationResult.SingleError
{
public class OperationResult
{
public OperationResult() { }
public OperationResult(string errorMessage)
{
ErrorMessage = errorMessage ?? throw new ArgumentNullException(nameof(errorMessage));
}
public bool Succeeded => string.IsNullOrWhiteSpace(ErrorMessage);
public string ErrorMessage { get; }
}
}
在前面的代码中,我们有以下内容:
-
Two constructors:
a) 处理成功的无参数构造函数。
b) 将错误消息作为处理错误的参数的构造函数。
-
检查
ErrorMessage
的Succeeded
属性。 - 包含可选错误消息的
ErrorMessage
属性。
该操作的执行者看起来类似,但使用新的构造函数,设置错误消息,而不是直接设置成功指示器:
namespace OperationResult.SingleError
{
public class Executor
{
public OperationResult Operation()
{
// Randomize the success indicator
// This should be real logic
var randomNumber = new Random().Next(100);
var success = randomNumber % 2 == 0;
// Return the operation result
return success
? new OperationResult()
: new OperationResult($"Something went wrong with the number '{randomNumber}'.");
}
}
}
消费代码与上一个示例中相同,但在响应输出中写入错误消息,而不是一般故障字符串:
namespace OperationResult
{
public class Startup
{
// ...
private async Task SingleErrorHandler(HttpRequest request, HttpResponse response, RouteData data)
{
// Create an instance of the class that contains the operation
var executor = new SingleError.Executor();
// Execute the operation and handle its result
var result = executor.Operation();
if (result.Succeeded)
{
// Handle the success
await response.WriteAsync("Operation succeeded");
}
else
{
// Handle the failure
await response.WriteAsync (result.ErrorMessage);
}
}
}
}
在查看该示例时,我们可以开始理解操作结果模式的有用性。它使我们更远离简单的成功指标,它看起来像一个过于复杂的布尔值。这并不是我们探索的终点,因为可以在更复杂的场景中设计和使用更多的表单。
增加一个返回值
既然我们有了失败的原因,我们可能希望操作返回一个值。为了实现这一点,让我们从上一个示例开始,在OperationResult
类中添加一个Value
属性,如下所示(我们在第 17 章、ASP.NET Core 用户界面中介绍了仅初始化属性:
namespace OperationResult.SingleErrorWithValue
{
public class OperationResult
{
// ...
public int? Value { get; init; }
}
}
操作也非常类似,但我们正在使用对象初始值设定项设置Value
:
namespace OperationResult.SingleErrorWithValue
{
public class Executor
{
public OperationResult Operation()
{
// Randomize the success indicator
// This should be real logic
var randomNumber = new Random().Next(100);
var success = randomNumber % 2 == 0;meet
// Return the operation result
return success
? new OperationResult { Value =
randomNumber }
: new OperationResult($"Something went wrong with the number '{randomNumber}'.")
{
Value = randomNumber
};
}
}
}
有了后,消费者可以按如下方式使用Value
:
namespace OperationResult
{
public class Startup
{
// ...
private async Task SingleErrorWithValueHandler (HttpRequest request, HttpResponse response, RouteData data)
{
// Create an instance of the class that contains the operation
var executor = new SingleErrorWithValue.Executor();
// Execute the operation and handle its result
var result = executor.Operation();
if (result.Succeeded)
{
// Handle the success
await response.WriteAsync($"Operation succeeded with a value of '{result.Value} '.");
}
else
{
// Handle the failure
await response.WriteAsync (result.ErrorMessage);
}
}
}
}
从这个示例中我们可以看到,当操作失败时,我们可以显示相关的错误消息,当操作成功时(甚至在这种情况下失败时),我们可以使用返回值,所有这些都不会引发异常。有了这一点,操作结果模式的力量开始显现。我们还没有完成,所以让我们跳到下一个进化。
多条错误消息
现在我们已经到了可以将Value
和ErrorMessage
传输给操作使用者的地步,但是传输多个错误(例如验证错误)呢?为此,我们可以将我们的ErrorMessage
属性转换为IEnumerable<string>
并添加管理消息的方法:
namespace OperationResult.MultipleErrorsWithValue
{
public class OperationResult
{
private readonly List<string> _errors;
public OperationResult(params string[] errors)
{
_errors = new List<string>(errors ?? Enumerable.Empty<string>());
}
public bool Succeeded => !HasErrors();
public int? Value { get; set; }
public IEnumerable<string> Errors => new ReadOnlyCollection<string>(_errors);
public bool HasErrors()
{
return Errors?.Count() > 0;
}
public void AddError(string message)
{
_errors.Add(message);
}
}
}
让我们先看看前面的代码中的新部分,然后再继续:
- 错误现在存储在
List<string> _errors
中,并通过IEnumerable<string>
接口下隐藏的ReadOnlyCollection<string>
实例返回给消费者。ReadOnlyCollection<string>
实例拒绝从外部更改集合,例如,假设消费者足够聪明,可以将IEnumerable<string>
转换为List<string>
。 Succeeded
属性已更新,以说明集合而不是单个消息,并遵循相同的逻辑。- 为方便起见,增加了
HasErrors
方法。 -
The
AddError
method allows adding errors after the instance creation, which could happen in more complex scenarios, such as multi-step operations where parts could fail without the operation itself failing.笔记
对于结果的额外控制,
AddError
方法应隐藏在操作外部。这样,使用者就不能在操作完成后向结果中添加错误。当然,所需的控制级别取决于每个特定场景。对此进行编码的一种方法是返回一个不包含该方法的接口,而不是返回包含该方法的具体类型。
现在更新了操作结果,操作本身可以保持不变,但我们可以在结果中添加多个错误。消费者必须处理这一细微差异,并支持多个错误。
让我们来看看这个代码:
namespace OperationResult
{
public class Startup
{
private async Task MultipleErrorsWithValueHandler(HttpRequest request, HttpResponse response, RouteData data)
{
// Create an instance of the class that contains the operation
var executor = new MultipleErrorsWithValue.Executor();
// Execute the operation and handle its result
var result = executor.Operation();
if (result.Succeeded)
{
// Handle the success
await response.WriteAsync($"Operation succeeded with a value of '{result.Value}'.");
}
else
{
// Handle the failure
var json = JsonSerializer.Serialize (result.Errors);
response.Headers["ContentType"] = "application/json";
await response.WriteAsync(json);
}
}
}
}
代码将IEnumerable<string> Errors
属性序列化为 JSON,然后将其输出到客户端,以帮助可视化集合。
提示
当操作成功时返回一个plain/text
字符串,当操作失败时返回一个application/json
数组通常不是一个好主意。我建议不要在实际应用中执行类似的操作。返回 JSON 或纯文本。尽量不要在单个端点中混合内容类型。在大多数情况下,混合内容类型只会产生可避免的复杂性。我们可以说,读取content-type
和状态码头就足以知道服务器返回了什么,这就是 HTTP 规范中这些头的用途。但是,即使这是真的,您的开发伙伴在使用您的 API 时总是能够期望相同的内容类型也要容易得多。
在设计系统契约时,一致性和一致性通常优于不一致性、模糊性和差异性。
我们的操作结果模式实现越来越好,但仍然缺少一些特性。这些特性之一是传播非错误消息的可能性,例如信息消息和警告,我们将在下一步实现这些功能。
增加消息严重性
既然我们的操作结果结构已经具体化,让我们更新我们的上一次迭代以支持消息严重性。
首先,我们需要一个严重性指标。安是这类工作的好人选,但可能是别的。让我们把它命名为OperationResultSeverity
。
然后我们需要一个消息类来封装消息和严重性级别;让我们把那个类命名为OperationResultMessage
。新代码如下所示:
namespace OperationResult.WithSeverity
{
public class OperationResultMessage
{
public OperationResultMessage(string message, OperationResultSeverity severity)
{
Message = message ?? throw new ArgumentNullException(nameof(message));
Severity = severity;
}
public string Message { get; }
public OperationResultSeverity Severity { get; }
}
public enum OperationResultSeverity
{
Information = 0,
Warning = 1,
Error = 2
}
}
正如您所看到的,我们有一个简单的数据结构来替换我们的string
消息。
然后我们需要更新OperationResult
类以使用新的OperationResultMessage
类。我们需要确保只有在没有OperationResultSeverity.Error
的情况下,操作结果才显示成功,允许其传输OperationResultSeverity.Information
和OperationResultSeverity.Warnings
消息:
namespace OperationResult.WithSeverity
{
public class OperationResult
{
private readonly List<OperationResultMessage> _messages;
public OperationResult(params OperationResultMessage[] errors)
{
_messages = new List<OperationResultMessage> (errors ?? Enumerable.Empty <OperationResultMessage>());
}
public bool Succeeded => !HasErrors();
public int? Value { get; init; }
public IEnumerable<OperationResultMessage> Messages
=> new ReadOnlyCollection <OperationResultMessage>(_messages);
public bool HasErrors()
{
return FindErrors().Count() > 0;
}
public void AddMessage(OperationResultMessage message)
{
_messages.Add(message);
}
private IEnumerable<OperationResultMessage> FindErrors()
=> _messages.Where(x => x.Severity == OperationResultSeverity.Error);
}
}
高亮显示的行表示仅当_messages
列表中不存在错误时设置成功状态的新逻辑。
有了这一点,Executor
类也需要改进。
那么让我们看看Executor
类的新版本:
namespace OperationResult.WithSeverity
{
public class Executor
{
public OperationResult Operation()
{
// Randomize the success indicator
// This should be real logic
var randomNumber = new Random().Next(100);
var success = randomNumber % 2 == 0;
// Some information message
var information = new OperationResultMessage(
"This should be very informative!",
OperationResultSeverity.Information
);
// Return the operation result
if (success)
{
var warning = new OperationResultMessage(
"Something went wrong, but we will try again later automatically until it works!",
OperationResultSeverity.Warning
);
return new OperationResult(information, warning) { Value = randomNumber };
}
else
{
var error = new OperationResultMessage(
$"Something went wrong with the number '{randomNumber}'.",
OperationResultSeverity.Error
);
return new OperationResult(information, error) { Value = randomNumber };
}
}
}
}
正如您可能已经注意到的,我们删除了第三个操作符,以使代码更易于阅读。
提示
您应该始终致力于编写易于阅读的代码。使用语言特性是可以的,但是将语句嵌套在单行语句之上有其局限性,很快就会变得一团糟。
在最后一个代码块中,成功和失败都返回两条消息:
- 成功时,消息为信息消息和警告。
- 如果失败,则消息为信息消息和错误。
从使用者的角度(请参阅下面的代码),我们现在只将结果序列化到输出,以清楚地显示结果。以下是使用此新操作的/multiple-errors-with-value-and-severity endpoint
委托:
namespace OperationResult
{
public class Startup
{
// ...
private async Task MultipleErrorsWithValueAndSeverityHandler(HttpRequest request, HttpResponse response, RouteData data)
{
// Create an instance of the class that contains the operation
var executor = new WithSeverity.Executor();
// Execute the operation and handle its result
var result = executor.Operation();
if (result.Succeeded)
{
// Handle the success
}
else
{
// Handle the failure
}
var json = JsonSerializer.Serialize(result);
response.Headers["ContentType"] = "application/json";
await response.WriteAsync(json);
}
}
}
正如您所看到的,它仍然很容易使用,但现在增加了更多的灵活性。我们可以处理不同类型的消息,例如向用户显示消息、重试操作等等。
目前,如果运行应用并调用该端点,成功的调用将返回一个 JSON 字符串,如下所示:
{
"Succeeded": true,
"Value": 86,
"Messages": [
{
"Message": "This should be very informative!",
"Severity": 0
},
{
"Message": "Something went wrong, but we will try again later automatically until it works!",
"Severity": 1
}
]
}
失败应返回如下所示的 JSON 字符串:
{
"Succeeded": false,
"Value": 87,
"Messages": [
{
"Message": "This should be very informative!",
"Severity": 0
},
{
"Message": "Something went wrong with the
number '87'.",
"Severity": 2
}
]
}
改进此设计的另一个想法是添加一个Status
属性,该属性根据每条消息的严重性级别返回复杂的成功结果。为此,我们可以创建另一个enum
:
public enum OperationStatus{ Success, Failure, PartialSuccess}
然后我们可以通过OperationResult
类上名为Status
的新属性访问它。有了这一点,消费者可以处理部分成功,而无需深入挖掘信息本身。我会让你自己玩这个。
现在我们已经将我们的简单示例扩展到这里,如果我们希望Value
是可选的,会发生什么?为此,我们可以创建多个操作结果类,每个类都包含或多或少的信息(属性);让我们下一步试试。
子类和工厂
在这个迭代中,我们保留了所有的属性,但是我们改变了实例化OperationResult
对象的方式。
一个静态工厂方法只不过是一个负责创建对象的静态方法。正如您将要看到的,它可以变得方便易用。和往常一样,我再怎么强调也不过分:在设计静态的东西时要小心,否则以后它可能会困扰你。
让我们从一些已经访问过的代码开始:
namespace OperationResult.StaticFactoryMethod
{
public class OperationResultMessage
{
public OperationResultMessage(string message, OperationResultSeverity severity)
{
Message = message ?? throw new ArgumentNullException(nameof(message));
Severity = severity;
}
public string Message { get; }
public OperationResultSeverity Severity { get; }
}
public enum OperationResultSeverity
{
Information = 0,
Warning = 1,
Error = 2
}
}
前面的代码与我们以前使用的代码相同。在下面的代码块中,计算操作的成功或失败结果时不考虑严重性。相反,我们创建了一个包含两个子类的抽象OperationResult
类:
SuccessfulOperationResult
,表示操作成功。FailedOperationResult
,表示操作失败。
然后下一步是通过创建两个静态工厂方法来强制使用专门设计的类:
public static OperationResult Success()
,返回一个SuccessfulOperationResult
。public static OperationResult Failure(params OperationResultMessage[] errors)
,返回一个FailedOperationResult
。
这样做将决定操作是否成功的责任从OperationResult
类本身转移到Operation
方法。
以下代码块显示了新的OperationResult
实现(静态工厂突出显示):
namespace OperationResult.StaticFactoryMethod
{
public abstract class OperationResult
{
private OperationResult() { }
public abstract bool Succeeded { get; }
public virtual int? Value { get; init; }
public abstract IEnumerable<OperationResultMessage> Messages { get; }
public static OperationResult Success(int? value = null)
{
return new SuccessfulOperationResult { Value = value };
}
public static OperationResult Failure(params OperationResultMessage[] errors)
{
return new FailedOperationResult(errors);
}
public sealed class SuccessfulOperationResult : OperationResult
{
public override bool Succeeded => true;
public override IEnumerable <OperationResultMessage> Messages
=> Enumerable.Empty <OperationResultMessage>();
}
public sealed class FailedOperationResult : OperationResult
{
private readonly List<OperationResultMessage> _messages;
public FailedOperationResult(params OperationResultMessage[] errors)
{
_messages = new List<OperationResultMessage>(errors ?? Enumerable.Empty<OperationResultMessage>());
}
public override bool Succeeded => false;
public override IEnumerable <OperationResultMessage> Messages
=> new ReadOnlyCollection <OperationResultMessage>(_messages);
}
}
}
在分析代码后,有两个密切相关的特殊性:
OperationResult
类有一个私有构造函数。SuccessfulOperationResult
和FailedOperationResult
类都嵌套在OperationResult
中并从中继承。
嵌套类是从OperationResult
类继承的唯一方法,因为作为类的成员,嵌套类可以访问其私有成员,包括构造函数。否则,无法从OperationResult
继承。
从本书开始,我已经多次重复了灵活性;但你并不总是想要灵活性。有时,您希望控制您公开的内容以及您允许消费者做的事情。
在这种特定的情况下,我们可以使用受保护的构造函数,或者我们可以实现一种更奇特的方法来实例化成功和失败实例。然而,我决定利用这个机会向您展示如何在适当的位置锁定实现,从而使它不可能通过从外部继承进行扩展。我们本可以在类中构建机制以允许受控的可扩展性,但对于这一个,让我们将其严格锁定!
从这里开始,唯一缺少的部分就是操作本身和使用该操作的客户端。让我们先看看操作:
namespace OperationResult.StaticFactoryMethod
{
public class Executor
{
public OperationResult Operation()
{
// Randomize the success indicator
// This should be real logic
var randomNumber = new Random().Next(100);
var success = randomNumber % 2 == 0;
// Return the operation result
if (success)
{
return OperationResult.Success(randomNumber);
}
else
{
var error = new OperationResultMessage(
$"Something went wrong with the number '{randomNumber}'.",
OperationResultSeverity.Error
);
return OperationResult.Failure(error);
}
}
}
}
前面代码块中突出显示的两行显示了这一新改进的优雅。我发现这段代码很容易阅读,这是我的目标。我们现在有两种方法可以清楚地定义我们在使用它们时的意图:Success
或Failure
。
消费者使用的代码与我们之前在其他示例中看到的代码相同,因此我将在这里省略它。
利与弊
下面是操作结果设计模式的一些优点和缺点。
优势
它比抛出一个Exception
更显式,因为操作结果类型被显式指定为方法的返回类型。这比知道操作及其依赖项可以引发什么类型的异常更为明显。
另一个优点是执行速度快;返回对象比引发异常更快。没那么快,但还是快了。
缺点
使用操作结果比抛出异常更复杂,因为我们必须手动将其传播到调用堆栈(也称为被调用方返回并由调用方处理)。如果操作结果必须上升到多个级别,则尤其如此,这可能是不使用模式的指示。
很容易公开并非在所有场景中都使用的成员,创建一个大于需要的 API 表面,其中某些部分仅在某些情况下使用。但是,在这和花费无数时间设计完美系统之间,有时暴露int? Value { get; }
财产可能是一个更可行的选择。从那里,您可以选择将曲面缩小到最小。运用你的想象力和设计技巧来克服这些挑战!
总结
在本章中,我们访问了操作结果模式的多种形式,从增广布尔型到包含消息、值和成功指标的复杂数据结构。我们还探索了静态工厂和私有构造函数来控制外部访问。此外,在所有这些探索之后,我们可以得出结论,围绕操作结果模式几乎有无限的可能性。每个特定的用例都应该说明如何实现它。从这里开始,我相信您有足够的关于模式的信息,可以自己探索更多的可能性,我强烈鼓励您这样做。
此时,我们通常会探索操作结果模式如何帮助我们遵循坚实的原则。但是,它太依赖于实现,因此这里有几点:
OperationResult
类封装结果,从其他系统的组件(SRP)中提取责任。- 在多个示例中,我们使用
Value
属性违反了 ISP。这是次要的,可以用不同的方法来完成,这可能会导致更复杂的设计。 - 我们可以将操作结果与视图模型或DTO进行比较,但通过操作(方法)返回。从那个里,我们可以添加一个抽象,或者继续返回一个具体的类,我们可以认为这违反了 DIP。
- 当优势超过了这两种违规行为的轻微和有限影响时,我不介意让它们溜走(原则是理想、规则,而不是法律)。
本章总结了组件级部分的设计,并引出了应用级部分的设计,我们将在其中探索更高层次的设计模式。
问题
让我们来看看几个练习题:
- 在执行异步调用(如 HTTP 请求)时返回操作结果是一个好主意吗?
- 我们使用静态方法实现的模式的名称是什么?
- 返回操作结果是否比引发异常更快?
进一步阅读
以下是我们在本章中学习的一些链接:
- 我博客上的一篇关于异常的文章(标题:异常入门指南基础知识:https://net5.link/PpEm
- 我博客上的一篇关于操作结果的文章(标题:操作结果设计模式:https://net5.link/4o2q
- 我有一个操作结果模式的通用开源实现,允许您通过向项目中添加 NuGet 包来使用它:https://net5.link/FeGZ
版权属于:月萌API www.moonapi.com,转载请注明出处