如何在领域驱动设计中使用多态实体进行领域建模?
How to Approach Domain Modelling with Polymorphic Entity in Domain-Driven Design?
我正在重建一个会计软件。该软件可以处理多种类型的交易,例如销售、库存补货和费用。我没有会计背景,但我在网上学到了一些概念。我正在尝试根据这些原则对域进行建模:
- 一个日记帐分录由多个记录组成,每个记录要么是借方,要么是贷方。在给定的日记帐分录中,这些记录的借方和贷方总额必须相等(平衡)
- 必须根据源文档创建日记帐分录。不能无缘无故地简单地创建一个日记条目。
根据这些,我得出结论,必须存在这些实体:
JournalEntry
个实体,其中有许多 JournalEntryItem
个值对象。该实体负责保持其所有条目处于平衡状态。
SourceDocument
实体,其中包含正在进行的交易的一些信息
JournalEntity
和SourceDocument
都是聚合根,JournalEntity
引用SourceDocument
身份。
问题来了。作为日记帐分录创建基础的源文档可以是具有不同行为的不同事物。例如,一个'expense'源文件可能由一对费用类别和费用金额的列表组成,而一个'sales'源文件可能由相关存货、每个存货的金额和每个存货的价格组成每一个单元。
这让我想到了模型中的另一种方法:
- 创建一个摘要
SourceDocument
class,具有创建日期和身份属性等共享属性
- 创建
SalesSourceDocument
、ExpenseSourceDocument
等,从摘要 SourceDocument
扩展而来
JournalEntry
实体仍然需要引用 SourceDocument
身份。
虽然这很有道理,但我认为这不是在 DDD 中对问题建模的最佳方式,特别是因为它需要有一个抽象实体。
从我在网上阅读的有关DDD的资料来看,当遇到需要抽象实体的问题时,我需要将其分离到不同的限界上下文中。但是,我不确定它是否可以完成,因为 SourceDocument
身份可能 return 在 'sales' 上下文中的具体 SalesSourceDocument
,但它也可能 return 'expense' 上下文中没有任何内容。
解决这个问题的最佳方法是什么?
What is the best way to approach this problem?
将您的单独用例视为单独的,直到您发现需要调整它们的领域问题。
您的销售单据和费用单据是不同的东西,您的数据模型应该反映出:与每个单据相关的信息存储在不同的表中;这些表的架构会随着对特定于它们管理的域的更改的响应而发展。
Duplication is far cheaper than the wrong abstraction -- Sandi Metz
我认为这里要认识到的重要一点是 源文档 术语是描述 期刊条目 和 文档,从JournalEntry
的角度来看。文档是它们自己的 AR,它很可能在没有 journal 的情况下存在,那么为什么要根据与另一个 AR 的关系来命名它们的抽象?
源文档很可能是描述与期刊条目相关的文档的必要术语,而且它很可能是您的模型也是如此,但可能采用值对象的形式,例如 SourceDocument { id, type }
.
这并不意味着如果有用的话,您也不能拥有 Document
抽象:没有理由不惜一切代价避免有用的多态性。
我的分析可能有误(无论如何所有模型都是错误的),但我只是想给你一个关于特定建模方面的不同视角。
I am rebuilding a software in accounting. This sofware can handle many
types of transactions, e.g. sales, inventory restock, and expenses. I
have no background in accounting, but I learned some concepts online.
如果您可以在线了解您的整个域,那么它很可能是一个通用的子域,您很可能可以在其中购买现成的产品。与其上网尝试自己想出一个模型,不如让领域专家参与进来:也许你的整个愿景在这里都是错误的。
此外,考虑到 源文档 很可能存在于不同的上下文中,Accounting 上下文似乎可以充当支持不明确了解或不依赖其他 BC 的 BC。
这是其中一个答案可能会变得很长的问题,所以我会尽力保持简短。我不是会计专家,但在我的职业生涯中我不得不实施 很多 会计。因此,当我有时间(一天)时,我会 developing one of those generic sub-domains。
我的一个建议是不要将 classification 构建为代码结构。然而,识别 class化并不总是那么容易。一个“简单”的例子可能是 Customer
。我们可以有 GoldCustomer
和 SilverCustomer
。我们可以从 abstract Customer
中提取 class 这些。我们必须决定我们为什么要这样做。例如,如果要确定每个周期需要的最低订单量,并且 upgrade/downgrade 客户 Type
才有资格获得一些折扣,那么可以很容易地将其抽象为自己的概念。如果业务决定添加 PlatinumCustomer
或 BronzeCustomer
那么我们必须更改代码以继承 abstract
class。如果我们宁愿将该概念建模为可以通过额外的 实例 (数据)定义的东西,那么我们不需要具有表示这些的代码结构:
public class CustomerDiscount
{
public int Type { get; }
public decimal MinimumSpend { get; }
public int Period { get; }
public decimal DiscountPercent { get; }
}
我的意思是,可以现成购买的通用子域(如@p|a|x 所述)不知道任何其他域中的任何概念,但需要能够将需求表示为与其领域特定语言相关的一组实例。
会计的核心是关于有借方和贷方的 Account
。从那里开始,使用 Journal
对交易进行分组并具有某种形式的 PostingRule
可以表示将金额细分为交易。如果没记错的话,Martin Fowler 在他的分析模式书中有一些会计方面的内容。您的帐户也 class 使用会计科目表 (COA) 进行了化,有时这些是标准化的 (SCOA)。这些 class 使事情变得有趣的原因在于它们通常还决定了您的发帖规则。
我不喜欢你领域中的抽象 classes,因为它们往往会使事情复杂化,但正如@p|a|x 所提到的,没有理由完全避免它们。我发现 abstract
classes 和继承通常比业务域模型更适合技术问题。如果您有一些业务方面的技术解决方案,那么您可能会发现它们很有用。如果您确实发现自己正在考虑任何类型的继承,请尝试查看您是否不能使用其他一些 concept/class.
来表示这种关系
我正在重建一个会计软件。该软件可以处理多种类型的交易,例如销售、库存补货和费用。我没有会计背景,但我在网上学到了一些概念。我正在尝试根据这些原则对域进行建模:
- 一个日记帐分录由多个记录组成,每个记录要么是借方,要么是贷方。在给定的日记帐分录中,这些记录的借方和贷方总额必须相等(平衡)
- 必须根据源文档创建日记帐分录。不能无缘无故地简单地创建一个日记条目。
根据这些,我得出结论,必须存在这些实体:
JournalEntry
个实体,其中有许多JournalEntryItem
个值对象。该实体负责保持其所有条目处于平衡状态。SourceDocument
实体,其中包含正在进行的交易的一些信息
JournalEntity
和SourceDocument
都是聚合根,JournalEntity
引用SourceDocument
身份。
问题来了。作为日记帐分录创建基础的源文档可以是具有不同行为的不同事物。例如,一个'expense'源文件可能由一对费用类别和费用金额的列表组成,而一个'sales'源文件可能由相关存货、每个存货的金额和每个存货的价格组成每一个单元。
这让我想到了模型中的另一种方法:
- 创建一个摘要
SourceDocument
class,具有创建日期和身份属性等共享属性 - 创建
SalesSourceDocument
、ExpenseSourceDocument
等,从摘要SourceDocument
扩展而来
JournalEntry
实体仍然需要引用SourceDocument
身份。
虽然这很有道理,但我认为这不是在 DDD 中对问题建模的最佳方式,特别是因为它需要有一个抽象实体。
从我在网上阅读的有关DDD的资料来看,当遇到需要抽象实体的问题时,我需要将其分离到不同的限界上下文中。但是,我不确定它是否可以完成,因为 SourceDocument
身份可能 return 在 'sales' 上下文中的具体 SalesSourceDocument
,但它也可能 return 'expense' 上下文中没有任何内容。
解决这个问题的最佳方法是什么?
What is the best way to approach this problem?
将您的单独用例视为单独的,直到您发现需要调整它们的领域问题。
您的销售单据和费用单据是不同的东西,您的数据模型应该反映出:与每个单据相关的信息存储在不同的表中;这些表的架构会随着对特定于它们管理的域的更改的响应而发展。
Duplication is far cheaper than the wrong abstraction -- Sandi Metz
我认为这里要认识到的重要一点是 源文档 术语是描述 期刊条目 和 文档,从JournalEntry
的角度来看。文档是它们自己的 AR,它很可能在没有 journal 的情况下存在,那么为什么要根据与另一个 AR 的关系来命名它们的抽象?
源文档很可能是描述与期刊条目相关的文档的必要术语,而且它很可能是您的模型也是如此,但可能采用值对象的形式,例如 SourceDocument { id, type }
.
这并不意味着如果有用的话,您也不能拥有 Document
抽象:没有理由不惜一切代价避免有用的多态性。
我的分析可能有误(无论如何所有模型都是错误的),但我只是想给你一个关于特定建模方面的不同视角。
I am rebuilding a software in accounting. This sofware can handle many types of transactions, e.g. sales, inventory restock, and expenses. I have no background in accounting, but I learned some concepts online.
如果您可以在线了解您的整个域,那么它很可能是一个通用的子域,您很可能可以在其中购买现成的产品。与其上网尝试自己想出一个模型,不如让领域专家参与进来:也许你的整个愿景在这里都是错误的。
此外,考虑到 源文档 很可能存在于不同的上下文中,Accounting 上下文似乎可以充当支持不明确了解或不依赖其他 BC 的 BC。
这是其中一个答案可能会变得很长的问题,所以我会尽力保持简短。我不是会计专家,但在我的职业生涯中我不得不实施 很多 会计。因此,当我有时间(一天)时,我会 developing one of those generic sub-domains。
我的一个建议是不要将 classification 构建为代码结构。然而,识别 class化并不总是那么容易。一个“简单”的例子可能是 Customer
。我们可以有 GoldCustomer
和 SilverCustomer
。我们可以从 abstract Customer
中提取 class 这些。我们必须决定我们为什么要这样做。例如,如果要确定每个周期需要的最低订单量,并且 upgrade/downgrade 客户 Type
才有资格获得一些折扣,那么可以很容易地将其抽象为自己的概念。如果业务决定添加 PlatinumCustomer
或 BronzeCustomer
那么我们必须更改代码以继承 abstract
class。如果我们宁愿将该概念建模为可以通过额外的 实例 (数据)定义的东西,那么我们不需要具有表示这些的代码结构:
public class CustomerDiscount
{
public int Type { get; }
public decimal MinimumSpend { get; }
public int Period { get; }
public decimal DiscountPercent { get; }
}
我的意思是,可以现成购买的通用子域(如@p|a|x 所述)不知道任何其他域中的任何概念,但需要能够将需求表示为与其领域特定语言相关的一组实例。
会计的核心是关于有借方和贷方的 Account
。从那里开始,使用 Journal
对交易进行分组并具有某种形式的 PostingRule
可以表示将金额细分为交易。如果没记错的话,Martin Fowler 在他的分析模式书中有一些会计方面的内容。您的帐户也 class 使用会计科目表 (COA) 进行了化,有时这些是标准化的 (SCOA)。这些 class 使事情变得有趣的原因在于它们通常还决定了您的发帖规则。
我不喜欢你领域中的抽象 classes,因为它们往往会使事情复杂化,但正如@p|a|x 所提到的,没有理由完全避免它们。我发现 abstract
classes 和继承通常比业务域模型更适合技术问题。如果您有一些业务方面的技术解决方案,那么您可能会发现它们很有用。如果您确实发现自己正在考虑任何类型的继承,请尝试查看您是否不能使用其他一些 concept/class.