65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (10投票s)

2011年8月12日

CPOL

10分钟阅读

viewsIcon

41834

downloadIcon

399

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自动完成。

add_ref.png

add_ref2.png

标准代码

让我们以 InvoiceLine 作为第一个示例。它基本上是发票或收据中的一个位置。它的职责是计算所购产品的总价。

InvoiceLine.jpg

实体有几个属性:名称、数量、价格和总计。

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));
    }
}

然而,上述代码有几个缺点

  1. 不能使用自动属性,因此代码更长且难以阅读。
  2. 每个属性都必须调用 RaisePropertyChanged - 这是重复的机械编码。
  3. 硬编码的属性名称阻止了正确的重构(例如使用 R#)
  4. 如果存在计算属性(Total),则必须从依赖属性中调用 RaisePropertyChanged,这违反了开/闭原则。请注意数量的实现。
  5. 在某些情况下,WCF 序列化是不可能的。
  6. 规则既不能更改也不能重用。

还有一个业务规则使解决方案更加复杂。有时收银员输入的是行总价而不是数量,例如在加油站。

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。它包含几组负责 RulesDynamicPropertiesIsDirty 和实体管理的方法和属性。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();

InvoiceLineSample.jpg

文本规则

如前所述,在某些情况下,如果用户或支持人员可以更改业务规则,那将是非常棒的。使用 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 中执行,实体会与标准验证机制集成,并在控件旁边显示错误。

UserRegistrationForm.png

行动规则

在最后一个示例中,我将展示第三种主要规则类型的使用 - 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 方法。它会检查服务器,并在用户名已被占用时添加一个中断规则。通过这种方法,您可以轻松地将规则执行订阅到所有实现 INotifyPropertyChangedINotifyCollectionChanged 的内容。

正如您所看到的,规则可以帮助构建更简洁的代码。它们可以减少执行例行任务所需的编码量。此外,规则可以提取并应用于其他地方,使它们成为可重用组件。

摘要

本文只是 BFsharp 的入门介绍,请尝试使用它并更熟悉它。在许多类型的应用程序中,还有更多功能可以在不同的情况和场景中使用。我将在接下来的文章中介绍它们。感谢您的阅读。

© . All rights reserved.