使用 BFsharp 编写简洁有效的代码






4.83/5 (10投票s)
BFsharp 是一个跨平台的开源库,提供多种对象服务,可用于各种应用程序中。
BFsharp 是什么?
它是一个开源框架,有助于在各种应用程序中创建可测试、可维护和可扩展的代码。它提供的功能可用于业务层构建您的域。BFsharp 可用于用户界面层(如 MVVM 模式),以更快、更轻松地提供更丰富的用户体验。它对数据和行为的可扩展性提供了强大的支持,允许您构建最终用户可以自定义的应用程序。更重要的是,BFsharp 使您的代码更灵活;您的设计将更能适应需求的变化。
这个项目现在应该有两年了。我是在参与一个项目开发期间开始使用 BFsharp 的。现在,它已成功应用于多个生产系统。该框架是跨平台的,这意味着 .NET、Silverlight 和 WP7 都可以访问相同的 API。该项目目前可以在这里找到,不久的将来还可以在这里找到。BFsharp 会不定期获得新功能。
下面是框架功能的粗略列表,但并不详尽。我将在本文中详细介绍如何使用其中一些功能,并在未来的文章中介绍其他功能
- 规则 - 它们允许定义可重用代码,这些代码可以响应属性更改而执行(策略模式)。规则可以计算值(业务规则)、验证条件(验证规则)或执行操作(动作规则)。
- 规则具有优先级。
- 规则支持异常处理。
- 规则有几种模式,用于控制其行为方式。
- 规则可以对属性更改和集合更改做出反应。依赖关系会自动从规则定义中获取或手动指定。
- 规则可以抑制其他规则。
- 规则对数据绑定提供强大的支持,无需额外代码即可实现丰富的用户体验。
- 规则可以分组,并根据任意条件启用/禁用。
- 规则可以针对客户端/服务器上下文进行不同配置。
- 支持 WCF 序列化。
- 实体具有 IsValid 属性,指示所有 ValidationRules 是否有效。
- 验证支持分层实体。有关详细信息,请参阅实体管理。
- 有规则和实体原型机制。
- 验证可以使用属性定义。
- 规则实现重用和关注点分离。
- POCO 支持。
- DynamicProperties - 实体可以在运行时动态扩展。
- DynamicProperties 可用于规则公式。
- DynamicProperties 支持动态或强模式。
- 生成动态类型,因此所有使用反射的框架都可以工作。
- 支持 WCF 序列化。
- 支持 DynamicProperties 原型。
- Formula DSL - 一种文本语言,使最终用户能够定义公式、谓词和其他代码。
- 可用于自定义功能。
- 规则可以使用公式编写。
- 可从 DSL 访问的方法和属性可以控制,从而提供更高的安全性。
- IsDirty - 响应属性更改将实体标记为脏。
- 实体管理 - 将实体分组到聚合中 - 领域驱动设计中的概念。它允许在逻辑组中管理实体。以发票为例。如果任何发票行(子项)被修改或验证,则应通知发票(父项)以做出相应反应。
- 它既可以在业务层使用,也可以在 ViewModel 层次结构中使用。
- 定义了几种策略,并且可以扩展。
在本文中,我将尝试通过一些示例来介绍 BFsharp 的基本概念。
入门
我们首先将 BFsharp 添加到示例项目中。您只需引用 _BFsharp.dll_ 和 _BFsharp.AOP.dll_(Silverlight 程序集以 SL 结尾,Windows Phone 以 WP7 结尾)或使用nuget自动完成。
标准代码
让我们以 InvoiceLine 作为第一个示例。它基本上是发票或收据中的一个位置。它的职责是计算所购产品的总价。
实体有几个属性:名称、数量、价格和总计。
public class InvoiceLine
{
public string Name { get; set; }
public decimal Quantity { get; set; }
public decimal Price { get; set; }
public decimal Total { get; set; }
}
控制实体的主要规则是
Total = Quantity*Price
此功能的一种可能实现是使 Total 属性为只读
public decimal Total { get { return Quantity*Price; } }
这很简单,但解决方案非常有限。现在想象一下,该实体在 Silverlight 或 WPF 中用作数据上下文。为了响应用户输入,该实体必须实现 INotifyPropertyChanged
。当然,这可以手动完成
public class InvoiceLine : INotifyPropertyChanged
{
private string _name;
public string Name
{
get { return _name; }
set
{
_name = value;
RaisePropertyChanged("Name");
}
}
private decimal _quantity;
public decimal Quantity
{
get { return _quantity; }
set
{
_quantity = value;
RaisePropertyChanged("Quantity");
RaisePropertyChanged("Total"); // Open/closed principle is broken here
}
}
[...]
public decimal Total
{
get { return Quantity*Price; }
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string name)
{
var e = PropertyChanged;
if (e != null)
(this, new PropertyChangedEventArgs(name));
}
}
然而,上述代码有几个缺点
- 不能使用自动属性,因此代码更长且难以阅读。
- 每个属性都必须调用 RaisePropertyChanged - 这是重复的机械编码。
- 硬编码的属性名称阻止了正确的重构(例如使用 R#)
- 如果存在计算属性(Total),则必须从依赖属性中调用 RaisePropertyChanged,这违反了开/闭原则。请注意数量的实现。
- 在某些情况下,WCF 序列化是不可能的。
- 规则既不能更改也不能重用。
还有一个业务规则使解决方案更加复杂。有时收银员输入的是行总价而不是数量,例如在加油站。
Quantity = Total/Price
将此场景实现到此实体中并不那么简单。您必须防止属性相互设置,从而导致堆栈溢出。想象一下,如果我们再添加两个属性 Tax 和 TotalWithTax
会发生什么。我将把这个练习留给读者。
BFsharp 代码
方面
为了解决上述问题,可以使用 BFsharp。首先,让我们使用切面范式,而不是手动实现 INotifyPropertyChanged
。
BFsharp 提供了几个可以使用属性应用的 PostSharp 切面。其中之一是 NotifyPropertyChangedAttribute
。在主编译之后,PostSharp 会对程序集进行后处理并添加适当的代码。PostSharp 可以从这里下载 - 免费版本就足够了。现在代码可以简化为
[NotifyPropertyChanged]
public class InvoiceLine
{
public string Name { get; set; }
public decimal Quantity { get; set; }
public decimal Price { get; set; }
[NotifiedBy("Quantity", "Price")]
public decimal Total { get { return Quantity*Price; } }
}
Business Rules
现在是时候外部化 Total 规则了。可以按如下方式完成
[NotifyPropertyChanged]
public class InvoiceLine : EntityBase<InvoiceLine>
{
public string Name { get; set; }
public decimal Quantity { get; set; }
public decimal Price { get; set; }
public decimal Total { get; set; }
public InvoiceLine()
{
Extensions.CreateBusinessRule(e => e.Quantity*e.Price, e => e.Total)
.Start();
}
}
首先,实体必须派生自 EntityBase<>
(BFsharp 也可以在不干扰继承层次结构的情况下启用)。基类有一个重要的属性 Extensions。它包含几组负责 Rules
、DynamicProperties
、IsDirty
和实体管理的方法和属性。BFsharp 的主要 API 是流畅 API;但是,也有一个标准的 .NET API,可以在其中设置属性。
CreateBusinessRule 方法创建一个规则,该规则负责从公式 (e.Quantity*e.Price) 计算值,然后将其插入目标属性 (e.Total)。请注意,没有任何地方指定属性依赖关系。在内部,CreateBusinessRule 不将 Func<>
作为参数,而是将 Expression<>
作为参数,因此可以分析公式并自动查找依赖关系 - 这称为自动依赖分析。如果其中一个依赖属性更改,则会重新评估规则。
为了满足第二个要求(从总数计算),可以添加另一个规则
Extensions.CreateBusinessRule(e => e.Price == 0 ? 0 :
e.Total/e.Price, e => e.Quantity).Start();
规则抑制
好奇的读者会注意到,在某些情况下,这两个规则都会被评估,从而覆盖彼此的值。解决该问题的一种方法是使用规则抑制
var rule = Extensions.CreateBusinessRule(e => e.Quantity*e.Price, e => e.Total)
.Start();
var rule2 = Extensions.CreateBusinessRule
(e => e.Price == 0 ? 0 : e.Total/e.Price, e => e.Quantity)
.Start();
rule.MutuallySuppressedBy(rule2);
现在,如果规则被评估并设置 Total 属性,则 rule2
不会运行。如果 rule2
被评估并设置 Quantity 规则,则不会运行。
InitializeRules
BFsharp 的设计目标之一是支持通过 Visual Studio 链接在客户端和服务器端使用实体的架构。在某些情况下,实体用作简单的 数据传输对象,这意味着它可以使用 WCF 发送。具有只读属性的实体可以轻松序列化和反序列化,但是更复杂的解决方案(如所呈现的解决方案)中存在属性设置顺序的依赖关系则不能。这是因为属性反序列化的顺序是未知的。BFsharp 定义了一种机制来帮助解决这种情况。与其在构造函数中创建规则,不如将它们定义在带有 RuleInit 属性的方法中(如果您使用了 nuget,则已经有一个名为 ri 的代码片段)
[NotifyPropertyChanged]
public class InvoiceLine : EntityBase<InvoiceLine>
{
public string Name { get; set; }
public decimal Quantity { get; set; }
public decimal Price { get; set; }
public decimal Total { get; set; }
[RuleInit]
public void RuleInit()
{
var r = Extensions.CreateBusinessRule(e => e.Quantity*e.Price, e => e.Total)
.Start();
var r2 = Extensions.CreateBusinessRule
(e => e.Price == 0 ? 0 : e.Total/e.Price, e => e.Quantity)
.Start();
r.MutuallySuppressedBy(r2);
}
}
默认情况下不创建规则,因此实体可以作为 DTO 使用,并且可以毫无问题地进行序列化和反序列化。如果需要规则,可以通过调用以下方法进行初始化
invoiceLine.Extensions.InitializeRules();
文本规则
如前所述,在某些情况下,如果用户或支持人员可以更改业务规则,那将是非常棒的。使用 BFsharp,可以从字符串创建规则
Extensions.CreateBusinessRule("Total=Quantity*Price")
.Start();
这是一种名为 formula 的语言,类似于 C# 并提供其子集。可以通过打开和关闭来控制可从其访问的方法和属性。代码在运行时编译为本机代码,因此在第一次使用后,它与正常编写的规则一样快;WP7 是一个例外,其中代码是解释执行的。
动态属性
可扩展性不仅是行为,也包括数据。BFsharp 定义了一个称为动态属性的概念——可以附加到实体的额外数据。它支持 WCF 序列化,并可在规则公式中使用。利用这两个功能,最终用户可以非常容易地扩展系统。例如,她可以向发票行添加属性 Discount 并修改业务规则,从而实现一个简单的忠诚度功能。
var invoiceLine = new InvoiceLine();
invoiceLine.Extensions.AddProperty<decimal>("Discount");
invoiceLine.Extensions.CreateBusinessRule("Total=Quantity*Price*Discount")
.Start();
所呈现的场景允许在不重新编译代码的情况下扩展数据(属性)和行为(规则)。
验证规则
在前面的例子中,使用了一种类型的规则,即 BusinessRule
。在这个场景中,我将展示另一种类型 - ValidationsRule
。我们以下面的用户注册表单为例
[NotifyPropertyChanged]
public class UserRegistrationForm : EntityBase<UserRegistrationForm>
{
public string UserName { get; set; }
public string Password { get; set; }
public string ConfirmPassword { get; set; }
public DateTime Birthday { get; set; }
public bool AcceptLicence { get; set; }
}
有几个验证规则控制实体;其中一个要求用户至少18岁。为了添加这样的约束,我们创建一个规则
Extensions.CreateValidationRule(r => DateTime.Now.AddYears(-18) > r.Birthday)
.WithMessage("You should be at least 18 years old.")
.WithOwner(r=>r.Birthday)
.Start();
与所有规则一样,它也有自动依赖分析。这意味着如果生日更改为小于 18 岁,则规则变为无效,并且实体被标记为无效。当谓词变为 false 时,规则会创建一个特殊对象 - BrokenRule。它指定了错误原因、严重性以及其他一些属性。可以使用 WithMessage 方法设置 BrokenRule 消息。
属性规则
添加验证规则的另一种方法是通过属性——一种添加基本规则的简单便捷方法。有几个预定义的属性
- 必需
- MaxLength
- 电子邮件
- 模式
- Compare
- Range
- 应为
使用上述属性,实体可以按如下方式编写
[NotifyPropertyChanged]
public class UserRegistrationForm : EntityBase<UserRegistrationForm>
{
[Email, Required]
public string UserName { get; set; }
[Required]
public string Password { get; set; }
[Required]
public string ConfirmPassword { get; set; }
public DateTime Birthday { get; set; }
[ShouldBe(true)]
public bool AcceptLicence { get; set; }
[RuleInit]
public void RuleInit()
{
Extensions.CreateValidationRule(r => string.IsNullOrEmpty(ConfirmPassword)
|| r.Password == r.ConfirmPassword)
.WithMessage("Confirm password doesn't match.")
.WithOwner(r => r.ConfirmPassword)
.Start();
Extensions.CreateValidationRule(r => DateTime.Now.AddYears(-18) > r.Birthday)
.WithMessage("You should be at least 18 years old.")
.WithOwner(r => r.Birthday)
.Start();
}
}
如果规则开启了自动依赖分析,它们会实时验证,并且其状态始终更新。这可以在 Extensions.IsValid
属性中检查。如果值为 false,则可以从 Extensions.BrokenRules
中读取损坏的规则。此外,如果代码在 Silverlight 中执行,实体会与标准验证机制集成,并在控件旁边显示错误。
行动规则
在最后一个示例中,我将展示第三种主要规则类型的使用 - ActionRule
。它是一种规则,当属性更改时,它会调用一些任意代码。它可以在 MVVM 设计模式中广泛使用。
让我们以上一个例子中的实体为例。假设我们想添加检查 UserName
是否可用的功能。该实体在服务器和客户端都使用,因此修改设置器不是一个选项(或者有时我们甚至无法修改实体)。我们必须订阅 UserName
属性更改事件,并做出相应反应,并在完成后取消订阅。手动编写它很麻烦。ActionRule
正是为了这种场景而创建的。另请注意,我们现在从外部扩展实体。
public class UserRegistrationFormViewModel
{
public UserRegistrationForm Form { get; private set; }
public UserRegistrationFormViewModel(UserRegistrationForm form)
{
Form = form;
Form.Extensions.InitializeRules();
Form.Extensions.CreateActionRule(x => CheckAccountAccessibility(x.UserName)).Start();
}
private readonly BrokenRule _rule = new BrokenRule
("UserName is already used.", BrokenRuleSeverity.Error,
"UserName");
public void CheckAccountAccessibility(string userName)
{
if (userName == "Michael") // Simulate server
Form.Extensions.BrokenRules.Add(_rule);
else
Form.Extensions.BrokenRules.Remove(_rule);
}
}
响应 UserName 更改,将调用 CheckAccountAccessibility
方法。它会检查服务器,并在用户名已被占用时添加一个中断规则。通过这种方法,您可以轻松地将规则执行订阅到所有实现 INotifyPropertyChanged
或 INotifyCollectionChanged
的内容。
正如您所看到的,规则可以帮助构建更简洁的代码。它们可以减少执行例行任务所需的编码量。此外,规则可以提取并应用于其他地方,使它们成为可重用组件。
摘要
本文只是 BFsharp 的入门介绍,请尝试使用它并更熟悉它。在许多类型的应用程序中,还有更多功能可以在不同的情况和场景中使用。我将在接下来的文章中介绍它们。感谢您的阅读。