前言

在软件开发领域,领域驱动设计(Domain-Driven Design,简称DDD)的概念已经存在超过30年。尽管在互联网快速发展的今天,DDD可能看起来是一种较为传统的设计方法,但随着互联网企业深入到实体经济,业务变得日益复杂,DDD的重要性逐渐凸显。本文将首先探讨在现代软件开发中遇到的一些挑战,然后探讨如何运用DDD来解决这些问题。

问题

过度耦合

在业务发展的初期阶段,系统功能相对简单,通常的创建、读取、更新和删除(CRUD)操作就足以满足需求,系统结构清晰。但随着业务的不断迭代和扩展,业务逻辑变得复杂,系统也随之变得混乱。各个模块之间的关联性增强,导致很难明确界定每个模块的具体功能和意图。在修改某个功能时,需要花费大量时间来追踪和理解影响的范围,修改本身也可能带来不可预见的连锁反应。 以下是系统耦合的一个典型示例图。 系统耦合示例

解决方案

(此部分将在下文展开,探讨如何利用DDD解决上述问题。)

在软件开发过程中,我们经常面临系统架构不清晰、模块内聚度低和高耦合的问题。这些问题往往导致代码维护困难,特别是在订单服务接口等复杂系统中。以下是对上述问题的分析和解决方案的梳理:

系统架构问题

  • 问题描述:在订单服务接口中,由于系统设计不够清晰,改动一处可能影响到整个系统。
  • 影响:即使是小的改动,如评价功能,也可能影响到核心的创单流程。

解决方案

  • 演进式设计:随着业务增长,系统设计应随之演进,而不是提前过度设计。
  • 敏捷实践:通过重构、测试驱动设计和持续集成来应对混乱。

敏捷实践中的问题

  • 重构:虽然可以改善局部设计,但可能缺乏业务含义,导致新成员难以理解。
  • 项目规范:制定规范可能不是最佳方案,代码腐败和重构成为循环问题。

领域驱动设计(DDD)

  • 概念模型:解决设计到代码的转换问题,确保设计模型具有实际的业务含义。
  • 同步与演化:DDD帮助领域模型与设计模型同步演化,转化为实际代码。

贫血症和失忆症

  • 贫血领域对象:指仅作为数据载体,缺乏行为的领域对象。
  • 问题:这种对象导致以数据为中心的开发模式,忽视了OO理论的应用。

实际案例分析

  • 系统抽奖平台
  • 场景需求:根据预设概率从奖池中抽取奖项。
  • 实现方案:设计奖池和奖项的数据库配置,然后通过生成随机数匹配奖项。

结论

  • 通过DDD,我们可以更好地将业务需求转化为代码实现,避免贫血领域对象的问题,提高系统的可维护性和扩展性。

  • 设计AwardPool和Award两个对象,只有简单的get和set属性的方法

1
class AwardPool { int awardPoolId; List<Award> awards; public List<Award> getAwards() { return awards; } public void setAwards(List<Award> awards) { this.awards = awards; } ...... } class Award { int awardId; int probability;//概率 ...... }
  • Service代码实现

设计一个LotteryService,在其中的drawLottery()方法写服务逻辑

1
AwardPool awardPool = awardPoolDao.getAwardPool(poolId);//sql查询,将数据映射到AwardPool对象 for (Award award : awardPool.getAwards()) { //寻找到符合award.getProbability()概率的award }

在软件开发过程中,随着业务逻辑的增长,传统的模型-视图-控制器(MVC)架构可能无法满足需求,特别是当业务逻辑变得复杂时。此时,领域驱动设计(DDD)提供了一种更好的解决方案。以下是对DDD及其在软件架构中应用的详细解析:

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

业务逻辑的封装在DDD中,业务逻辑被封装在领域模型中,而非仅仅作为数据载体。例如,在抽奖系统中,奖品的选择逻辑应该封装在AwardPool类中,而非散布在多个服务方法中。

应对软件系统的复杂性DDD通过以下三种方式应对软件系统的复杂性:

  • 分治:将问题分解为更小、更易于管理的部分,并确保这些部分的内聚性。
  • 抽象:通过抽象简化问题,使问题更易于理解和解决。
  • 知识:DDD本身可以看作是一种知识体系,指导我们如何进行抽象和分治。

与微服务架构的协同DDD与微服务架构在理念上是相辅相成的。微服务架构强调创建高内聚、低耦合的服务,而DDD中的限界上下文概念与微服务的要求不谋而合。

架构设计层面

在DDD中,架构设计活动可以分为三个层面:

  1. 业务架构:根据业务需求设计业务模块及其相互关系。
  2. 系统架构:设计系统和子系统的模块。
  3. 技术架构:选择适合的技术及框架。 DDD的核心是将业务架构映射到系统架构上,确保在业务需求变化时,系统架构能够灵活调整。

架构设计的顺序和重要性

在实际开发中,业务架构、系统架构和技术架构的确定并没有固定的先后顺序。但在业务逻辑较为复杂的情况下,跳过业务架构的设计可能导致系统难以适应需求变化。

结论

DDD通过将业务逻辑封装在领域模型中,并与现实世界的业务对象相映射,提供了一种有效的解决方案来应对软件系统的复杂性。同时,DDD与微服务架构的结合,能够创建出既灵活又可维护的软件系统。

如何实践领域驱动设计(DDD)

领域驱动设计(DDD)是一种软件开发方法,它强调以领域为中心的软件开发。以下是通过抽奖平台案例来介绍如何实践DDD,实现系统的高内聚和低耦合。

抽奖系统需求概览

  • 运营:配置抽奖活动,面向特定用户群体,发放多种奖品。
  • 用户:参与不同类型抽奖活动。

设计领域模型的步骤

  1. 划分领域和限界上下文:根据需求初步划分领域和上下文及其关系。
  2. 识别实体和值对象:进一步分析上下文,识别实体和值对象。
  3. 关联和聚合:对实体、值对象进行关联和聚合,确定聚合范畴和聚合根。
  4. 设计仓储:为聚合根设计仓储,考虑实体或值对象的创建方式。
  5. 实践与检验:在工程中实践领域模型,检验并重构模型。

战略建模

  • 战略设计:高层次、宏观划分和集成限界上下文。
  • 战术设计:使用建模工具细化上下文。

领域与限界上下文

  • 领域:包含问题域和解系统,软件是现实世界的部分模拟。
  • 限界上下文:具有特定职责的边界,领域模型存在于此边界内。

划分限界上下文的方法

  • 不应按技术架构或开发任务划分,而应按语义边界考虑。
  • 从产品通用语言中提取概念对象,寻找对象间联系。
  • 从需求中提取动词,观察动词与对象的关系。
  • 形成界限上下文后,用语言描述其职责,确保清晰、准确、简洁、完整。

抽奖平台的限界上下文划分根据业务特点,将抽奖平台划分为两个子域:

  • C端抽奖:用户高频参与,无感知配置。
  • M端抽奖管理平台:运营低频复杂配置,完全解耦。

实践案例通过上述方法,我们对抽奖平台进行了DDD实践,实现了系统的解耦和内聚,提高了开发效率和系统可维护性。

在确认了M端领域和C端的限界上下文后,我们再对各自上下文内部进行限界上下文的划分。下面我们用C端进行举例。

产品的需求概述如下:

1
1. 抽奖活动有活动限制,例如用户的抽奖次数限制,抽奖的开始和结束的时间等; 2. 一个抽奖活动包含多个奖品,可以针对一个或多个用户群体; 3. 奖品有自身的奖品配置,例如库存量,被抽中的概率等,最多被一个用户抽中的次数等等; 4. 用户群体有多种区别方式,如按照用户所在城市区分,按照新老客区分等; 5. 活动具有风控配置,能够限制用户参与抽奖的频率。

根据产品的需求,我们提取了一些关键性的概念作为子域,形成我们的限界上下文。


抽奖平台的业务架构设计是围绕抽奖上下文展开的,它作为整个系统的核心,负责用户抽奖的主要流程。在设计初期,我们考虑将抽奖和发奖划分为两个不同的领域,但随着开发过程的深入,我们发现这两个领域之间的逻辑联系紧密,难以分割。发奖过程相对简单,主要依赖第三方服务进行奖品的发放。因此,我们决定将它们合并为一个统一的抽奖上下文。 抽奖平台还涉及其他几个关键上下文:

  • 活动准入上下文:定义了活动参与的通用规则,包括活动的时间限制和参与次数限制。

  • 风控上下文:针对C端用户的刷单行为,我们定义了风控上下文,以确保活动的公平性。

  • 计数上下文:由于活动准入、风控和抽奖等环节都涉及到次数的限制,计数上下文负责管理这些限制。

  • 库存上下文:由于库存管理的通用性,我们将其定义为一个独立的上下文,关注库存内容的核销。 通过领域驱动设计(DDD)的限界上下文划分,我们明确了抽奖、活动准入、风控、计数和库存五个上下文,每个上下文都具有高度的内聚性。

上下文映射图

在上下文划分后,我们进一步梳理了上下文之间的关系,遵循康威定律,确保系统结构与组织结构的一致性。这有助于任务的拆分和团队内沟通的顺畅。同时,明确的上下文关系也有助于团队间的协作和领域概念的明确。 限界上下文之间的映射关系包括:

  • 合作关系(Partnership)

  • 共享内核(Shared Kernel)

  • 客户方-供应方开发(Customer-Supplier Development)

  • 遵奉者(Conformist)

  • 防腐层(Anticorruption Layer)

  • 开放主机服务(Open Host Service)

  • 发布语言(Published Language)

  • 大泥球(Big Ball of Mud)

  • 另谋他路(SeparateWay) 这些映射关系帮助我们理解上下文间的交互方式,确保系统的稳定性和可维护性。 最后,我们根据反复斟酌的结果,绘制了抽奖平台上下文的映射关系图,以指导后续的开发和维护工作。

抽奖领域上下文建模

一、上下文关系概述在抽奖领域中,风控、活动准入、库存、计数等上下文与抽奖上下文存在紧密的合作关系(PartnerShip,简称PS),它们相互依赖,共同发展。抽奖上下文在发券时,依赖于券码、平台券、外卖券三个上下文,通过防腐层(Anticorruption Layer,ACL)实现隔离,并通过开放主机服务(Open Host Service)提供访问机制。

限界上下文耦合性

  • 内部交互:抽奖领域内部上下文之间通过合作关系实现数据耦合。
  • 外部交互:抽奖上下文与外部上下文通过防腐层限制耦合度。

二、战术建模——细化上下文

定义与实践

  • 实体(Entity):具有唯一标识的对象,如公安系统的身份信息。
  • 实践建议:将属性验证放入实体中。
  • 值对象(Value Object):没有唯一标识,用于描述事务的对象,如颜色信息。
  • 特性:不变性、相等性、可替换性。
  • 实践建议:创建后不可修改,避免系统复杂性。

聚合根与聚合设计

  • 聚合根(Aggregate Root):聚合的根节点,代表一组相关对象。
  • 聚合设计原则
  • 边界内一致性:一个事务只修改一个聚合实例。
  • 设计小聚合:尽量只包含根实体。
  • 引用唯一标识:通过唯一标识引用其他聚合或实体。

数据库与聚合关系

  • 数据库设计:聚合内部关系指导数据库表结构,注意值对象单独建表时的id处理。

领域服务与领域事件

  • 领域服务:不属于实体或值对象的领域行为或操作。
  • 领域事件:对领域内活动进行建模。

三、抽奖上下文建模案例抽奖平台的核心上下文是抽奖上下文,以下是对抽奖上下文的建模示例。

抽奖上下文建模要点

  • 明确上下文边界。
  • 定义实体与值对象。
  • 设计合理的聚合结构。
  • 通过领域服务暴露业务逻辑。
  • 使用领域事件响应领域内活动。

实践步骤1. 确定抽奖上下文的实体与值对象。2. 设计聚合,确保边界内一致性。3. 通过领域服务实现业务逻辑的封装与暴露。4. 利用领域事件处理抽奖过程中的关键活动。

通过上述步骤,可以实现抽奖上下文的有效建模,确保系统的清晰性与可维护性。

在抽奖上下文中,我们通过抽奖(DrawLottery)这个聚合根来控制抽奖行为,可以看到,一个抽奖包括了抽奖ID(LotteryId)以及多个奖池(AwardPool),而一个奖池针对一个特定的用户群体(UserGroup)设置了多个奖品(Award)。

另外,在抽奖领域中,我们还会使用抽奖结果(SendResult)作为输出信息,使用用户领奖记录(UserLotteryLog)作为领奖凭据和存根。

谨慎使用值对象

在实践中,我们发现虽然一些领域对象符合值对象的概念,但是随着业务的变动,很多原有的定义会发生变更,值对象可能需要在业务意义具有唯一标识,而对这类值对象的重构往往需要较高成本。因此在特定的情况下,我们也要根据实际情况来权衡领域对象的选型。

DDD工程实现

在对上下文进行细化后,我们开始在工程中真正落地DDD。

模块

模块(Module)是DDD中明确提到的一种控制限界上下文的手段,在我们的工程中,一般尽量用一个模块来表示一个领域的限界上下文。

如代码中所示,一般的工程中包的组织方式为{com.公司名.组织架构.业务.上下文.*},这样的组织结构能够明确的将一个上下文限定在包的内部。

1
import com.company.team.bussiness.lottery.*;//抽奖上下文 import com.company.team.bussiness.riskcontrol.*;//风控上下文 import com.company.team.bussiness.counter.*;//计数上下文 import com.company.team.bussiness.condition.*;//活动准入上下文 import com.company.team.bussiness.stock.*;//库存上下文

代码演示1 模块的组织

对于模块内的组织结构,一般情况下我们是按照领域对象、领域服务、领域资源库、防腐层等组织方式定义的。

1
import com.company.team.bussiness.lottery.domain.valobj.*;//领域对象-值对象 import com.company.team.bussiness.lottery.domain.entity.*;//领域对象-实体 import com.company.team.bussiness.lottery.domain.aggregate.*;//领域对象-聚合根 import com.company.team.bussiness.lottery.service.*;//领域服务 import com.company.team.bussiness.lottery.repo.*;//领域资源库 import com.company.team.bussiness.lottery.facade.*;//领域防腐层

模块的组织与领域对象设计

在软件开发过程中,模块的组织和领域对象的设计是至关重要的。本文将详细探讨如何通过领域驱动设计(Domain-Driven Design, DDD)来解决对象的贫血问题,并以抽奖系统为例进行说明。

领域驱动设计概述

领域驱动设计是一种软件开发方法,它强调以业务领域为中心,通过创建有丰富行为的领域对象来实现业务逻辑。

解决贫血问题

所谓贫血问题,指的是在某些设计中,对象仅作为数据的载体,而缺乏行为。这会导致业务逻辑分散在各处,难以维护和扩展。

领域对象的构成

领域对象通常包括实体(Entity)和值对象(Value Object)。实体具有唯一标识,而值对象则用于表示不可变的属性集合。

抽奖系统领域对象分析

抽奖聚合根(DrawLottery)

抽奖聚合根是抽奖活动的核心,它包含以下特性:

  • 唯一标识:每个抽奖活动都有一个唯一的ID。
  • 奖池列表:包含所有可用的奖池,用于存储奖品信息。

抽奖领域功能

抽奖聚合根的主要功能是chooseAwardPool,即根据抽奖上下文选择适配的奖池。这一过程如下:

  1. 上下文信息DrawLotteryContext携带用户抽奖时的场景信息,如用户得分或所在城市。
  2. 匹配奖池DrawLottery根据上下文信息,匹配一个合适的AwardPool来发放奖品。

奖池值对象(AwardPool)

奖池值对象用于表示奖品集合,它可能包含以下信息:

  • 奖品类型:奖品的种类,如实物奖品或虚拟奖品。
  • 奖品数量:每种奖品的可用数量。

总结

通过上述分析,我们可以看到领域驱动设计如何帮助我们构建具有丰富行为的领域对象,从而解决贫血问题。在抽奖系统中,通过定义清晰的聚合根和值对象,我们可以更好地组织代码,提高系统的可维护性和扩展性。 在下文中,我们将具体展开每个模块的实现细节,以展示如何将理论应用到实践之中。

1
package com.company.team.bussiness.lottery.domain.aggregate; import ...; public class DrawLottery { private int lotteryId; //抽奖id private List<AwardPool> awardPools; //奖池列表 //getter & setter public void setLotteryId(int lotteryId) { if(id<=0){ throw new IllegalArgumentException("非法的抽奖id"); } this.lotteryId = lotteryId; } //根据抽奖入参context选择奖池 public AwardPool chooseAwardPool(DrawLotteryContext context) { if(context.getMtCityInfo()!=null) { return chooseAwardPoolByCityInfo(awardPools, context.getMtCityInfo()); } else { return chooseAwardPoolByScore(awardPools, context.getGameScore()); } } //根据抽奖所在城市选择奖池 private AwardPool chooseAwardPoolByCityInfo(List<AwardPool> awardPools, MtCifyInfo cityInfo) { for(AwardPool awardPool: awardPools) { if(awardPool.matchedCity(cityInfo.getCityId())) { return awardPool; } } return null; } //根据抽奖活动得分选择奖池 private AwardPool chooseAwardPoolByScore(List<AwardPool> awardPools, int gameScore) {...} }

代码演示3 DrawLottery

在匹配到一个具体的奖池之后,需要确定最后给用户的奖品是什么。这部分的领域功能在AwardPool内。

1
package com.company.team.bussiness.lottery.domain.valobj; import ...; public class AwardPool { private String cityIds;//奖池支持的城市 private String scores;//奖池支持的得分 private int userGroupType;//奖池匹配的用户类型 private List<Awrad> awards;//奖池中包含的奖品 //当前奖池是否与城市匹配 public boolean matchedCity(int cityId) {...} //当前奖池是否与用户得分匹配 public boolean matchedScore(int score) {...} //根据概率选择奖池 public Award randomGetAward() { int sumOfProbablity = 0; for(Award award: awards) { sumOfProbability += award.getAwardProbablity(); } int randomNumber = ThreadLocalRandom.current().nextInt(sumOfProbablity); range = 0; for(Award award: awards) { range += award.getProbablity(); if(randomNumber<range) { return award; } } return null; } }

领域对象与资源库的概念

领域对象是业务逻辑的核心,与传统的业务对象相比,它不仅包含数据,还拥有行为,使得对象更加完整和丰富。将逻辑封装在领域对象内,可以提高内聚性,明确职责。

资源库的角色

资源库(Repository)是领域对象与存储介质之间的桥梁,负责统一管理领域对象的存储和访问。存储介质可以是数据库、分布式缓存或本地缓存等多种形式。

抽奖平台的资源库实现

在抽奖平台中,资源库的组织方式如下:

  1. 定义领域对象:首先定义领域对象,使其具备必要的属性和行为。
  2. 实现资源库接口:为领域对象实现资源库接口,该接口定义了存储和访问数据的方法。
  3. 选择存储介质:根据业务需求选择合适的存储介质,如关系型数据库、NoSQL数据库或缓存系统。
  4. 封装存储逻辑:在资源库实现中封装具体的存储逻辑,确保领域对象与存储介质的交互是透明的。 通过这种方式,资源库不仅简化了领域对象与存储介质之间的交互,而且提高了代码的可维护性和可扩展性。
1
//数据库资源 import com.company.team.bussiness.lottery.repo.dao.AwardPoolDao;//数据库访问对象-奖池 import com.company.team.bussiness.lottery.repo.dao.AwardDao;//数据库访问对象-奖品 import com.company.team.bussiness.lottery.repo.dao.po.AwardPO;//数据库持久化对象-奖品 import com.company.team.bussiness.lottery.repo.dao.po.AwardPoolPO;//数据库持久化对象-奖池 import com.company.team.bussiness.lottery.repo.cache.DrawLotteryCacheAccessObj;//分布式缓存访问对象-抽奖缓存访问 import com.company.team.bussiness.lottery.repo.repository.DrawLotteryRepository;//资源库访问对象-抽奖资源库

代码演示5:Repository组织结构

概述

资源库(Repository)是对外提供整体访问的核心组件,它不仅整合了各个资源库的数据信息,还负责资源存储的逻辑,例如实现缓存更新机制等。

抽奖资源库的设计

在抽奖资源库的设计中,我们采用了一种策略,即屏蔽对底层奖池和奖品的直接访问,而是通过聚合根来管理抽奖资源。这种方式提高了代码的安全性和封装性。

缓存旁路模式(Cache Aside Pattern)示例代码示例中展示了如何使用缓存旁路模式来获取抽奖资源。这是一种常见的缓存更新策略,可以有效地减少对数据库的直接访问,提高系统性能。

资源库与服务的职责分离与传统将资源管理集成在服务中的做法不同,本设计将资源管理职责明确地交给了资源库。这样做的好处是,它使得代码的可读性和可维护性得到了显著提升。

结构化与条理性通过将资源库的职责明确化,我们能够更清晰地组织代码结构,实现逻辑的分层,从而使得整个系统更加模块化,易于理解和维护。

1
package com.company.team.bussiness.lottery.repo; import ...; @Repository public class DrawLotteryRepository { @Autowired private AwardDao awardDao; @Autowired private AwardPoolDao awardPoolDao; @AutoWired private DrawLotteryCacheAccessObj drawLotteryCacheAccessObj; public DrawLottery getDrawLotteryById(int lotteryId) { DrawLottery drawLottery = drawLotteryCacheAccessObj.get(lotteryId); if(drawLottery!=null){ return drawLottery; } drawLottery = getDrawLotteyFromDB(lotteryId); drawLotteryCacheAccessObj.add(lotteryId, drawLottery); return drawLottery; } private DrawLottery getDrawLotteryFromDB(int lotteryId) {...} }

抗腐蚀层概述

抗腐蚀层,亦称为适配层,是一种软件设计模式,用于在不同上下文或服务之间进行交互时,保护内部逻辑不受外部变化的影响。以下是引入抗腐蚀层的几种常见情况:

  1. 模型转换:当需要将外部上下文的模型转换为当前上下文能够理解的模型时。
  2. 团队协作:在不同团队之间进行协作,特别是当存在供奉者关系时,引入抗腐蚀层可以防止外部上下文的变更对当前上下文造成影响。
  3. 广泛使用:如果对外部上下文的访问在内部多个上下文中非常普遍,为了避免修改带来的广泛影响,可以考虑使用抗腐蚀层。 在抽奖平台的设计中,我们引入了用户城市信息的抗腐蚀层(UserCityInfoFacade),它作为一个中间层,处理来自外部用户城市信息服务的交互。

抗腐蚀层的实现

以用户信息抗腐蚀层为例,其主要功能是接收抽奖请求参数(LotteryContext),并将其转换为城市信息(MtCityInfo)的输出。这样的设计可以确保抽奖平台的内部逻辑与外部服务的耦合度降低,提高系统的稳定性和可维护性。

结构化示例

假设我们有一个抽奖请求,其参数可能包括用户ID、抽奖类型等。抗腐蚀层将这些参数作为输入,然后进行以下步骤:

  • 验证输入参数的有效性。
  • 调用外部服务获取用户的城市信息。
  • 将获取到的城市信息转换为内部模型。
  • 返回转换后的模型供抽奖逻辑使用。 通过这种方式,即使外部服务的接口或数据格式发生变化,我们也可以在抗腐蚀层中进行相应的调整,而不会影响到抽奖平台的核心逻辑。
1
package com.company.team.bussiness.lottery.facade; import ...; @Component public class UserCityInfoFacade { @Autowired private LbsService lbsService;//外部用户城市信息RPC服务 public MtCityInfo getMtCityInfo(LotteryContext context) { LbsReq lbsReq = new LbsReq(); lbsReq.setLat(context.getLat()); lbsReq.setLng(context.getLng()); LbsResponse resp = lbsService.getLbsCityInfo(lbsReq); return buildMtCifyInfo(resp); } private MtCityInfo buildMtCityInfo(LbsResponse resp) {...} }

领域服务概述

在软件开发中,领域服务扮演着重要的角色。它通过整合领域对象、资源库和防腐层等组件,为其他上下文提供交互接口。以下是对领域服务的详细解析。

领域对象的行为封装

领域对象是业务逻辑的核心,它们封装了业务行为。每个领域对象都负责特定的业务规则和逻辑。

资源库的资源管理行为封装

资源库是领域对象与数据存储之间的桥梁。它们管理数据的存取,确保数据的一致性和完整性。

防腐层的外部交互行为封装

防腐层负责与外部系统的交互,它隔离了外部变化对内部领域模型的影响。

领域服务的职责

领域服务作为协调者,串联上述组件,提供清晰的业务交互接口。以抽奖服务issueLottery为例,领域服务的逻辑清晰,职责明确。 在实现过程中,我们通常会省略一些防御性逻辑,如异常处理和空值判断,以简化领域服务的实现。

结构化示例

以下是对领域服务实现的一个结构化示例:

  1. 初始化
  • 设置抽奖服务所需的初始状态和参数。
  1. 验证
  • 检查输入数据的有效性,确保抽奖条件满足。
  1. 执行
  • 执行抽奖逻辑,生成中奖结果。
  1. 反馈
  • 将抽奖结果反馈给用户或其他系统。
  1. 记录
  • 记录抽奖过程和结果,为后续分析和审计提供数据。 注意:在实际开发中,还应考虑异常处理和日志记录等辅助功能。
1
package com.company.team.bussiness.lottery.service.impl import ...; @Service public class LotteryServiceImpl implements LotteryService { @Autowired private DrawLotteryRepository drawLotteryRepo; @Autowired private UserCityInfoFacade UserCityInfoFacade; @Autowired private AwardSendService awardSendService; @Autowired private AwardCounterFacade awardCounterFacade; @Override public IssueResponse issueLottery(LotteryContext lotteryContext) { DrawLottery drawLottery = drawLotteryRepo.getDrawLotteryById(lotteryContext.getLotteryId());//获取抽奖配置聚合根 awardCounterFacade.incrTryCount(lotteryContext);//增加抽奖计数信息 AwardPool awardPool = lotteryConfig.chooseAwardPool(bulidDrawLotteryContext(drawLottery, lotteryContext));//选中奖池 Award award = awardPool.randomChooseAward();//选中奖品 return buildIssueResponse(awardSendService.sendAward(award, lotteryContext));//发出奖品实体 } private IssueResponse buildIssueResponse(AwardSendResponse awardSendResponse) {...} }

代码演示8 LotteryService

数据流转


在抽奖平台的构建中,我们遵循了一套清晰的数据流转和架构设计原则。以下是对整个流程和架构的梳理:

数据流转和对象转换

  1. 数据交互:领域开放服务通过信息传输对象(DTO)与外部系统进行数据交换。
  2. 领域内部数据:领域对象(DO)作为领域内部数据和行为的载体。
  3. 数据库交互:资源库使用数据库持久化对象(PO)与数据库进行交互。
  4. 转换过程:DTO与DO的转换在领域服务内完成,而DO与PO的转换则在资源库内进行。 这种设计虽然增加了数据转换的步骤,但确保了数据对象的职责明确,使得数据流转更加清晰。

上下文集成

在系统架构中,上下文集成是关键。我们通常采用以下几种手段:

  • 开放领域服务接口
  • 开放HTTP服务
  • 消息发布-订阅机制 在我们的抽奖系统中,主要通过开放服务接口进行上下文间的交互。计数上下文作为一个通用上下文,为个其他上下文提供了访问接口。如果需要在上下文间进行集成且保持一定隔离和适配,可以引入防腐层的概念。

领域分离

在实施领域模型时,我们采用了微服务架构风格,这与Vernon在《实现领域驱动设计》中描述的有所不同。具体差异可以在阅读他的书中找到。

  1. 微服务架构:领域服务独立部署,并通过服务接口对外暴露。
  2. 领域逻辑:领域对外的业务逻辑完全依赖于领域服务。
  3. 应用服务层:相对简单,主要负责获取请求参数并调度领域服务以实现界面层功能。 这种架构设计有助于保持系统的灵活性和可扩展性。

结论

通过上述实践,我们构建了一个职责分明、数据流转清晰的抽奖平台。微服务架构的应用使得系统更加模块化,便于维护和扩展。

随着业务的不断发展,我们的业务系统迅速扩张,当其成为核心系统时,应用服务的角色也随之发生了变化。以下是对这一变化的详细阐述:

应用服务的角色转变

  1. 领域逻辑的缺失:虽然应用服务本身不包含领域逻辑,但它需要对多个领域服务进行编排。
  2. 业务逻辑的积累:随着业务规模的扩大,编排过程本身逐渐积累了丰富的业务逻辑。
  3. 稳定性与性能的统一:为了保持系统的稳定性和性能,需要将相关的措施统一起来,避免分散在不同地方。

应用服务的双重身份

  • 对内视角:应用服务在内部依然保持其应用服务的特性。
  • 对外视角:对外,应用服务逐渐转变为领域服务的角色,需要以微服务的形式对外提供服务。

微服务的实现

  • 将应用服务作为微服务暴露,可以提高系统的灵活性和可扩展性。
  • 微服务架构允许各个服务独立开发、部署和扩展,从而更好地适应快速变化的业务需求。

结论应用服务在业务系统成为核心系统的过程中,其角色和功能都发生了重要的转变。通过将其作为微服务进行管理和部署,可以更好地应对业务发展带来的挑战。


在软件开发中,架构设计是一项至关重要的工作。本文档将介绍一个抽奖应用服务的架构设计实践。

架构设计概述

架构设计需要根据团队和业务的具体情况来定制。除了常见的分层架构,CQRS(命令查询责任分离)架构也是一种有效的选择。

抽奖应用服务架构

本抽奖应用服务的架构设计包含以下几个关键领域服务:

  • 抽奖服务:负责抽奖逻辑的实现。
  • 活动准入服务:确保用户符合参与活动的条件。
  • 风险控制服务:对可能的风险进行识别和控制。

服务组织结构

这些服务将被组织成一套完整的系统,以提供功能完备的抽奖应用服务。具体组织方式如下:

  1. 客户端集成:客户端通过API与服务进行交互。
  2. 领域服务集成:各个领域服务协同工作,共同提供服务。
  3. 数据流管理:确保数据在服务间有效流动。

注意事项

  • 架构设计应保持灵活性,以适应未来可能的业务变化。
  • 考虑使用现代架构模式,如微服务,以提高系统的可扩展性和可维护性。
1
package ...; import ...; @Service public class LotteryApplicationService { @Autowired private LotteryRiskService riskService; @Autowired private LotteryConditionService conditionService; @Autowired private LotteryService lotteryService; //用户参与抽奖活动 public Response<PrizeInfo, ErrorData> participateLottery(LotteryContext lotteryContext) { //校验用户登录信息 validateLoginInfo(lotteryContext); //校验风控 RiskAccessToken riskToken = riskService.accquire(buildRiskReq(lotteryContext)); ... //活动准入检查 LotteryConditionResult conditionResult = conditionService.checkLotteryCondition(otteryContext.getLotteryId(),lotteryContext.getUserId()); ... //抽奖并返回结果 IssueResponse issueResponse = lotteryService.issurLottery(lotteryContext); if(issueResponse!=null && issueResponse.getCode()==IssueResponse.OK) { return buildSuccessResponse(issueResponse.getPrizeInfo()); } else { return buildErrorResponse(ResponseCode.ISSUE_LOTTERY_FAIL, ResponseMsg.ISSUE_LOTTERY_FAIL) } } private void validateLoginInfo(LotteryContext lotteryContext){...} private Response<PrizeInfo, ErrorData> buildErrorResponse (int code, String msg){...} private Response<PrizeInfo, ErrorData> buildSuccessResponse (PrizeInfo prizeInfo){...} }

领域驱动设计在互联网业务中的应用

摘要

本文通过领域驱动设计(DDD)的视角,探讨了其在复杂互联网业务系统中的实际应用。我们从理论到实践,逐步展示了如何利用DDD来优化系统架构,提高软件设计的质量和效率。

引言

在软件开发过程中,DDD作为一种设计方法论,帮助开发者更深入地理解业务领域,构建出更加合理和可维护的系统。本文将详细介绍DDD的核心概念及其在实际业务中的应用策略。

DDD的核心概念

  • 领域模型:反映业务概念和业务逻辑的模型。
  • 实体:具有唯一标识和生命周期的对象。
  • 值对象:描述领域特性但不具有唯一标识的对象。
  • 聚合:一组相关对象的集合,由根实体和子实体组成。
  • 领域服务:执行领域逻辑但不自然属于任何实体或值对象的服务。

DDD在实践中的应用

  1. 战略设计:确定业务领域和界限上下文。
  2. 战术设计:细化领域模型,实现高内聚低耦合。
  3. 演进式设计:逐步迭代和完善模型,适应快速变化的业务需求。

案例分析

以一个虚构的LotteryApplicationService为例,展示了如何将DDD应用于实际的业务场景中,实现业务逻辑的清晰划分和高效管理。

结语

尽管DDD是一种强大的设计工具,但它并不适用于所有情况。对于简单系统或特定类型的项目,如SmartUI,可能不需要引入DDD。此外,本文未涉及DDD在迭代过程中可能出现的模型腐化问题,这将在后续文章中进行讨论。

作者建议

作者根据自身经验,提出了对DDD的一些看法和建议。鼓励读者根据自己的实际情况和团队情况,选择适合的设计方法。

参考书籍

  • Eric Evans. 领域驱动设计. 赵俐、盛海艳、刘霞等译. 人民邮电出版社,2016.
  • Vaughn Vernon. 实现领域驱动设计. 滕云译. 电子工业出版社,2014.

作者简介

文彬、子维,美团点评资深研发工程师,专注于美团外卖营销相关研发工作,毕业于南京大学。

招聘信息

美团外卖上海研发中心长期招聘前端、客户端、后端、数据仓库和数据挖掘相关工程师。有兴趣的同学可以将简历发送至wenbin.lu@dianping.com。