原文 url:http://www.davenkin.me/post/2014-02-19/domain-driven-design-example
一次领域驱动设计(DDD)的实际应用
笔者先前参与了一个有关汽车信息的网站开发,用于显示不同品牌的汽车的信息,包括车型,发动机型号,车身尺寸和汽车报价等信息。在建模时,我们只需要创建名为Car的实体(Entity)对象。其他的信息,比如车身尺寸,都是对Car起描述作用的,因此应该建模成值对象(Value Object)。
此时创建的Car对象如下:
public class Car {
private String id;
private CarType type;
private EngineType engineType;
private String brand;
private double length;
private double height;
private double width;
private int price;
}
对应的CarRepository为:
public interface CarRepository {
List<Car> getAllCars();
Car getCarById(String id);
}
现在新的需求来了:对于有些品牌的汽车,该网站与这些品牌的汽车经销商建立了合作关系,使得用户在网站上点击一个链接便可以进入对应的汽车经销商网站。用户每点击一次链接,汽车经销商都会给该网站相应的提成,这也成为了该网站的收入来源之一。该网站因此做出预测,在将来还会有更多这样的定制化需求,即针对不同的品牌显示不同的内容。
(一)错误的建模方法
该网站的开发者立刻决定:可以将这些定制化需求建模成对象,名为Functionality,再在数据库中存放这些Functionality和品牌(Brand)之间关联关系。比如现在有两种类型的定制化需求,一种即为上面讲到的是否显示经销商链接,另一种即为是否显示报价。因此,他们建立了以下数据库表:
Functionality BrandsShow
AgencyLink BMW,HONDAShowPriceTOYOTA,VOLVO,HONDA
相应地,他们创建了一个名为FuncitonalityEnablement的类与上表对应:
public class FunctionalityEnablement {
private Functionality functionality;
private String brands;
}
请注意,这里使用了一个String来包含多个Brand。要看某个品牌的汽车是否具有某个Functionality,可以通过以下Service类来完成:
public interface BrandFunctionalityService {
boolean isFunctionalityEnabled(Functionality functionality, String brand);
}
该BrandFuntionalityService先通过DAO层获取到某中Functionality在数据库中所对应的FunctionalityEnablement,再调用isFunctionalityEnabled()方法,传入Brand值,检查该Brand是否拥有该Functionality,即检查该Brand是否包含在FunctionalityEnablement中的brands中。
对于以上建模方式,我至少可以看到两处不足之处:
(1)判断某个Brand是否拥有某种Functionality更应该是Brand本身的一种行为,而不是通过Service来完成。
(2)在有了新的需求之后,不同的Functionality对Brand起到了描述作用,并且这些描述信息有可能随着时间改变,比如在之后某个时刻,该网站又与BUICK品牌的经销商建立的合作关系。这样一来,Brand不再是值对象了,而是变成了具有生命周期的实体对象。但是以上的解决方案依然将Brand作为值对象来使用,并且将本应该成为描述信息的Functionality当成了实体来使用,的确不应该。
(二)正确的建模方法——采用领域驱动设计(DDD)
在使用领域驱动设计时,我们实际上可以建立两个限界上下文(Bounded Context),一个为汽车目录上下文(Car Category Context),另一个为品牌功能上下文(Brand Functionality Context)。在有些情况下,不同的上下文运行在不同的进程空间中,但是对于本文中的情况,由于两个上下文联系密切,又相对较小,我们可以通过引入不同的Java包来划分这两个限界上下文。
这样一来,在汽车目录上下文中,Brand依然可以建模成值对象,但是在品牌功能上下文中,Brand则应该建模成实体对象并且进行持久化。汽车目录上下文将作为品牌功能上下文的下游,即依赖于品牌功能上下文。在汽车目录上下文中,如果需要查看某个品牌是否拥有某种功能,我们可以调用品牌功能上下文所提供的应用服务(Application Service)。应用服务是非常薄的一层,限界上下文的领域模型便通过该层向外界提供基于用例的服务。
这里我们将重点放在品牌功能上下文上。通过以上讨论,我们知道,Brand应该为实体对象,并且拥有一种或多种Functionality,为了不至产生混淆,我们将实体类型的Brand命名为ConfigurableBrand。该ConfigurableBrand定义如下:
public class ConfigurableBrand {
private String name;
private List<Functionality> functionalities;
publicboolean hasFunctionality(Functionality functionality) {
return functionalities.contains(functionality);
}
}
对应的ConfigurableBrandRepository为:
public interface ConfigurableBrandRepository {
public List<ConfigurableBrand> getAllConfigurableBrands();
public ConfigurableBrand getConfigurableBrandByName(String name);
}
在持久化ConfigurableBrand时,我们可以像上文中那样,在不完全遵循关系型数据库范式的情况下对其进行持久化,此时是将ConfigurableBrand的name作为主键,其他信息(这里只有Functionality)则序列化到一个列中:
BrandName Funcionalities
BMW ShowAgencyLink
TOYOTA ShowPrice
HONDA ShowAgencyLink,ShowPrice
VOLVO ShowPrice
当然,如果你习惯了遵循数据库范式,那么你也可以建立3张数据库表,一张用于存放ConfigurableBrand,一张用于存放Functionality,另一张关联表存放前两者之间的关联关系。此时,ConfigurableBrand和Functionality存在着多对多的关系。
品牌功能上下文的应用服务提供了以下业务方法:
public interface ConfigurableBrandFunctionalityService {
boolean isFunctionalityEnabled(String functionality, String brand);
}
当汽车目录上下文需要知道某个品牌是否拥有某种功能时,它便应该调用品牌功能上下文的应用服务ConfigurableBrandFunctionalityService,该Service首先通过ConfigurableBrandRepository找到相应的ConfigurableBrand实体对象,再调用ConfigurableBrand中的hasFunctionality()方法以判断该ConfigurableBrand是否拥有某种Functionality。
对于ConfigurableBrandFunctionalityService,我们需要注意,首先外界上下文如果需要访问品牌功能上下文,它必须通过ConfigurableBrandFunctionalityService应用服务,再由该应用服务委派给品牌功能的领域模型,即应用服务才是领域模型的直接客户。另外,在调用ConfigurableBrandFunctionalityService时,我们并没有传入ConfigurableBrand和Functionality领域对象,而是直接使用了String类型,这也是合理的,因为外界不应该直接访问品牌功能上下文中的领域模型,而是应该通过应用服务。再者,在上文中我们讲到,isFunctionalityEnabled()方法更应该建模在ConfigurableBrand实体上,但是这里我们依然将其放在了ConfigurableBrandFunctionalityService上。原因在于,判断一个品牌是否拥有某种功能的核心业务逻辑的确是放在ConfigurableBrand中的,即hasFunctionality()方法,而ConfigurableBrandFunctionalityService中的isFunctionalityEnabled()方法只是反应了一个业务用例,它本身并不处理业务逻辑,而是将逻辑委派给领域模型ConfigurableBrand。
分享到:
相关推荐
Every Entity as A Microservice - 领域驱动设计DDD 分享我对领域驱动设计(DDD)的学习成果 化繁为简--DDD驱动复杂业务软件架构的演进 基于DDD的领域建模中的模版和工具实践 基于FP的DDD实践分享 架构分层模型适配 ...
最新领域驱动设计(DDD)资料合集,共23份。 金融支付系统的改造之路 化繁为简--DDD驱动复杂业务软件架构的演进 基于DDD的领域建模中的模版和工具实践 基于FP的DDD实践 架构分层模型适配 可视化的遗留系统微服务...
DDD领域驱动设计&中台实践资料(20份): DDD促进传统架构微服务转型(42页).pdf DDD在旅游电商架构演进中的实践(47页).pdf DDD实践中的那些坑(28页).pdf DDD的为与不为(25页).pdf Every Entity as A ...
DDD领域驱动设计&中台实践资料合集,共20份。 DDD促进传统架构微服务转型 化繁为简--DDD驱动复杂业务软件架构的演进 基于FP的DDD实践 基于DDD的领域建模中的模版和工具实践 架构分层模型适配 金融支付系统的改造之...
在应用系统开发中,采用严格的、单一的、真正的的分层架构是可以的,但实际上我们已经采用了多种架构模式设计系统。当多种不同范式的架构混合在一起,你会不会出现“指鹿为马”的现象呢? 在研究分层架构时,常通过...
领域驱动设计(Domain-Driven Design,DDD)是Evans提出来的用来处理软件系统核心复杂性的方法。该方法的有效性在实践中得到证明,但是方法在细节上存在不够清晰、对设计人员素质要求高等问题。在对大量业务系统进行...
基于阿里技术专家开源的COLA 4.x架构,详细讲解COLA框架的使用,领域驱动设计DDD的建模过程,以及DDD经典示例项目-货物运输系统(Cargo Tracker Application)代码实现细节。 COLA既是框架,也是架构。开源作者创建...
《领域驱动设计与模式实战》全面详细地解释了领域驱动设计、测试驱动开发、依赖注入、持久化、重构、模式等很多基本概念,并以C#和.NET实例为依托,展示了这些概念的实际应用和重要价值。更重要的是,《领域驱动设计...
这个系列的教程会和大家一起一步一步搭建一个简单实用的基于领域驱动设计(DDD)开发模式的一个项目开发框架。 通过结合实际的代码应用让大家对领域驱动开发有一个更好的理解。 章节1:基础介绍 课时1课程介绍 课时2...
采用的架构是基于DDD领域驱动设计思想的BFF架构。目标是打造一个支持多端适配、分布式、高可用、多模块的系统。在未来可扩展成任意的企业中后台系统解决方案。 前端基于 Ant Design 5.0,为 Web ….zip 适合学习/练...
机构树形的公司部门结构,国内公司用的比较多,它实际上就是一个用户组,机构和用户设计成N:N的关系,也就是说有时候一个用户可以从属于两个部门,这种情况在我们客户需求中的确都出现过。 ####系统工程结构: ...
领域驱动设计(DomainDrivenDesign)的概念已经被发明了十多年,而且也不乏相关著作,但是业界宣称自己应用了DDD原则的项目,软件却鲜有耳闻。随着微服务架构的流行,DDD在边界识别,服务划分等方面不断被提及,作为...
本文主要是参考MartionFowler所著的《企业应用架构模式》与EricEvans所著的《领域驱动设计》这两本泰山之作,加上本人在近年实际的工作过程中开发SOA系统所认识到的问题所写的一篇文章,欢迎各位点评。 最后两节细说...
Eclipse货物追踪器-Jakarta EE的应用领域驱动设计蓝图 概述 该项目演示了如何使用广泛采用的体系结构最佳实践(例如域驱动设计(DDD))来使用Jakarta EE开发应用程序。 该项目直接基于DDD先驱Eric Evans的Domain ...
如何组装实际应用我们必须通过我们的基础设施实现域接口,例如。 如果我们使用 Doctrine,我们实现 ,如果我们使用 CSV 存储定价,我们根据项目需要实现等等。 然后我们在或喜欢的 DI 容器中注册这些类。 如果你不...