高德信息业务DDD实战 - 聊聊用领域重构胶水代码
阿里妹导读
本文主要记录了高德信息业务DDD实战中如何用领域重构胶水代码。
一、背景
团队简介:高德信息业务us团队主要承接聚合下游服务(搜索、推荐、广告、离线、商服、营销等)面向端、车机等上游,持续、稳定的提供多行业、多品类、多场景的结构化数据服务。
目前高德信息业务门户有 美食、酒店、旅游、休娱、房产、商超、火客飞、周边游等行业或场景的portal页。由于每个业务的发展周期和节奏大不相同,每个细分行业下portal页的维护迭代不论是从产品、开发都是隔离的,自身团队的定位在没有严格统一的把控下比较容易产生胶水代码。这个现状对于信息业务的大目标-支撑好359行 来说是尤其困难的。
方案链路不一致,多种链路交替,各自维护;
存在重复逻辑,散落在不同应用中,代码各自为战,重复实现;
系统设计的质量不高,故障率和风险加大
开发维护成本大致随着portal页的数量线性增长
无知识管理、团队之间协作困难,容易产生信息孤岛和理解不一致
容易产生系统之间边界不清晰,加大维护成本
二、目标
对明确的固定场景,有统一的、稳定的实现方案
提高设计质量和可维护性,降低故障率和风险
减少重复工作,降低开发维护成本
提高知识管理,促进团队协作,避免信息孤岛和误解
使不同团队、系统之间的边界清晰,减少内耗
结合信息业务的实际情况,不难发现行业是一个可变的因素,但是场景相对来说比较固定,也就是说同样一个场景,需要同时支撑多个行业,每个行业在同一时间点,会有行业专属的发展状态、定制逻辑。
当场景相对固定后,面对相同的场景,业务系统更期待的是相对稳定、统一的技术方案和链路。举个例子:portal的金刚位,业务站在金刚位自身去考虑,应该主要包括运营配置、算法推荐、广告植入这些重要功能,并且将这些特性和功能提供给每个产品或运营负责的行业阵地中去,当某个行业需要该组件新的特性时,可以统一考虑设计,从而可以将该功能延展到其他行业使用。同时当流程和链路统一后,协议也就很容易做到了通用和一致。
多行业多形态的业务迭代下,产品的设计基于行业,能力的沉淀考虑不同行业间的共性和差异,进而做到不同场景的复用。业务创新、能力沉淀、场景复用可以形成有效紧密的闭环。
三、设计
0、什么是GBF
GBF(Gaode Business Framework)是高德信息业务提供的一套用于业务身份+场景策略的一套业务框架,框架自身借鉴了TMF的理念,结合领域建设(DDD)的思想,为产品业务和技术开发之间提供一套完善的、高效的、跨行业、多场景的轻量级框架。
1、为什么要用GBF
GBF使用了领域的概念:在研究某个问题时,将复杂问题划分为若干个相互独立的子问题,以便更好地组织、分析和解决问题。领域划分的作用在US的体现主要有以下几点:
提高问题分析的精度:使用划定好的领域,能够快速的将需求点定位到某个具体的问题域,从而将某一类问题归类管理,简化复杂性的同时也能够让领域内部的逻辑能更聚焦
促进知识共享和演进:使用领域构建系统后,某个具体的领域交给专人维护,培养领域专家,做好领域模型表达促进技术和产品之间的交流和合作,促进知识共享,高效支持业务分析。
提高复用性,降低开发成本:可以将相似的功能模块归为同一领域,使得不同业务能力可以共享同一领域的逻辑,提高代码的复用性。不同的开发小组可以同时并行开发各自的领域,避免小组之间的冲突和耦合问题,提高开发效率 提高稳定性:划分领域可以将系统分解成多个部分,使得每个部分的功能更加独立,发生问题时不会对整个系统产生影响,提高整体的稳定性。
拿个具体的例子来看:各行业portal页的金刚位。目前us的portal页金刚位不同场景、行业均有不同的实现方式,如果涉及到不同应用相似逻辑容易有cv代码的情况
景区、周边游之前配置在了diamond,后来切到了bff-other(一个提供轻量逻辑的服务,包含运营配置) 休娱portal页取的merger-sp(一个引擎代理,屏蔽不同引擎间的差异) 房产portal取的house-sp(另一个商品搜索引擎) 其他场景例如附近页取的bast(一个推荐引擎),结合diamond配置有干预逻辑
通过现状我们看到每个行业或场景的金刚位实现方案都是不一样的,不同的小组开发、不同的链路方案,意味着迭代维护成本居高不下,虽然目前的新方案在一个应用中统一对金刚位进行了收拢,但是只对输出进行了统一,底层的逻辑差异还在,且不同逻辑的实现无统一标准管理,不方便复用。
基于领域的系统建设,金刚位是一个具体的问题,适合放在一个地方解决实现。也正是基于领域建设这个抓手,将金刚位这个功能点统一组织、分析,拆解金刚位需要提供哪些能力,进而逻辑分类、链路归一,然后沉淀出金刚位统一的标准链路方案。
链路方案归一后,就可以统一输入和输出
bizId 身份id,代表不同行业(hotel、scenic、house、unkonw)。
scenario 场景id,例如 portal(落地页)、nearyby(附近页)、search(搜索)。
// 位置参数:依据推荐位id选择提供必要位置参数
user_loc 用户定位(坐标信息)
geoobj 图面信息(x1,y1;x2,y2)portal页场景一般不会有这个参数。
cityAdcode 城市code
// 透传公参
diu、adiu、uid、gsid、csid、usid、div、superid、stepid、tid 、testid
ajxVersion 版控
traceAppId 应用id
tracePageId 页面id
traceModuleId 模块id
{
"code": 1,
"message":"success",
"data": {
"items":[
{
"icon":"http://", //icon地址
"icon_height": 128,
"icon_width": 56,
"title":"海洋馆", //展示标题文案
"tool_tips": "9.9元", //icon右上角的气泡
"log_info": {}, //上报埋点信息,需要算法产生的无业务语义只用来统计指标的埋点在这里存放,如果业务字段中已存在的这里不再冗余
"schema": "uri://" //跳转schema uri://path?city=${adcode}&lat=${y}&lon=${x} 入参us传,querysug替换占位符生成schema
}
]
}
}
2、使用GBF概念设计
下面我们来看下,基于GBF的领域驱动设计理念去支撑业务开发,需要有哪些步骤
① 全局领域划分
目前在各个域内的逻辑相对复杂度不高,为避免过度设计,按照指导原则进行聚合设计出来8个领域,支持在后续领域复杂度提升支持进行域的拆分。领域建设的指导原则大致分下面几类:
1. 用例分析中出现的业务实体、业务规则可以进入域模型的候选集;
用例分析中出现的一个或者多个对象,可以进入域模型的候选集。对应种类:业务实体、业务规则;
正例:运营规则描述了在商品、内容上的加工与干预逻辑,可以作为分域的参考实体。对照可参考淘系的限购域;
反例:搜索、推广对应的实体为doc,业务应用体现在商品、poi的id列表及标签。doc实体自身没有业务属性,属于系统内功能实现层的模型。引擎召回的标签属性归属于poi或者商品主体,故doc不能作为域模型的参考实体;
2. 领域建立的前提条件是明确稳定的实体与稳定的行为;
反例:面向场景、行业设计领域,由于场景、行业的不可枚举(发散、不收敛),会导致域的不稳定。比如酒店域、旅游域;
3. 如果多个实体间是归属关系,需要合并成同一个域;
归属关系定义:在没有父实体存在的前提下,子实体无明确业务语义。
正例:营销时有活动规则、优惠规则,运营时有干预规则。脱离营销活动、运营的限定下,规则自身没有明确的业务语义。规则需要分别归属至对应的父实体;
4. 如果多个实体间为引用关系则各自成域;
引用关系定义:多个实体同时在多个用例中出现。任意实体的业务语义不依赖其他实体的补充。
正例:商铺会挂接在poi上,在大部分业务场景下会同时出现。挂接关系属于引用关系的一种;
反例:参见归属关系正例;
5. 分域时需要参考组织结构关系;
优惠和活动是否合为一个域?是。业务行为逻辑耦合度较高,例如:秒杀活动使用优惠券的方式支持。同时在信息工程视角,优惠与活动都属于营销中心;
② 统一领域建设
团队以前抽象和复用的角度主要关注点在链路上,缺少领域的管理,容易导致当链路发生变化时,各领域间的调用和依赖关系发生调整,协议也容易发生变化(系统模块间解耦不够清晰),不同应用之间出现逻辑相似度高但无法复用的代码。拿POI卡这个例子看,当抽象管理的维度添加领域对象后,可以归纳出如上图的结构关系,借此可以明确各领域的数据结构表达和管理规范每个领域之间的行为。当POI的核心领域和业务领域明确后,标准业务能力层的Process可以直接引用,两者之间只剩下引用关系,不再有任何的复杂加工逻辑。Process对应着不同的场景,自此POI的领域模型和方法对标准能力和场景解耦。
③ 系统分层,标准输入输出
gbf应用分层
gbf对象分层
④接口行为统一,差异定制实现
⑤portal整体架构
5.1结构
5.2 流程
3、使用GBF开发
使用GBF实现一个简单逻辑,以us的金刚位调用querysug下游为例
1、在repository包里定义fetcher的输入reqParam、输出 PO,在repo impl中创建声明式fetcher
public interface SugFetcher {
QuerySugResult<QuickLinkPO> getQuickLink( QuickLinkReqParam reqParam);
}
2、在repository中定义repo接口,在impl中定义实现
public interface QuickLinkRepo {
"获取金刚位") (title =
QuickLinkPO getQuickLink(QuickLinkReqParam reqParam);
}
public class QuickLinkRepoImpl implements QuickLinkRepo {
SugFetcher sugFetcher;
public QuickLinkPO getQuickLink(QuickLinkReqParam reqParam) {
return Optional.ofNullable(sugFetcher.getQuickLink(reqParam))
.map(QuerySugResult::getData)
.orElse(null);
}
}
3、定义金刚位的DO和入参(这里入参没有特殊参数直接使用了基类),定义金刚位的DomainService接口,和对应的实现类。
"运营领域金刚位服务", domain = OperationDomain.class) (name =
public interface IQuickLinkDomainService {
QuickLinkDO getQuickLink(BaseRequest baseRequest);
}
public class QuickLinkDomainService implements IQuickLinkDomainService {
QuickLinkRepo QuickLinkRepo;
IQuickLinkAbility quickLinkAbility;
public QuickLinkDO getQuickLink(BaseRequest baseRequest) {
//构建请求参数
final QuickLinkReqParam quickLinkReqParam = quickLinkAbility.buildQueryParam(baseRequest);
//请求金刚位服务
final QuickLinkPO quickLinkPO = quickLinkRepo.getQuickLink(quickLinkReqParam);
//根据返回结果构建金刚位DO(标准DO)可作为DTO输出
final quickLinkDO quickLinkDO = quickLinkAbility.buildQuickLinkDO(quickLinkPO);
return quickLinkDO;
}
}
抽象领域方法内的行为,生成金刚位的三部曲:构建请求参数、请求金刚位服务、构建金刚位DO返回结果。
这块是需要注意的地方,为什么要做接口的抽象?因为要做行为的管理,行为管理的意义分多个方面去看:一方面在于统一输入输出,将流程的代码逻辑放到实现类里,DomainService里的领域方法就可以比较清晰的表达出来;
另一方面接口抽象后就可以有不同的实现方式,这就和设计模式里的策略模式一样,GBF使用了bizId(业务身份,代表行业) scenario(场景id)作为策略的key,对应不同的行业就可以有不同的实现类,从而做到实现差异化的同时又能够统一输入和输出。尽管很多时候现存的差异化从长期来看没有存在的意义,作为短中期的实现这是比较理想的方案。
4、定义ability、实现action
@DomainAbility(name = "金刚位ability", domain = OperationDomain.class)
public interface IQuickLinkAbility {
@ActionExtensible(name = "构建金刚位请求参数")
QuickLinkReqParam buildQueryParam(BaseRequest request);
@ActionExtensible(name = "构建金刚位DO")
QuickLinkDO buildKQuickLinkDO(QuickLinkPO quickLinkPO);
}
@Component
public class DefaultQuickLinkAction implements IQuickLinkAbility {
@Override
@Action(name = "构建金刚位请求参数")
public QuickLinkReqParam buildQueryParam(BaseRequest request) {
return new QuickLinkReqParam(request);
}
@Override
@Action(name = "构建金刚位DO")
public QuickLinkDO buildQuickLinkDO(QuickLinkPO quickLinkPO) {
return Optional.ofNullable(quickLinkPO).map(QuickLinkPO::getItems)
.map(Collection::stream)
.map(itemStream -> itemStream.map(po -> QuickLinkDO.ItemDO.builder()
.title(po.getTitle())
.icon(po.getIcon())
.iconHeight(po.getIconHeight())
.iconWidth(po.getIconWidth())
.logInfo(po.getLogInfo())
.toolTips(po.getToolTips())
.schema(po.getSchema())
.build()).collect(Collectors.toList()))
.map(QuickLinkDO::new).orElse(null);
}
}
5、实现NodeService
"QuickLinkService") (name =
public class QuickLinkNodeService extends AbstractCommonNodeService<QuickLinkDO, BaseRequest>{
IQuickLinkDomainService quickLinkDomainService;
public QuickLinkDO doInvoke(BaseRequest request) {
return quickLinkDomainService.getQuickLink(request);
}
}
6、Process注册Node
//在ProcessConfig中声明Node
BaseNodeConfigBuilder quickLinkNode = new NodeConfigBuilder()
.preJoinPoint(new JoinPoint<PortalParam, BaseRequest>() {
public BaseRequest join(PortalParam request) {
return request;
}
}).nodeBeanName(BeanUtil.getBeanName(QuickLinkNodeService.class));
//processConfig注册Node
public ProcessConfig portalProcess() {
return new ProcessConfigBuilder()
.request(Request.class)
.response(CommonResponse.class)
.nodes(userNode)
.nodes(filterTabNode)
.nodes(feedNode)
.nodes(bannerNode)
.nodes(quickLinkNode)
.build();
}
public class ProcessController {
public Result<Response> portal( String biz, PortalParam param) {
NodeService<Response, Request> process = BeanUtil.getSpringBean(ProcessRegister.genericBeanName(PortalProcessConfig.BEAN_NAME));
param.setBizId(biz);
return process == null ? ProcessResult.fail("未找到流程") : process.invoke(param);
}
}
四、总结
基于领域建设的思想,使用GBF构建业务能力能够给US带来以下几点优势:
帮助us沉淀各领域的业务,标准化流程链路,明确业务逻辑; 需求拆解后能够有对应的领域收拢,各领域间无耦合,使问题更聚焦; 领域沉淀的能力方法能够很好的复用,不同场景间的功能节点基于领域能力去构建,构建好的能力可以在其他场景(附近页、会场、泛搜等)直接复用,提高开发效率; 进一步强化能力逻辑的表达,借助业脉系统可视化流程构建专业知识库,帮助开发同学快速、直观的了解业务,进而提高技术对业务的scense,有全局观的同时又能够方便把握细节,提高产品和技术、技术和技术之间的沟通效率; 可针对某个领域或功能节点进行稳定性建设,减少不同功能节点之间的依赖,提高整体稳定性;
业脉对业务流程的表达:
阿里云开发者社区,千万开发者的选择
阿里云开发者社区,百万精品技术内容、千节免费系统课程、丰富的体验场景、活跃的社群活动、行业专家分享交流,欢迎点击【阅读原文】加入我们。
微信扫码关注该文公众号作者