一、领域驱动设计(DDD)概述

领域驱动设计(Domain-Driven Design,简称DDD)是一种以领域为核心的软件开发设计思想。它强调在软件设计过程中,应准确反映真实业务过程,满足业务问题域的需求。 DDD将设计过程分为两个层面:

  • 战略设计:提炼问题域,塑造应用程序架构。
  • 战术设计:创建复杂有界上下文的有效模型。 DDD倡导专注于核心领域,通过团队协作提炼公共语言和知识,持续推进领域知识的深入发展。

二、实践案例分析

本文以携程国际火车票中台预订系统项目为例,探讨DDD思想在实际项目中的应用。该系统服务结构如图1所示(图略)。

2.1 项目背景

携程国际火车票中台预订系统是一个面向全球用户的在线服务平台,提供火车票预订服务。项目团队采用DDD思想,以期实现以下目标:

  • 精确建模业务过程。
  • 构建灵活、可扩展的系统架构。
  • 促进团队成员对业务领域的深入理解。

2.2 战略设计实践

在战略设计阶段,项目团队通过以下步骤实现问题域的提炼和架构的塑造:

  1. 领域建模:识别业务领域的关键概念和实体。
  2. 定义有界上下文:划分业务问题域,明确系统边界。
  3. 架构设计:基于领域模型设计系统架构,确保系统的灵活性和可扩展性。

2.3 战术设计实践

战术设计阶段,项目团队专注于创建有效的领域模型:

  1. 提炼公共语言:通过团队协作,形成统一的业务术语和概念。
  2. 构建领域模型:基于公共语言,构建反映业务逻辑的领域模型。
  3. 持续迭代:根据业务发展和需求变化,不断优化和调整领域模型。

2.4 实践成果

通过DDD思想的实践,携程国际火车票中台预订系统项目取得了以下成果:

  • 系统架构更加清晰,易于维护和扩展。
  • 团队成员对业务领域有了更深入的理解。
  • 业务需求变更能够快速响应,系统适应性得到提升。

2.5 总结与展望

DDD思想为携程国际火车票中台预订系统项目提供了一种有效的设计方法论。未来,项目团队将继续深化DDD实践,探索更多领域驱动设计的应用场景,以实现更高效、更精准的软件开发。

伪代码如下所示:

1
@Override protected CreateOrderResponse execute(CreateOrderRequest request) { // 1、参数校验 if (!validate(request)) { throw new BusinessException(P2pBookingResultCode.PARAM); } if (orderMapper.select(request.getOrderId()) != null) { throw new BusinessException(P2pBookingResultCode.ORDER_EXISTS); } // 2、初始化订单 OrderDao orderDao = new OrderDao(); orderDao.setOrderId(request.getOrderId()); orderDao.setOrderStatus(100); orderMapper.insert(orderDao); // 初始化乘客信息 PassengerDao passengerDao = new PassengerDao(); ... passengerMapper.insert(passengerDao); // 3、转换汇率 ExchangeRate exchangeRate = exchangeService.getExchangeRate(originCurrency, targetCurrency); // 4、购买保险 if (isBuyInsurance(request)) { // 调用保险服务 InsuranceInfo insuranceInfo = insuranceService.buyInsurance(request); // 保存保险信息 InsuranceDao insuranceDao = new InsuranceDao(); ... insuranceMapper.insert(insuranceDao); } // 5、供应商创单 SupplierOrder supplierOrder = supplierService.createOrder(request, exchangeRate); // 保存供应商订单信息 SupplierOrderDao supplierOrderDao = new SupplierOrderDao(); ... supplierOrderMapper.insert(SupplierOrderDao); // 6、保存订单信息 orderDao = new orderDao(); orderDao.setOrderId(request.getOrderId); orderDao.setOrderStatus(OrderStatusEnum.WAIT_FOR_PAY.getCode()); ... orderMapper.update(orderDao); // 7、发送超时支付取消消息 messageProducer.push(MessageQueueConstants.TOPIC_TIMEOUT_CANCEL, "orderId", String.valueOf(orderDao.getOrderId()), appSettingProp.getTimeoutMinutes(), TimeUnit.MINUTES); // 8、返回结果 return mappingResponse(orderDao, orderInsuranceEntity, exchangeRateResponse); }

在软件开发过程中,随着业务的不断增长和迭代,传统架构模式可能会遇到一些挑战。本文将探讨这些问题,并介绍如何通过领域驱动设计(DDD)来优化系统架构。

一、传统架构的挑战

1.1 控制层臃肿

在MVC架构中,控制层随着业务逻辑的复杂化而变得臃肿。Model层负责数据,View层提供用户界面,而Controller层作为逻辑控制核心,处理用户指令和数据操作。然而,随着时间推移,控制层代码量激增,导致维护困难。

1.2 过度耦合业务逻辑的增长和第三方服务的引入导致系统模块之间高度耦合,形成所谓的“大泥球”模式,使得代码难以维护和扩展。例如,在出票系统中,多个服务接口和模块功能耦合在控制层,增加了维护成本。

1.3 失血模型领域对象若仅包含基本的get和set方法,将导致对象失去行为,成为数据的简单载体,这与面向对象的设计原则相悖。与之相对的充血模型则包含业务逻辑,提高了对象的自洽性和复用性。

二、领域驱动设计(DDD)的优势

2.1 系统设计DDD通过战略设计和战术设计两个层面来优化系统架构。战略设计关注于如何拆分复杂系统,而战术设计则关注于单个域的具体实现。

2.1.1 战略设计

  • 通用语言:定义了预定系统的通用语言,包括用户搜索、供应商下单、财务统计、汇率转换和保险购买等。

  • 领域:建立领域模型,反映业务需求本质,促进领域专家、设计和开发人员之间的交流。

  • 限界上下文:通过建模划分业务领域边界,明确不同子域的职责。

2.1.2 战术设计

  • 实体、值对象、聚合、工厂和仓储等概念,指导如何具体实现领域模型,并遵循相应的原则。 通过DDD,系统能够实现高内聚、低耦合,提高代码的可维护性和可扩展性。

    在软件架构设计中,领域驱动设计(DDD)是一种将业务逻辑集中在核心领域层的方法,以提高系统的可维护性和可扩展性。以下是对DDD核心概念的梳理和架构设计的描述。

3.1.2 战术设计

失血模型问题:在传统的开发模式中,业务逻辑和校验逻辑常常分散在各个Service层,这使得维护变得困难。DDD通过区分领域模型和数据模型来解决这一问题。

  • 领域模型:内聚业务行为的对象。

  • 数据模型:持久化业务数据的模型。 仓储(Repository):作为领域模型和数据模型之间的桥梁,统一管理领域对象的存储和访问。 实体(Entity):具有唯一标识和生命周期的对象,如订单,可通过订单号唯一标识。 值对象(Value Object):没有唯一标识的对象,关注对象的属性而非身份,如行程上下文,不可变,需整体替换。 聚合(Aggregate):通过定义清晰的对象关系和边界,实现领域模型的内聚。聚合根作为聚合的根节点,必须是实体。

3.2 架构设计

DDD支持多种分层架构模式,但本文采用的是六边形架构(Hexagonal Architecture),其特点在于:

  • 防腐层(Anticorruption Layer, ACL):隔离外部领域,确保通过防腐层与业务逻辑交互。

  • 合作关系(Partner Ship, PS):与外部系统或服务的交互点。 六边形架构通过定义清晰的内外边界,使得系统更加灵活,易于与外部系统或服务集成,同时保持内部业务逻辑的独立性和清晰性。

    六边形架构是一种软件设计模式,它通过依赖倒置原则对传统分层架构进行了优化。在这种架构中,无论是高层还是低层组件,都依赖于抽象,从而实现了架构的扁平化。六边形架构中,每条边代表一种端口,这些端口负责处理输入或输出,实现了系统内部与外界的隔离。对于每种外部类型,都有一个适配器与之对应,确保了系统的灵活性和可扩展性。 六边形架构的核心优势在于它能够实现技术与业务的分离。架构的内部核心是领域模型和不同领域的逻辑编排,而外部基础设施层则负责提供技术实现和外部系统的适配。这种分离使得业务人员能够专注于领域模型的更新,而不必过多关注技术实现的细节。 在面对三方接口结构不稳定的情况时,六边形架构可以通过适配器将外部模型转化为内部模型,从而降低修改成本。对于外部请求,无论是通过RPC、REST、HTTP还是消息队列(MQ)等方式,都可以通过适配器进行输入转化,将控制权交给内部区域处理。 此外,六边形架构中的仓储(repository)实现可以看作是一种持久化适配器。这种适配器用于访问或保存聚合实例,可以通过不同的技术实现,例如MySQL、Redis等。

    DDD实现

    本文以国际火车票中台预订系统项目为例,探讨了领域驱动设计(DDD)在实际项目中的应用。项目架构的设计遵循了DDD的原则,将业务逻辑与技术实现分离,使得项目能够更加灵活地应对需求变化和技术更新。

    4.1 项目架构设计

    项目架构的设计包括了多个层面,从领域模型的构建到基础设施的适配,每一部分都紧密遵循DDD的原则,以确保项目的可维护性和可扩展性。

系统架构概述

根据DDD(领域驱动设计)六边形架构原理,本项目系统架构被清晰地划分为四个层次,以实现高内聚、低耦合的设计目标。以下是各层的详细说明:

1. Gateway层Gateway层作为整个项目的入口,承担着接收外部请求的责任。它包括RPC(远程过程调用)、MQ(消息队列)等多种不同的接入方式,确保系统能够灵活地处理各种外部交互。

2. Infrastructure层Infrastructure层扮演着基础设施的角色,它主要有两个功能:

  • 防腐层:通过适配器模式,将外部的请求或数据适配为系统内部能够理解的形式,从而保护系统内部的稳定性。
  • 技术实现:实现领域层定义的接口,提供具体的技术实现细节。

3. Application层Application层是逻辑编排的核心,负责管理和调度业务逻辑。它的设计目标是尽可能地轻薄,突出核心业务逻辑,避免过多地涉及技术细节。

4. Domain层Domain层是领域模型的所在地,负责定义和建模领域内的概念和行为。它确保了业务逻辑的准确性和完整性。

4.2 领域对象领域对象是DDD中的核心组成部分,它们不仅仅是数据的载体,更包含了相应的行为。例如,对象自身的参数校验应该是其行为的一部分,而非由外部强制赋予。通过适配器将外部数据转换为领域对象,可以使得对象能够自主完成参数校验等行为。以下是实现这一行为的示例代码:

java// 示例代码省略,具体实现根据实际项目而定 在DDD实践中,确保领域对象的完整性和行为的自足性是至关重要的,这有助于提高系统的可维护性和可扩展性。

1
public class CreateOrderRequest extends CommonRequest { private List<SolutionOfferPair> outSolutionOfferPairList; private List<SolutionOfferPair> returnSolutionOfferPairList; private String transactionNo; ... private Contact contact; private List<Passenger> passengerInfoList; private boolean isSplitOrder; private boolean randomAssigned; private List<ExtraInfo> extraInfos; @Override public void requestCheck() { if (StringUtils.isEmpty(splitPlanId) && CollectionUtil.isEmpty(outSolutionOfferPairList)) { throw new BusinessException(ResponseCodeEnum.PARAM_ERROR); } ... } }

4.3 战术设计实现分析

在本节中,我们将以订单聚合根为案例,深入探讨战术设计的实现过程。

聚合根概述

聚合根是领域驱动设计(DDD)中的一个核心概念,它不仅包含实体和值对象,还封装了业务逻辑,从而提升了模块的内聚性。与那些仅提供数据访问功能的业务对象不同,聚合根还负责自身的持久化操作。

实体与值对象

  • 实体:具有唯一标识,可以独立存在的对象。
  • 值对象:没有唯一标识,通常用于描述实体的属性或行为的对象。

业务逻辑封装聚合根通过封装业务逻辑,确保了业务操作的一致性和完整性。这避免了业务规则在不同部分的重复实现,提高了代码的可维护性。

持久化操作聚合根内部封装了仓储模式,负责与数据库等持久化存储进行交互,确保了数据的持久化和聚合根状态的一致性。

订单聚合根案例以订单为例,订单聚合根可能包含如下元素:

  • 订单详情:描述订单的具体信息,如商品、数量等。

  • 支付信息:记录支付方式和支付状态。

  • 物流信息:跟踪订单的配送过程。 订单聚合根的设计需要考虑以下方面:

  • 数据一致性:确保订单状态在任何时候都是一致的。

  • 操作原子性:订单的创建、修改和取消等操作应该是原子性的。

  • 并发控制:处理多用户同时操作同一订单的情况。 通过上述分析,我们可以看到,聚合根的设计实现是确保系统稳定性和可维护性的关键。

1
public class P2pOrder { private P2pOrderRepository repository; @Getter private long orderId; @Getter private OrderMasterModel orderMasterModel; @Getter private List<OrderItemModel> orderItemModels; public P2pOrder(P2pOrderRepository repository, long orderId) { this.repository = repository; this.orderId = orderId; orderInfoModel = new OrderInfoModel(); orderItemModels = new ArrayList<>(); } public boolean find() { return repository.find(this); } public void createOrder(CreateOrderRequest request) { if (find()) { throw new BusinessException(ResponseCodeEnum.ORDER_EXISTED); } this.orderMasterModel.createOrderMaster(request); repository.createP2pOrder(this); // 发送超时支付取消消息 pushDelayMessage(this); } }

实体

实体是指会存在状态变更的类,比如order,其可以提供订单的变更状态等。

1
@Getter public class OrderMasterModel { private OrderStatusEnum orderStatus; private LocalDateTime ticketTime; private LocalDateTime expirationTime; private String lang; ... public void init(CreateOrderRequest request) { this.channelName = request.getChannelMetaInfo().getChannel(); this.orderStatus = OrderStatusEnum.SEAT_BOOKING; ... } public void ticketing() { if (this.orderStatus != OrderStatusEnum.WAIT_FOR_PAY) { throw new BusinessException(ResponseCodeEnum.ORDER_STATUS_ERROR); } this.orderStatus = OrderStatusEnum.TICKETING; } }

在软件开发中,值对象是一种特殊的类,它没有唯一的标识符,其主要作用是描述数据。例如,行程信息就是一个典型的值对象。当行程信息发生变化时,我们不是通过修改现有对象的方式来实现,而是重新创建一个新的行程信息对象。本文将探讨如何通过私有化构造方法,并仅提供静态方法来创建对象,来实现值对象的不可变性。

值对象的特点

  1. 无唯一标识:值对象不具有区分不同实例的唯一标识。
  2. 数据描述:值对象的主要作用是描述数据,而不是执行操作。
  3. 不可变性:值对象一旦创建,其状态不能被修改。

构造方法的私有化

  • 私有化构造方法可以防止外部直接通过构造函数创建值对象实例。
  • 这有助于保证值对象的不可变性。

提供静态方法创建对象

  • 通过静态方法,我们可以控制值对象的创建过程。
  • 静态方法允许我们根据需要重新构造值对象,而不是修改现有对象。

应用场景

  • 当需要确保数据一致性和防止数据被意外修改时,使用值对象是一个好选择。
  • 值对象常用于表示那些不需要独立存在,而是作为其他对象属性的数据。

结论值对象的设计模式强调了数据的不可变性和一致性,通过私有化构造方法和静态工厂方法,我们可以有效地控制对象的创建和状态,从而提高软件的稳定性和可维护性。

1
@Getter public class OrderSegmentModel { private long orderSegmentId; private short sequence; private TravelTypeEnum direction; private String segmentType; private String departureLocationCode; private String departureLocationName; private String arriveLocationCode; private String arriveLocationName; ... private OrderSegmentModel() {} public static OrderSegmentModel init(OrderSegment orderSegment, short sequence) { OrderSegmentModel model = new OrderSegmentModel(); model.orderSegmentId = Long.valueOf(orderFareId + "0" + orderSegment.getSegmentId()); model.sequence = sequence; if (Objects.nonNull(orderSegment.getDepartureLocation())) { model.departureLocationCode = orderSegment.getDepartureLocation().getLocationCode(); model.departureLocationName = ConvertUtil.getLocationName(orderSegment.getDepartureLocation()); } if (Objects.nonNull(orderSegment.getArrivalLocation())) { model.arriveLocationCode = orderSegment.getArrivalLocation().getLocationCode(); model.arriveLocationName = ConvertUtil.getLocationName(orderSegment.getArrivalLocation()); } model.departureTime = DateUtil.parseStringToDateTime(orderSegment.getDepartureDateTime(), DateUtil.YYYY_MM_DDHHmm); model.arriveTime = DateUtil.parseStringToDateTime(orderSegment.getArrivalDateTime(), DateUtil.YYYY_MM_DDHHmm); ... return model; }

仓储

仓储封装于聚合根内部,不用于外部调用,故通过工厂方法将仓储注入聚合根中。

1
@Slf4j @Component public class OrderFactory { @Autowired private OrderIdGenerator orderIdGenerator; @Autowired private P2pOrderRepository repository; public P2pOrder create(CreateOrderRequest request) { long orderId = orderIdGenerator.generateOrderId(); if (orderId < 1) { log.error("fail to gen order id"); throw new BusinessException(ResponseCodeEnum.FAIL_GEN_ORDER_ID); } return new P2pOrder(repository, orderId); } }

仓储用于链接领域层与数据层,使领域对象与DAO隔离,使我们软件更加健壮。

1
@Slf4j @Component public class P2pOrderRepositoryImpl implements P2pOrderRepository { @Autowired private OrderMapper orderMapper; @Override public boolean createP2pOrder(P2pOrder p2pOrder) { OrderMasterEntity orderMasterEntity = new OrderMasterEntity; orderMasterEntity.setOrderId(p2pOrder.getOrderId()); orderMasterEntity.setOrderStatus( p2pOrder.getOrderMasterModel().getOrderStatus().getCode()); ... return orderMapper.insert(orderMasterEntity) > 0; } }

在软件开发中,防腐层是一种重要的设计模式,它有助于维护系统的稳定性和可维护性。以下是对防腐层概念和应用的详细阐述:

防腐层的概念防腐层,也被称作适配层,是一种设计模式,用于隔离系统内部模型与外部上下文。当系统需要与外部系统或服务交互时,防腐层能够将外部数据或行为转化为系统内部能够理解和处理的形式。

防腐层的作用

  1. 隔离外部变化:通过防腐层,系统的内部逻辑可以避免因外部系统的变更而频繁修改。
  2. 数据转换:防腐层负责将外部数据格式转换为内部模型,确保数据的一致性和正确性。
  3. 服务适配:防腐层能够适配不同的服务接口,使得系统能够灵活地与多种外部服务进行交互。

仓储作为防腐层在某些情况下,仓储模式也可以视为一种防腐层。它通过将数据库操作抽象化,将数据库中的记录映射为系统内部的实体和值对象,从而隔离了数据访问层和业务逻辑层。

服务结构的DDD应用采用领域驱动设计(DDD)的思想进行系统建模,可以构建出更加清晰和灵活的服务结构。DDD的六边形架构是一种常见的实现方式,它通过定义清晰的领域边界和交互协议,使得系统更加易于扩展和维护。

重构后的服务结构

  1. 领域层:包含业务逻辑和领域模型,是系统的核心。
  2. 应用层:负责协调领域层和基础设施层,处理应用程序的用例。
  3. 基础设施层:提供技术实现,如数据库访问、消息传递等。
  4. 防腐层:位于应用层和基础设施层之间,负责外部服务的适配和数据转换。 通过上述结构,系统可以更加灵活地应对外部变化,同时保持内部逻辑的清晰和稳定。

五、总结

本文以携程国际火车票出票系统为例,探讨了领域驱动设计(Domain-Driven Design, DDD)在实际开发中的应用。通过将系统划分为多个领域,使得业务逻辑更加明确,代码维护和迭代更加容易。领域驱动设计的核心思想是通过六边形架构将业务逻辑与技术实现分离,提高代码的可读性,并通过防腐层隔离外部上下文与内部模型,防止外部对象对内部模型的侵蚀。此外,通过以领域模型为驱动,将需求迭代转化为模型的更新,使功能开发更加可控,避免了软件架构设计模式的混乱。

领域驱动设计实践要点

  1. 领域划分:明确系统的不同领域,使业务逻辑更加清晰。
  2. 六边形架构:业务逻辑与技术实现分离,提高代码的可读性。
  3. 防腐层:隔离外部上下文与内部模型,防止外部对象侵蚀。
  4. 领域模型驱动:以领域模型为驱动,实现功能开发的可控性。

作者自述作者Ma Ning,作为携程国际火车票后端开发工程师,对系统架构、微服务、高可用等领域有深入研究。本文基于作者的实践经验,对领域驱动设计进行了探讨。由于作者经验有限,对DDD的理解可能存在不足,欢迎业界同仁提出宝贵意见,共同进步。

参考文献1. Scott Millett 等著,蒲成 译;《领域驱动设计模式、原理与实践》;清华大学出版社,2016。2. Eric Evans 著,赵俐 等译;《领域驱动设计:软件核心复杂性应对之道》;人民邮电出版社,2010。3. 领域驱动设计在互联网业务开发中的实践。4. 阿里技术专家详解DDD系列 第二讲

  • 应用架构。5. 基于 DDD 思想的酒店报价重构实践。6. DDD(领域驱动设计)总结。7. 谈谈MVC模式。8. 阿里技术专家详解DDD系列 第三讲
  • Repository模式。9. 领域驱动设计详解:是什么、为什么、怎么做?10. 领域建模在有赞客户领域的实践。11. DDD分层架构的三种模式。

团队招聘信息携程火车票研发团队致力于火车票业务的开发与创新,不断探索和创新在多种交通线路联程联运算法、多种交通工具一站式预定、高并发方向。团队持续优化用户体验,提高效率,为全球人民提供便捷的火车票购买服务。

加入我们,你将与技术大牛一起工作,让你的产品和代码服务亿万用户,提升全球旅行者的出行体验。 技术岗位:前端、后台、算法、大数据、测试等。 简历投递:tech@trip.com,邮件标题格式:【姓名】-【携程火车票】-【投递职位】。

作者简介Ma Ning,携程国际火车票后端开发工程师,专注于系统架构、微服务、高可用等技术领域。