委托和业务对象






4.90/5 (95投票s)
2006 年 5 月 21 日
9分钟阅读

289414

2490
一种使用委托实现自定义业务规则验证的方法。
引言
本文旨在帮助您了解在我的个人会计项目 Trial Balance 中,我是如何进行业务对象验证的。本文最初是我网站上的一篇博文,您可以 在此处 查看。我的方法很大程度上借鉴了 Rocky Lhotka 的《Expert Business Objects》 一书中的技术(如果您没读过,也没关系),但我加入了一些 Stovell 风格的改动,让它更具特色。我还会顺带讨论一下 .NET 框架提供的一个非常棒的接口 IDataErrorInfo
,它旨在让我们的工作更轻松。
典型的实现
让我们来看一个我所说的业务规则的简单示例
账户名称不能为空,且长度不能超过 20 个字符。
实现这样的规则,通常会看到以下方式
public string Name
{
get { return _name; }
set
{
if (value == null
|| value.Trim().Length == 0
|| value.Trim().Length > 20)
{
throw new ArgumentException("Account names can't be" +
" blank, and can't be longer than 20 characters.");
}
_name = value;
}
}
虽然这可以阻止任何人将您的账户名称设置为“Supercallafragelisticexpialadocious”,但我认为它存在一些缺点
- 有时您可能确实需要一个空的名称。例如,作为“创建账户”表单的默认值。
- 如果您依赖此方法在保存前验证数据,那么您将错过数据本身就无效的情况。我的意思是,如果您从数据库加载一个名称为空的账户,并且不更改它,那么您可能永远不知道它实际上是无效的。
- 如果您不使用数据绑定,您将不得不编写大量使用
try/catch
块的代码来向用户显示这些错误。尝试在用户填写表单时显示错误会变得非常困难。 - 我不喜欢为非异常情况抛出异常。用户将账户名称设置为“Supercalafragilisticexpialadocious”不是异常,而是错误。当然,这只是我个人的偏好。
- 这样一来,就很难获取所有被违反规则的列表。例如,在某些网站上,您会看到验证消息,如 *“必须输入姓名。必须输入地址。必须输入电子邮件”*。要显示这些消息,您将需要大量的
try/catch
块。
CSLA 中的规则
Rocky 的书以不同的方式处理验证。在 Rocky 的 CSLA 框架中,当您将属性设置为无效值时,业务对象通常不会抛出异常,而是会将自身标记为无效。如果您尝试保存业务对象,*那时* 可能会抛出异常。
这背后有两个基本原则
- 业务对象无效并没有什么问题,只要您不尝试持久化它。
- 所有被违反的规则都应该可以从业务对象中检索,以便数据绑定以及您自己的代码能够看到是否存在错误并适当地处理它们。
当我在实现 Trial Balance 的规则时,我很喜欢 Rocky 的想法,但有几点让我感到担忧
- 同样,规则仅在属性设置器中被标记为已违反。如果通过其他方法加载数据,这会成为一个问题。
- 假设您有一个 `StartDate` 和一个 `EndDate`。您的 `EndDate` 业务规则可能会说 `EndDate` 必须大于 `StartDate`,对吧?
如果发生以下情况怎么办?
StartDate = DateTime.MaxValue; EndDate = DateTime.Now; // EndDate is marked as invalid StartDate = DateTime.MinValue;
因此,尽管 `EndDate` 现在在技术上是有效的,但属性设置器并未被调用,因此 `EndDate` 仍被标记为无效。您可以通过一些额外的代码来解决这个问题,但这并不是我个人想过多考虑的事情。
- 我们之前同意,业务对象无效并没有什么问题,只要我们不尝试保存它。在不进行任何验证但调用了属性设置器的情况下,我们的验证代码仍然会被调用。这有点不必要。
Trial Balance 中的规则
这促使我产生了以下设计目标
- 业务对象在不尝试保存的情况下可以允许无效。
- 规则*不应该*在属性设置器中实现,因为正如上面所述,这太受限制了。
- 除非绝对必要,否则不应该检查规则。
- 除非对象试图在未经验证的情况下保存,否则不应使用异常。
- 任何被违反的规则都应该可以从业务对象中检索。
Trial Balance 中的典型属性看起来会是这样
public string Name {
get { return _name; }
set { _name = value; }
}
注意到里面没有任何规则了吗?
如果您查看 `Account` 类,向下滚动到大约中间位置,您会看到一个名为 `CreateRules` 的方法,它看起来有些像这样
protected override List<Rule> CreateRules() {
List<Rule> rules = base.CreateRules();
rules.Add(new SimpleRule("Name", "An account name is required" +
" and cannot be left blank.",
delegate { return this.Name.Length != 0; }));
rules.Add(new SimpleRule("Name", "Account names cannot be" +
" more than 20 characters in length.",
delegate { return this.Name.Length <= 20; }));
return rules;
}
对于适用于对象的每个规则,我都在使用委托来验证规则。由于这些通常是简短的验证例程,因此它们通常只需要一行代码即可完成。
IDataErrorInfo
在深入探讨我如何验证规则的细节之前,我想先谈谈如果您是 .NET 开发者,应该了解的一个非常重要的接口。它叫做 IDataErrorInfo
,它位于 `System.ComponentModel` 命名空间中。
IDataErrorInfo
用于告知人们您对象上的错误。它有两个部分——一个名为 `Error` 的字符串属性,您可以在其中返回对象上所有错误的列表(例如:*“姓名不能为空。电子邮件不能为空。地址不能为空……”*),以及一个索引器,它接受属性名称并返回一个相关的错误消息(例如,`account["Name"]` 可能会返回 *“姓名不能为空”*)。
在 .NET 中,如果像 `DataGridView` 这样的控件被数据绑定到一个对象,并且该对象实现了 `IDataErrorInfo`,那么 `DataGridView` 会非常智能地为您调用业务对象上的方法。因此,如果您正确实现了 `IDataErrorInfo`,您可能会看到这个
或者,如果您有一些数据绑定的 `TextBox`、`CheckBox` 或 `ComboBox`,您可以将 `ErrorProvider` 拖到您的表单上。只需将错误提供程序的 `DataSource` 设置为您的业务对象,您也会看到报告的错误
错误提供程序通过查看数据源,了解哪些其他控件已绑定到它,然后在数据源存在错误时,在这些控件上显示错误。
请注意——在上面的屏幕截图中,触发显示错误的*代码位于业务对象中*,而不是在 GUI 中。我不需要编写任何 GUI 代码——数据绑定和 `IDataErrorInfo` 已经完成了。
在 Trial Balance 中,我的 `DomainObject` 基类实现了 `IDataErrorInfo`,因此我所有的业务对象都继承了此功能。
回到规则
这是重要类的类图
在 Trial Balance 中,每个业务对象都有一个 `CreateRules` 方法,它从 `DomainObject` 基类继承。当对象上的任何验证方法*第一次*被调用时,就会调用 `CreateRules` 方法来获取适用于所有规则的列表。这一点很重要,我想强调一下——除非您实际调用了验证方法或属性之一,否则 `CreateRules` 方法*永远不会*被调用。
`CreateRules` 方法向基类返回一个通用的 `Rule` 列表。`Rule` 类是一个抽象类,它包含两个属性和一个抽象方法
- `PropertyName` – 获取规则所属属性的名称。
- `Description` – 获取有关被违反规则的描述性文本。
- `ValidateRule()` – 这是返回您的代码是否有效的抽象方法。
SimpleRule
由于大多数规则都是非常简单的单行构造,因此有一个额外的类继承自 `Rule`,称为 `SimpleRule`。这个类在其构造函数中接受一个委托,该委托由 `ValidateRule()` 方法调用。
如果您查看 `DomainObject` 类,大约在中间位置,您会看到一个名为 `GetBrokenRules()` 的方法
public virtual ReadOnlyCollection<Rule> GetBrokenRules(string property) {
property = CleanString(property);
// If we haven't yet created the rules, create them now.
if (_rules == null) {
_rules = new List<Rule>();
_rules.AddRange(this.CreateRules());
}
List<Rule> broken = new List<Rule>();
foreach (Rule r in this._rules) {
// Ensure we only validate a rule that
// applies to the property we were given
if (r.PropertyName == property || property == string.Empty) {
bool isRuleBroken = !r.ValidateRule(this);
// [...Snip...]
if (isRuleBroken) {
broken.Add(r);
}
}
}
return broken.AsReadOnly();
}
首先,我们检查 `CreateRules()` 方法是否已被调用,如果没有,我们将*第一次*调用它。然后,我们遍历所有返回的规则。如果规则适用于作为方法参数传递的属性名称,或者没有指定属性名称,则调用抽象的 `ValidateRule()` 方法。如果该方法返回 false
,则将该规则添加到要返回给调用者的列表中。
由于 `IDataErrorInfo` 存在的属性索引器,它利用了 `GetBrokenRules()`
public virtual string this[string propertyName] {
get {
string result = string.Empty;
propertyName = CleanString(propertyName);
foreach (Rule r in GetBrokenRules(propertyName)) {
result += r.Description;
result += Environment.NewLine;
}
result = result.Trim();
if (result.Length == 0) {
result = null;
}
return result;
}
}
在调用 `GetBrokenRules()` 之后,它会遍历每一个规则,如果它们适用,就会被添加为一个长长的错误消息列表来返回。例如,如果索引器以“Name”作为参数被调用,那么结果中将只包含适用于 `Name` 属性的规则。
我们作为 `IDataErrorInfo` 必须实现的另一个属性 `Error`,它调用索引器时不指定属性名称。在这种情况下,索引器将验证所有规则,这正是 `Error` 属性应该返回的内容。
结论
我认为我的方法为开发者提供了很多强大的功能,因为他们不只局限于使用委托作为规则。`Rule` 类被设计为遵循策略模式,因此您可以简单地通过继承 `Rule` 来创建常见的、重复出现的规则,例如 `NotBlankRule`、`DateRangeRule`,甚至是使用多个属性并且会调用 Web 服务进行验证的非常复杂的规则。
我认为我将总结这篇博文,指出在实现业务规则方面,确实没有单一的最佳解决方案。它总是因项目而异,我们作为开发人员,需要做出最佳选择。本文只是尝试概述一种可能的替代方案,您在开发自己的业务规则系统时可以考虑,并讨论了我认为任何业务规则系统都应该具备的重要属性。
修订历史
- 2006 年 5 月 24 日 - 上传了演示项目。
最后一点!
我还想说,Windows Forms 中的数据绑定功能非常丰富,利用它们来创建非常丰富的 GUI 并不需要费多少力。如果您还没有听说过 `IDataErrorInfo`、`INotifiesPropertyChanged` 或 `IEditableObject`,那么您真的应该到 MSDN 上去看一下与数据绑定相关的接口列表,这对您非常有益。