如何避免领域模型贫血,并将业务逻辑形式化为规则?

7
我正在设计一个系统,它有一个基于简单Entity Framework支持的域对象,其中包含一些需要根据一系列规则进行更新的字段。我希望逐步实现这些规则(以敏捷的方式),而且由于我正在使用EF,我对将每个规则放入域对象中感到怀疑。然而,我想避免编写"过程性代码"并使用贫血的领域模型。所有这些都需要可测试性。
例如,对象是:
class Employee { 
 private string Name; 
 private float Salary; 
 private float PensionPot;
 private bool _pension;
 private bool _eligibleForPension;

}

我需要建立一些规则,例如“如果工资高于100,000且不符合养老金要求,则将符合养老金要求标记为true”,以及“如果符合养老金要求,则将符合养老金要求标记为true”。大约有20个这样的规则,我想知道它们应该在员工类中实现还是像EmployeeRules类那样?我的第一反应是为每个规则创建一个继承自“Rule”的单独类,然后将每个规则应用到员工类上,也许使用Visitor模式,但是这样做需要将所有字段都暴露给规则,所以感觉不太对。然而,将每个规则放在Employee类中也不太对。这应该如何实现呢?
第二个问题是,实际的员工是Entity Framework实体,支持DB,因此我不希望向这些“实体”添加逻辑-特别是当我需要模拟对象以对每个规则进行单元测试时。如果它们具有我正在测试的规则,则如何模拟它们?
我一直在考虑使用AutoMapper将其转换为简单的领域对象,然后再应用规则,但我需要自己管理字段的更新。对此有什么建议吗?
2个回答

7

一种方法是将规则作为Employee的内部类。这种方法的好处是可以保持字段的私有性。此外,规则的调用可以被Employee类本身强制执行,确保在需要时始终调用它们:

class Employee
{
    string id;
    string name;
    float salary;
    float pensionPot;
    bool pension;
    bool eligibleForPension;

    public void ChangeSalary(float salary)
    {
        this.salary = salary;
        ApplyRules();
    }

    public void MakeEligibleForPension()
    {
        this.eligibleForPension = true;
        ApplyRules(); // may or may not be needed
    }

    void ApplyRules()
    {
        rules.ForEach(rule => rule.Apply(this));
    }

    readonly static List<IEmployeeRule> rules;

    static Employee()
    {
        rules = new List<IEmployeeRule>
        {
            new SalaryBasedPensionEligibilityRule()
        };
    }

    interface IEmployeeRule
    {
        void Apply(Employee employee);
    }

    class SalaryBasedPensionEligibilityRule : IEmployeeRule
    {
        public void Apply(Employee employee)
        {
            if (employee.salary > 100000 && !employee.eligibleForPension)
            {
                employee.MakeEligibleForPension();
            }
        }
    }
}

这里的一个问题是Employee类必须包含所有规则实现。虽然这不是一个大问题,因为规则体现了与员工养老金相关的业务逻辑,它们应该放在一起。


这看起来可以解决问题。虽然我给出了一个使用私有字段的示例,这是我想要的设计,但 EF 有公共属性,所以如果我直接访问 Employees 类,我就不必使用内部类。我希望有人能回答包括 EF 部分的问题,因此我会暂时保持问题开放状态。谢谢! - PCurd
我使用稍微修改的方法构建了系统的演示模型 - 我没有将规则作为内部类 - 它运行良好且感觉良好。我知道对于这个系统,将字段设置为公共并不是一个可怕的罪行,但我可能会再次尝试使用内部类来看看感觉如何。谢谢你的帮助。 - PCurd

4

业务规则通常是一个有趣的话题。聚合/实体不变量和业务规则之间可能确实存在差异。业务规则可能需要外部数据,我不同意规则改变聚合/实体。

对于规则,您应该考虑使用规范模式。规则基本上只返回是否违反以及可能的描述。

在您的例子中,eulerfx 使用的 SalaryBasedPensionEligibilityRule 可能需要一些 PensionThreshold。这个规则看起来更像一个任务,因为规则并没有检查实体的任何有效性。

因此,我建议规则是决策机制,而任务是用于更改状态。

话虽如此,您可能想要征求实体的建议,因为您可能不想公开状态:

public class Employee
{
    float salary;
    bool eligibleForPension;

    public bool QualifiesForPension(float pensionThreshold) 
    {
        return salary > pensionThreshold && !eligibleForPension;
    }

    public void MakeEligibleForPension()
    {
        eligibleForPension = true;
    }
}

这遵循了命令/查询分离的思想。

如果您直接从ORM对象构建,但不想要或无法包含所有行为,则可以接受,但这肯定会有所帮助 :)


一种有趣的方法,更符合“将逻辑与对象保持一致”的口号。您最终会使每个业务逻辑片段都在Employee对象上实现,然后由另一个对象负责询问“您是否满足此规则的条件?然后触发此更新”?我可以看到这变得非常复杂,但也保持了所有逻辑与Employee对象的关联。我想这取决于在Employee对象上具有许多条件和更新是否可取。您能举个例子,另一个处理更新的对象会是什么样子吗? - PCurd
你能举个例子来说明另一个对象如何进行更新的封装吗?--- 我真的*不太明白这个 :) --- 你能重新表达一下你的问题吗? - Eben Roux
抱歉!必须有一个函数做“如果员工符合养老金资格(100000),则使员工有资格获得养老金;”。这将如何影响Employee的关系 - 它会是一个单独的类吗?如果是,它会是什么样子? - PCurd
啊,好的 :) --- 我说的是(操作脚本)在一个单独的类中的“任务”部分。这里有一个链接:http://my.safaribooksonline.com/book/web-development/9780321669636/reference-to-external-patterns/app01lev1sec1,其中定义得很好:操作脚本[POEAA]包含指导域模型[POEAA]中实体(即对象)活动的应用程序逻辑。每个脚本通常在应用程序域中实现一个用例。操作脚本与事务脚本[POEAA]不同之处在于它们将大部分工作委托给域对象。 - Eben Roux
这听起来很有趣,对于一个更大的系统来说,我认为这里有价值,但是我已经决定对于这个系统,在这个规模上,eulerfx的方法对我来说更实用。非常感谢,我学到了一个新技巧! - PCurd

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接