最近换工作,面试了几家公司,发现领域驱动设计(DDD)现在越来越火了,很多公司的架构师岗位都会问DDD相关的问题。我这几年在项目里实践过DDD,对DDD有一些理解和经验,面试的时候被问到了不少DDD相关的问题,有的答得不错,有的答得不太好。
今天就来整理一下我面试中被问到的DDD相关的问题,以及我的回答和思考,希望能帮到正在准备面试或者想学习DDD的朋友。
一、什么是领域驱动设计,它解决了什么问题
这是最基础的问题,几乎每次面试都会问。我的回答是,领域驱动设计(Domain-Driven Design,简称DDD)是一种软件开发方法论,由Eric Evans在2003年的《领域驱动设计》一书中提出。它的核心思想是,软件的核心复杂度在于业务领域,而不是技术,所以开发应该以业务领域为核心,让业务领域的模型来驱动软件的设计和开发。
传统的软件开发方式,往往是技术驱动的,开发者先考虑技术架构、数据库设计、框架选择,然后再把业务逻辑塞进去。这样做的问题是,业务逻辑和技术逻辑混在一起,代码越来越复杂,越来越难维护,而且业务人员和开发人员之间沟通困难,业务需求的变化很难快速响应。
DDD解决的就是这个问题,它把业务领域放在核心位置,让开发团队和业务专家一起,建立一个统一的、准确的业务领域模型,然后基于这个领域模型来设计和开发软件。这样做的好处是,业务逻辑清晰,代码可维护性高,业务人员和开发人员沟通顺畅,业务需求变化能快速响应,软件的质量和开发效率都能得到提升。
DDD不是一个具体的技术框架,也不是一套固定的设计模式,而是一种设计思想和方法论,它提供了一套通用的语言、原则和模式,帮助开发团队更好地理解业务领域,建立高质量的领域模型,开发出更符合业务需求的软件。
二、DDD的核心概念有哪些
这个问题也是必问的,考察你对DDD的基本概念是否熟悉。DDD的核心概念比较多,我通常会分成战略设计和战术设计两部分来回答。
战略设计部分的核心概念:
- 领域(Domain):就是业务领域,是一个组织所做的事情以及其中所包含的一切。比如电商领域、金融领域、医疗领域等等。
- 子域(Subdomain):一个大的领域可以分成多个子域,比如电商领域可以分成订单子域、商品子域、用户子域、支付子域等等。子域又可以分为核心域、支撑域和通用域。
- 限界上下文(Bounded Context):这是DDD战略设计中最重要的概念,它定义了一个模型的边界,在这个边界内,模型是统一的、一致的,出了这个边界,模型可能就不一样了。每个限界上下文对应一个子域,有自己独立的模型和代码。
- 上下文映射(Context Map):描述不同限界上下文之间的关系和集成方式,比如合作关系、客户-供应商关系、遵奉者关系、分离方式等等。
战术设计部分的核心概念:
- 实体(Entity):有唯一标识的对象,它的属性可能会变化,但是标识不变。比如用户、订单、商品等等。
- 值对象(Value Object):没有唯一标识的对象,它的属性是不可变的,通过属性来判断相等。比如地址、金额、日期范围等等。
- 聚合(Aggregate):一组相关的对象的集合,作为一个数据修改的单元,有一个聚合根。比如订单聚合,包含订单、订单项、收货地址等等,订单是聚合根。
- 聚合根(Aggregate Root):聚合的根对象,是外部访问聚合的唯一入口,外部对象不能直接访问聚合内部的对象,只能通过聚合根来访问。
- 领域服务(Domain Service):不属于任何实体或值对象的业务逻辑,通常是一些涉及多个聚合的操作。比如转账操作,涉及到两个账户聚合,就可以放在领域服务里。
- 仓储(Repository):负责聚合的持久化和检索,封装了数据访问的细节,让领域层不需要关心数据是怎么存的。
- 领域事件(Domain Event):领域中发生的有意义的事情,比如订单已创建、支付已完成等等,通过事件来实现不同限界上下文之间的解耦。
- 工厂(Factory):负责创建复杂的实体或聚合,封装创建的逻辑,让客户端不需要知道创建的细节。
这些概念是DDD的基础,面试的时候一定要能说清楚,最好能结合实际项目举例说明,这样更有说服力。
三、什么是限界上下文,怎么划分限界上下文
限界上下文是DDD战略设计的核心,也是面试中经常被深入问到的问题。我的理解是,限界上下文就是一个模型的边界,在这个边界内,所有的术语、概念、模型都是统一的、一致的,出了这个边界,同样的术语可能有不同的含义,模型也不一样。
比如"商品"这个概念,在商品上下文中,它有详细的属性,比如名称、描述、价格、库存、分类等等;但是在订单上下文中,"商品"可能就只有商品ID、名称、价格这几个属性,因为订单只需要记录买了什么商品,多少钱,不需要知道商品的详细信息。所以同样是"商品",在不同的上下文中含义是不一样的,这就是限界上下文的意义,它明确了每个模型适用的边界,避免模型混乱。
怎么划分限界上下文呢?我通常会从以下几个方面来考虑:
- 按业务领域划分:这是最基本的划分方式,一个子域对应一个限界上下文。比如电商系统,可以分成商品上下文、订单上下文、用户上下文、支付上下文、物流上下文、营销上下文等等。
- 按组织结构划分:康威定律说,系统的架构反映了组织的沟通结构。如果一个团队负责一个业务模块,那这个模块就可以划分为一个限界上下文,因为团队内部沟通顺畅,模型容易统一,团队之间沟通成本高,模型应该隔离。
- 按变化频率划分:如果一部分业务逻辑变化很频繁,另一部分变化很少,那可以把它们分成不同的限界上下文,这样变化频繁的部分可以独立迭代,不会影响变化少的部分。
- 按性能要求划分:如果一部分业务对性能要求很高,另一部分要求不高,可以分成不同的限界上下文,这样可以对高性能的部分单独优化,不会影响其他部分。
- 按安全要求划分:如果一部分业务涉及敏感数据,安全要求高,另一部分不敏感,可以分成不同的限界上下文,对敏感部分单独做安全加固。
划分限界上下文没有一个固定的标准,需要根据实际的业务情况、团队情况、技术情况来综合考虑。而且限界上下文的划分也不是一成不变的,随着业务的发展和团队的变化,限界上下文也可能需要调整。重要的是,每个限界上下文要有明确的边界,有统一的模型,上下文之间通过明确的接口来集成,不要互相依赖对方的内部模型。
四、聚合和聚合根是什么,设计聚合的原则是什么
聚合是DDD战术设计的核心概念,也是面试中经常问的。我的理解是,聚合是一组相关的对象的集合,这些对象在业务上是紧密关联的,需要作为一个整体来保证业务规则的一致性。聚合根是聚合的入口,是聚合中最核心的对象,外部只能通过聚合根来访问聚合内部的对象,不能直接访问聚合内部的对象。
比如订单聚合,包含订单、订单项、收货地址、优惠信息等等,这些对象在业务上是紧密关联的,下单的时候需要一起创建,修改订单的时候需要一起修改,删除订单的时候需要一起删除,而且要保证业务规则的一致性,比如订单总金额必须等于所有订单项的金额之和,优惠金额不能超过订单总金额等等。这些规则需要在聚合内部保证,外部不能绕过聚合根直接修改订单项,否则就可能破坏业务规则的一致性。所以订单就是聚合根,订单项、收货地址等等是聚合内部的对象,外部只能通过订单来访问它们。
设计聚合的原则,我总结了以下几点:
- 聚合要小,不要太大:聚合不要包含太多的对象,不要把所有相关的对象都放到一个聚合里。聚合太大了,会导致加载和修改的性能差,并发冲突多,而且聚合的边界不清晰。一般来说,一个聚合只包含最核心的、必须保证一致性的对象,其他对象可以单独作为聚合,通过ID关联。
- 通过ID引用其他聚合:聚合之间不要直接持有对方的对象引用,应该通过ID来引用。比如订单聚合里不要直接持有用户对象,只需要持有用户ID,需要用户信息的时候再通过用户ID去查询。这样可以降低聚合之间的耦合,提高性能,也避免了加载一个聚合的时候把关联的其他聚合都加载出来。
- 一个事务只修改一个聚合:这是很重要的一个原则,一个事务里只修改一个聚合实例,不要在一个事务里修改多个聚合。如果业务需要修改多个聚合,应该通过领域事件来异步处理,最终一致性。这样可以减少事务的范围,提高并发性能,也降低了分布式事务的复杂度。
- 聚合根负责保证业务规则的一致性:聚合内部的业务规则,必须由聚合根来保证,外部不能绕过聚合根直接修改聚合内部的对象。所有对聚合内部对象的修改,都必须通过聚合根的方法来进行,这样才能保证业务规则的一致性。
- 聚合的边界要根据业务规则来定:聚合的边界不是根据对象的关系来定的,而是根据业务规则的一致性来定的。哪些对象需要在同一个事务里保证一致性,就把它们放到同一个聚合里;不需要在同一个事务里保证一致性的,就可以放到不同的聚合里。
这些原则是我在实践中总结的,也是DDD社区比较公认的原则。面试的时候能说出这些原则,再结合实际项目举例说明,基本就能答得不错了。
五、DDD和传统的三层架构有什么区别
这个问题也是经常问的,考察你对DDD和传统架构的理解。我的回答是,传统的三层架构是表现层、业务逻辑层、数据访问层,它是按技术分层的,每一层负责不同的技术职责。而DDD的分层架构通常是用户接口层、应用层、领域层、基础设施层,它虽然也是分层,但是核心是领域层,领域层是整个系统的核心,不依赖任何其他层,其他层都依赖领域层。
具体的区别主要有以下几点:
- 核心不同:传统三层架构的核心是数据访问层,业务逻辑层往往是围绕数据库表来写的,是表驱动的。而DDD架构的核心是领域层,领域模型是整个系统的核心,数据库只是持久化领域模型的工具,是模型驱动的。
- 业务逻辑的位置不同:传统三层架构的业务逻辑往往散落在业务逻辑层,甚至有些业务逻辑会写到表现层或者数据访问层,代码混乱。而DDD架构的业务逻辑都集中在领域层,由实体、值对象、领域服务来承载,应用层只负责协调,不包含业务逻辑,职责清晰。
- 数据访问的方式不同:传统三层架构的数据访问层是直接操作数据库表的,业务逻辑层知道数据是怎么存的,甚至会写SQL。而DDD架构的基础设施层通过仓储来封装数据访问的细节,领域层只依赖仓储接口,不知道数据是怎么存的,甚至可以是内存存储、文件存储,不一定要是数据库,这样领域层和数据存储解耦了。
- 扩展性和可维护性不同:传统三层架构因为业务逻辑混乱,数据和业务耦合,随着业务复杂度的增加,代码会越来越难维护,扩展性差。而DDD架构因为领域模型清晰,职责分明,业务逻辑集中,所以可维护性和扩展性都更好,特别是对于复杂的业务系统,DDD的优势更明显。
- 团队协作方式不同:传统三层架构的开发人员往往只关注技术,和业务人员沟通少,业务需求的理解容易有偏差。而DDD强调开发人员和业务专家一起工作,建立统一的领域模型和通用语言,团队协作更顺畅,业务需求的理解更准确。
当然,DDD也不是银弹,不是所有项目都适合用DDD。对于业务简单的项目,用传统三层架构可能更简单更高效。但是对于业务复杂、需求变化频繁、长期维护的项目,DDD的优势就很明显了。
六、你在项目中是怎么实践DDD的,遇到了什么问题
这个问题是考察你的实际经验,不是背概念就能答好的,需要结合自己的实际项目来说。我通常会讲我做过的一个电商项目,从传统三层架构重构到DDD架构的经历。
那个项目一开始是传统的三层架构,业务越来越复杂,代码越来越乱,维护成本越来越高,需求迭代越来越慢。后来我们决定引入DDD,对系统进行重构。
我们的做法是:
- 先和业务专家一起,梳理业务领域,划分限界上下文,把整个电商系统分成了商品、订单、用户、支付、物流、营销六个限界上下文。
- 每个上下文独立开发,有自己的领域模型、应用层、基础设施层,上下文之间通过REST API或者消息队列来集成。
- 在每个上下文内部,按照DDD的战术设计来建模,识别实体、值对象、聚合、聚合根、领域服务、仓储、领域事件等等。
- 数据库设计不再是先建表,而是先建立领域模型,然后通过仓储把领域模型持久化到数据库,表结构是为模型服务的,不是反过来。
遇到的问题主要有:
- 团队的思维转变难:团队成员习惯了传统三层架构,习惯了先建表再写代码,对DDD的概念理解不深,一开始写出来的代码还是传统的风格,只是套了DDD的概念。后来我们通过培训、代码评审、结对编程,慢慢让团队理解了DDD的思想,代码质量才慢慢提上来。
- 聚合的边界不好把握:一开始设计聚合的时候,要么聚合太大,包含了太多对象,性能差;要么聚合太小,导致一个事务要修改多个聚合,分布式事务复杂。后来通过不断的实践和调整,才慢慢找到了合适的聚合边界。
- 领域事件的最终一致性处理:用了领域事件之后,跨上下文的操作变成了异步的,最终一致性,但是业务上有些场景需要强一致性,或者需要处理事件失败的情况,这就增加了系统的复杂度。后来我们引入了事件溯源、重试机制、死信队列等等,才比较好地解决了这个问题。
- 查询性能问题:DDD强调领域模型的纯度,但是查询的时候往往需要跨多个聚合、多个上下文的数据,如果按照领域模型来查询,会很麻烦,性能也差。后来我们引入了CQRS模式,命令端按照领域模型来写,查询端单独做,直接查数据库或者用ES,不经过领域模型,这样既保证了领域模型的纯度,又解决了查询性能的问题。
通过这个项目的实践,我深刻体会到,DDD不是一套固定的模板,不是把概念套上去就行的,它是一种设计思想,需要根据实际的业务情况灵活运用,而且需要团队有比较高的架构能力和业务理解能力。用好了DDD,能大大提升系统的可维护性和扩展性,但是用不好,可能会比传统架构更糟糕。
七、写在最后
领域驱动设计DDD面试题:我被问到的那些问题。
以上就是我面试中被问到的一些DDD相关的问题,以及我的回答和思考。当然,DDD的内容远不止这些,还有很多深入的话题,比如事件溯源、CQRS、六边形架构、事件风暴等等,面试的时候如果遇到深入的问题,还需要更深入的理解。
DDD这几年越来越火,特别是在微服务架构流行之后,DDD的战略设计(限界上下文、上下文映射)和微服务的划分不谋而合,很多公司都在用DDD来指导微服务的划分和设计。所以不管是面试还是实际工作,掌握DDD都是很有价值的。
但是我也要提醒大家,DDD不是银弹,不是所有项目都适合用DDD,也不是用了DDD就一定能做好。对于业务简单的项目,用DDD可能会过度设计,增加复杂度。而且DDD对团队的要求比较高,需要团队成员对业务有深入的理解,对DDD的思想有正确的认识,否则很容易写成"伪DDD",只是套了概念,没有领会思想,反而比传统架构更糟糕。
学习DDD,建议先读Eric Evans的《领域驱动设计》,这是DDD的经典著作,然后读Vaughn Vernon的《实现领域驱动设计》,这本书更偏向实践,然后在实际项目中多实践多总结,慢慢就能掌握DDD的精髓了。
最后用一句话结尾:"DDD不是关于技术的,而是关于业务的;不是关于框架的,而是关于思想的。理解了业务,掌握了思想,DDD自然就用好了。"
祝大家都能学好DDD,在面试和工作中都能游刃有余!
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录