如何避免使用领域驱动设计拥有非常大的对象
Posted
技术标签:
【中文标题】如何避免使用领域驱动设计拥有非常大的对象【英文标题】:How to avoid having very large objects with Domain Driven Design 【发布时间】:2011-02-23 05:18:53 【问题描述】:我们正在遵循领域驱动设计来实施大型网站。
但是,通过将行为放在域对象上,我们最终会得到一些非常大的类。
例如,在我们的 WebsiteUser 对象上,我们有很多方法 - 例如处理密码、订单历史、退款、客户细分。所有这些方法都与用户直接相关。其中许多方法在内部委托给其他子对象,但 这仍然会导致一些非常大的类。
我很想避免暴露大量子对象 例如user.getOrderHistory().getLatestOrder()。
还有哪些其他策略可以用来避免这个问题?
【问题讨论】:
【参考方案1】:您看到的问题不是由领域驱动设计引起的,而是由于缺乏关注点分离造成的。领域驱动设计不仅仅是将数据和行为放在一起。
我建议的第一件事是花一天左右的时间阅读 Domain Driven Design Quickly 可从 Info-Q 免费下载。这将概述不同类型的域对象:实体、值对象、服务、存储库和工厂。
我建议的第二件事是阅读Single Responsibility Principle。
我要推荐的第三件事是你开始沉浸在Test Driven Development 中。虽然通过首先编写测试来学习设计并不一定会让你的设计变得很棒,但它们往往会引导你走向松散耦合的设计并更早地揭示设计问题。
在您提供的示例中,WebsiteUser 肯定有太多责任。事实上,您可能根本不需要WebsiteUser
,因为用户通常由ISecurityPrincipal
表示。
由于缺乏业务背景,很难准确地建议您应该如何处理您的设计,但我首先建议通过创建一些索引卡来进行头脑风暴,这些索引卡代表您系统中的每个主要名词(例如客户、订单、收据、产品等)。在顶部写下候选类的名称,你认为左边的类固有的职责,以及它将与之协作的类在右边。如果某些行为感觉不属于任何对象,则它可能是一个很好的候选服务(即 AuthenticationService)。与你的大学一起将卡片摊在桌子上并讨论。不过不要过分强调这一点,因为这实际上只是作为一个头脑风暴设计练习。有时这样做比使用白板要容易一些,因为您可以四处移动。
从长远来看,你真的应该拿起 Eric Evans 的书Domain Driven Design。这是一本大书,但非常值得您花时间阅读。我还建议您根据您的语言偏好选择 Agile Software Development, Principles, Patterns, and Practices 或Agile Principles, Patterns, and Practices in C#。
【讨论】:
感谢您的评论。我读过埃文斯先生的书。我想问题是当一个实体有很多合作者时。例如网站用户(或委托人)拥有最新订单、第一笔订单、购物车、密码等。所有这些都与网站用户直接相关。 身份验证、待处理订单状态(即购物车)和订单历史记录是真正独立的问题。考虑将身份验证提取到 IAuthenticationService 并创建 IOrderHistoryRepository(或者可能是 IOrderHistoryService)来封装检索给定用户的订单历史记录的行为。【参考方案2】:虽然真正的人类有很多责任,但你正朝着God object anti-pattern前进。
正如其他人所暗示的,您应该将这些职责提取到单独的存储库和/或域服务中。例如:
SecurityService.Authenticate(credentials, customer)
OrderRepository.GetOrderHistoryFor(Customer)
RefundsService.StartRefundProcess(order)
具体命名约定(即使用 OrderRepository 或 OrderService,而不是 OrderManager)
由于方便,您遇到了这个问题。即,将 WebsiteUser
视为 聚合根 并通过它访问所有内容很方便。
如果您更强调清晰而不是方便,它应该有助于区分这些问题。不幸的是,这确实意味着团队成员现在必须了解新服务。
另一种思考方式:就像实体不应该执行自己的持久性(这就是我们使用Repositories的原因),您的 WebsiteUser
不应处理退款/分段/等。
希望有帮助!
【讨论】:
很好的答案,但我认为OrderService
与 OrderManager
的名称一样模糊。【参考方案3】:
要遵循的一个非常简单的经验法则是“类中的大多数方法都必须使用类中的大多数实例变量” - 如果您遵循此规则,类将自动具有正确的大小。
【讨论】:
这是一个有趣的观察。你能提供任何链接吗? 这基本上是一种更简单的 SRP 记忆方式——我在一本 oo 书籍中读过它,但发现它真的很有趣,并且深深地留在了我的脑海中。 这也称为高内聚【参考方案4】:我遇到了同样的问题,我发现在我们的案例中使用子“管理器”对象是最好的解决方案。
例如,在您的情况下,您可能有:
User u = ...;
OrderHistoryManager histMan = user.getOrderHistoryManager();
然后,您可以将 histMan 用于您想要的任何东西。显然你想到了这一点,但我不知道你为什么要避免它。当您的对象似乎做得太多时,它会分离关注点。
这样想。如果您有一个“人类”对象,并且您必须实现 chew()
方法。你会把它放在Human
对象还是Mouth
子对象上。
【讨论】:
【参考方案5】:您可能需要考虑颠倒一些事情。例如,客户不需要具有 Order 属性(或订单历史记录) - 您可以将它们排除在 Customer 类之外。所以不是
public void doSomethingWithOrders(Customer customer, Calendar from, Calendar to) List = customer.getOrders(from, to); 对于(订单订单:订单) order.doSomething();你可以这样做:
public void doSomethingWithOrders(Customer customer, Calendar from, Calendar to) List = orderService.getOrders(customer, from, to); 对于(订单订单:订单) order.doSomething();这是“松散”耦合,但您仍然可以获得属于某个客户的所有订单。我敢肯定有比我更聪明的人有正确的名字和引用上述内容的链接。
【讨论】:
【参考方案6】:我相信您的问题实际上与限界上下文有关。对于我所看到的,“处理密码、订单历史、退款、客户细分”,每一个都可以是一个有界上下文。因此,您可能会考虑将您的 WebsiteUser 拆分为多个实体,每个实体对应一个上下文。可能会出现一些重复,但您可以专注于自己的领域并摆脱具有多种职责的大型类。
【讨论】:
+1 这非常接近带有 DCI 的 DDD 的样子。区分系统是什么和系统做什么。以上是关于如何避免使用领域驱动设计拥有非常大的对象的主要内容,如果未能解决你的问题,请参考以下文章