领域模型和“业务逻辑”混淆问题

3
每当我阅读一篇关于现代设计模式,如MVVM或DDD的文章时,我都很难将示例翻译成我通常工作的领域。所有这些模式都得出结论,即领域模型应该存在于自己的小气泡中,没有任何引用,不应该暴露给视图进行绑定,应该是POCOs / POJOs并包含“业务逻辑”。我总是问自己的问题是:那么领域模型应该做什么?答案显然是“处理业务逻辑”,但是当我考虑可能是什么时,我很难找到真实世界的例子。
例如:一个常见的例子是金融应用程序,您可以有一个BankAccount实体,它可以有一个TransferMoneyTo(otherAccount)函数。理论上听起来很好,但在现实世界中,这个应用程序不会管理全球所有银行账户,而只是一个银行的账户。因此,真实世界的应用程序必须以某种方式联系另一家银行的服务器来启动此交易。这个“某种方式”显然是一个服务BankAccount不允许引用它。这意味着这不是一个非常好的孤立领域模型的例子。
到目前为止,我读过的所有例子都是这样的,要么是因为忽略了重要细节而导致的示例成功,要么是微不足道的。我所说的微不足道是指,“业务逻辑”仅包含简单的验证(例如,必填字段)。
所有这些都导致了贫血领域模型(除了验证),这被认为是一件坏事。
我的问题是:除了验证之外,“业务逻辑”这个术语背后隐藏着什么,这是否可以证明需要一个单独的领域模型?
注:我知道这取决于您正在处理的领域,但我认为至少提供一些实际上使用DDD会很有用的示例会很好。

2
我之前也有些困惑,但后来找到了这篇精彩的文章。或许对您也会有所帮助: http://www.codeproject.com/Articles/10746/Dude-where-s-my-business-logic - Guanxi
3个回答

2
这里的“某种方式”显然是指银行账户不允许引用的服务。这意味着这并不是一个很好的隔离领域模型的例子。
虽然BankAccount本身没有对这个服务的引用,但它仍然可以与这样的服务进行交互。
举个更简单的例子,让我们来看一下利息的计算。天真的方法可能是:
public BankAccount 
{
    public decimal Balance { get; set; }
    public decimal Interest { get; set; }
    private public List<Transaction> transactions = new List<Transaction>();
    public List<Transaction> Transactions { get { return transactions; } } 

    public decimal CalculateInterest() 
    {
        return Balance * Interest;
    }
}

// inside a service
BankAccount account = ...;
var interest = account.CalculateInterest();
account.Balance += interest;
account.AddTransaction(new Transaction() { Description = "Monthly Interest", Amount = interest });

这样做不好,因为你现在有混合责任。计算利息不是BankAccount类的重点,而且它现在涉及到多个责任,比如计算,这可能会随着几个因素的变化或依赖而改变。

public BankAccount 
{
    // private setters, so no one outside BankAccount can update it directly
    public decimal Balance { get; private set; }
    public AccountType AccountType { get; private set; } // assume business and private account
    private public List<Transaction> transactions = new List<Transaction>();
    // return as "AsEnumerable" so user can't later cast it back to list and
    // directly add Transactions, skipping the AddTransaction method
    public IEnumerable<Transaction> Transactions { get { return transactions.AsEnumerable(); } } 

    public void CalculateInterest(IInterestCalculator calc) 
    {
        decimal interest = calc.CalculateInterest(this);
        this.AddTransaction(new Transaction() { Description = "Monthly Interest", Amount = interest });
    }

    public void AddTransaction(Transaction transaction) 
    {
        var newBalance = this.Balance + transaction.Balance;

        if(this.transaction.Amount < 0 && newBalance < this.Limit) 
        {
            // new balance would exceed the accounts limit
            throw new NotEnoughFundsException();
        }

        this.transactions.Add(transaction);
        this.Balance = newAmount;
    }
}

public interface IInterestCalculator 
{
    decimal CalculateInterest(Bankaccount);
}

public class DefaultAccountInterestCalculator : IInterestCalculator
{
    public decimal CalculateInterest(BankAccount account) 
    {
        // for sake of simplicity, inlined
        return account.Balance * 0.02;
    }
}
public class PremiumAccountInterestCalculator : IInterestCalculator 
{
    private const decimal Threshold = 10000m;
    public decimal CalculateInterest(BankAccount account) 
    {
        // Premium customers receive increased interest, above a certain threshold. 3% for the balance above the threshold of 10000 USD
        if(account.Balance > Threshold) 
        {
            return (decimal)((Threshold * 0.02) + (account.Balance-Threshold) * 0.03);
        } 
        else 
        {
            return (decimal)(account.Balance * 0.02);
        }
    }
}

您的服务中

BankAccount account = ...;
IInterestCalculator calculator = (account.AccountType == AccountType.Premium)?new PremiumAccountInterestCalculator():DefaultAccountInterestCalculator();

BankAccount account.CalculateInterest(calculator);

现在你的BankAccount类只有一个职责,即维护其状态和所需的业务逻辑(例如检查余额是否足够,仅允许通过方法操作银行账户而不是直接更改Balance或操纵List<Transaction>)。
计算由计算器类完成,这些类被传递到BankAccountCalculateInterest方法中。服务包含所需的逻辑,既不适合计算器也不适合银行账户类。
简而言之:业务逻辑(在丰富的领域模型中)是维护类的状态所需的所有逻辑,并尽可能地将其封装起来。在第二个类中,不能直接更改余额。需要AddTransactionCalculateInterest(用于计算利息)。
这保证了(假设它是并发安全的BalanceTransactions始终处于一致的状态(即永远不会漏掉添加任何交易或更新余额)。

在这个例子中,为什么一个账户需要知道它需要计算利息、奖金和其他东西呢?难道让账户只能增加和减少它所拥有的钱,并由服务或其他外部实体来计算增加或减少的金额并调用账户的increase()方法不是更好吗? - Álvaro García

2
“业务逻辑”这个术语背后隐藏了什么?
很多领域模型反映了业务流程,因此包含状态机,在其中您可以根据一些规则将事物从已知的有效状态转换到另一个状态。几乎所有企业都有这种流程。其他领域可能涉及更复杂的内部算法和数据转换。
除非您认为铁路公司的座位预订系统或政府的税收计算过程是“验证”,否则它们很难归入简单的“只是验证”的类别。
关于领域与外界通信,这并不是它们的责任。通常情况下,领域会发出事件,说明“这就是发生的!”应用上下文处理它并启动与外部系统的适当通信。
编排对内部和外部子系统的调用,以便数据在应用程序中流动进入、流出和通过,这不是领域逻辑,而是技术应用级别的问题。控制反转,以某种形式(事件、DI等),通常是保持领域不知道这一点的关键。

2
DDD的一个关键是在代码中明确领域概念。我对银行业务一窍不通,但我猜你可以将其建模为两个状态:TransactionPending和TransactionCompleted。应用层会让领域意识到周围的变化。 - guillaume31
1
关于火车的例子,DDD将聚合作为修改的单个入口点。决策和操作所需的所有数据必须是要么传递给聚合,要么包含在其中。请注意,聚合不是单个类,而是一组相关的类。您可以从中轻松地推导出座位预订的设计。 - guillaume31
我只是举了银行的例子,因为其他人都这么做。主要问题在于模型如何与其无法控制的外部世界保持一致。你是指我的模型类有一些属性反映出发出了交易,可能还有一个方法像 TransactionCompleted(transaction),在成功或失败的情况下执行所有业务逻辑(即实际提取资金)吗? - Karsten
我想我得研究一下聚合根...也许那会回答我的一些问题。 - Karsten
2
当然,模型不能与外部世界“即时一致”,因此当世界回应我们时,我们会使其“最终一致”(这里也涉及到最终一致的关键概念)。 - guillaume31
显示剩余2条评论

0
我有一些用php编写的足球比赛管理软件(不完全是oop的巅峰)。
我的业务逻辑包括一个积分计算器,根据球队比赛结果确定他们的排名。解决平局的策略可能会有点复杂。其他业务逻辑包括比赛安排、裁判指派和志愿者协调。
这些逻辑位于我认为是域层的位置。实体本身(比赛等)往往是贫血的。除了保存数据外,它们没有太多要做的事情。但对我来说没关系。真正的工作是在服务类中进行。
我使用了限界上下文和聚合根的概念。我的比赛实体包括球队、官员和场地。当我处于比赛上下文时,比赛是王者,负责管理它的子元素。在场地管理上下文中,场地是老大,比赛只是附带品。
我实现了持久性独立性。我可以根据域要求建模我的域实体和值对象,而不必担心它们最终会出现在哪些表中。ORM层负责映射,并允许我在多个表之间存储一个域实体,反之亦然。
重点是,选择DDD中有助于你特定应用程序的部分。不必担心其他方面。

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