DDD领域驱动设计内容分享(九):从高德portal重构,看DDD的巨大价值

目录

一、背景

二、目标

三、设计

0、什么是GBF

1、为什么要用GBF

2、使用GBF概念设计

① 全局领域划分

② 统一领域建设​编辑

③ 系统分层,标准输入输出

④ 接口行为统一,差异定制实现

⑤ portal整体架构

5.1结构

5.2 流程

3、使用GBF开发

四、总结


一、背景

图片

目前,高德信息业务门户涵盖了美食、酒店、旅游、休娱、房产、商超、火客飞、周边游等多个行业或场景。

由于业务发展周期和节奏的差异,每个细分行业的门户页维护和迭代都是独立的,导致出现所谓的“面条式”的产品逻辑和代码。

这种隔离现象在没有统一把控下,容易产生胶水代码,对于支持359行的目标构成了挑战。

这种隔离方式导致各业务之间存在信息茧房,也就是面条式的产品逻辑和代码,唯一的交集是代码库,

各组开发同学在没有portal顶层设计输入的情况下,没有业务全局观,不同portal页各自开发,相同模块在没有统一设计和管理的情况下面临两种问题:

  • 方案链路不一致,多种链路交替,各自维护

  • 存在重复逻辑,散落在不同应用中,代码各自为战,重复实现

在方案不统一、重复建设的情况下,us的portal面临着更长期的问题:

  • 系统设计的质量不高,故障率和风险加大

  • 开发维护成本大致随着portal页的数量线性增长

  • 无知识管理、团队之间协作困难,容易产生信息孤岛和理解不一致

  • 容易产生系统之间边界不清晰,加大维护成本

二、目标

通过统一的、规范的、合理的系统设计做到增强复用降本提效,提高稳定性

  • 对明确的固定场景,有统一的、稳定的实现方案

  • 提高设计质量和可维护性,降低故障率和风险

  • 减少重复工作,降低开发维护成本

  • 提高知识管理,促进团队协作,避免信息孤岛和误解

  • 使不同团队、系统之间的边界清晰,减少内耗

为了达到这个目标,我们需要分析业务演进过程中哪些是经常变化的,哪些是相对稳定的。

图片

结合信息业务的实际情况,行业是一个变化的因素,但场景相对固定。

这意味着,在同一时间点,同一场景需要支持多个行业,每个行业会有自身的发展状态和定制逻辑。

图片

当场景相对固定时,业务系统更希望采用稳定、统一的技术方案和链路。

例如,portal的关键位置,业务人员需要考虑的包括运营配置、算法推荐、广告植入等重要功能,并将这些特性和功能提供给各个行业阵地。

当某个行业需要新特性时,可以统一设计,以便延展到其他行业。

同时,统一流程和链路后,协议也易于实现通用和一致。

图片

在多行业多形态的业务迭代中,产品设计应基于行业,同时考虑不同行业间的共性和差异,以实现场景复用。

业务创新、能力沉淀和场景复用形成了一个有效且紧密的闭环。

三、设计

0、什么是GBF

高德商业框架(Gaode Business Framework),它是一种集成了业务身份与情境策略的综合性框架,由高德信息业务精心打造。其设计初衷是为了实现业务身份和场景策略的无缝对接。

GBF深受TMF(技术管理框架)的启发,并融合了领域驱动设计(Domain-Driven Design, DDD)的先进思想,致力于为产品业务和技术开发提供一个既全面又高效,同时具备跨行业、多场景适应力的轻量级解决方案。

GBF作为一种创新的业务框架,它不仅仅是一个技术工具,更是一种全新的商业思维。它通过将业务身份和场景策略有机地结合在一起,使得业务操作更加精准、高效。同时,GBF的轻量级设计使其能够灵活适应各种行业和场景,为企业的业务拓展和技术开发提供了强大的支持。在我国,GBF已经得到了广泛的应用,并取得了良好的效果,展现出了巨大的商业价值。

1、为什么要用GBF

GBF使用了领域的概念:它通过将复杂问题分解为多个独立的子问题,以便更加有效地组织、分析和解决这些问题。

在US的应用场景中,领域的划分具有以下几个关键作用:

  • 提高问题分析的精度:使用划定好的领域,需求可以迅速定位到特定的问题域,简化问题的复杂性,使得领域内的逻辑更加集中。

  • 促进知识共享和演进:使用领域构建系统后,可以将特定的领域交给专家进行维护,这有助于培养领域专家,构建和维护领域模型,促进技术产品和产品间的交流与合作,提升知识共享,从而有效地支持业务分析。

  • 提高复用性,降低开发成本:可以将功能相似的模块归为同一领域,使得不同的业务能力可以共享同一领域的逻辑,提高代码的复用性。不同的开发团队可以同时并行开发各自的领域,避免团队间的冲突和耦合问题,提高开发效率。

  • 提高稳定性:领域的划分使得系统可以分解为多个独立的部分,每个部分的功能更加自治,当发生问题时,不会对整个系统造成影响,从而提高整体的稳定性。

拿个具体的例子来看:各行业portal页的金刚位。

“金刚区"是banner下方的功能入口导航区域,通常以“图标+文字”的宫格导航的形式出现(例如淘宝、饿了么app)。之所以叫“金刚区”,是因为该区域会随着业务目标的改变,展示不同的功能图标,就像“变形金刚”一样可百变。

目前us的portal页金刚位不同场景、行业均有不同的实现方式,如果涉及到不同应用,相似逻辑容易有cv代码的情况

  • 景区、周边游之前配置在了diamond,后来切到了bff-other(一个提供轻量逻辑的服务,包含运营配置)

  • 休娱portal页取的merger-sp(一个引擎代理,屏蔽不同引擎间的差异)

  • 房产portal取的house-sp(另一个商品搜索引擎)

  • 其他场景例如附近页取的bast(一个推荐引擎),结合diamond配置有干预逻辑

从现状来看,每个行业或场景的金刚位实现方案都是独特的,不同的开发团队,不同的链路方案,这意味着迭代和维护的成本很高。

尽管目前的新方案在一个应用中统一了对金刚位的处理,但仅仅是对输出进行了统一,底层的逻辑差异仍然存在,且不同逻辑的实现缺乏统一的管理标准,不便于复用。

基于领域的系统建设,金刚位是一个具体的问题,适合放在一个地方解决实现。

也正是基于领域建设这个抓手,我们可以对金刚位这个功能点进行统一的组织、分析和拆解,明确金刚位需要提供的能力,进而实现逻辑的分类、链路的标准化,并沉淀出金刚位统一的标准链路解决方案。

领域划分在GBF中的应用,不仅能够提高问题解决的效率,还能有效降低开发成本,提升系统的稳定性。以金刚位为例,通过领域的划分,我们可以将金刚位的实现方案进行统一,避免了因为不同场景、行业导致的多种实现方式,从而降低了系统的复杂度和维护成本。此外,基于领域的系统建设也有助于推动知识共享和进步,提升团队间的协作效率。

 链路方案归一后,就可以统一输入和输出

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
      }
    ]
  }
}

至此,我们观察到US对金刚位的实现包括统一的链路、输入输出和逻辑管理,接近理想状态。

这种实现方式在复用性、开发成本和需求方案对接成本方面都有显著提升,稳定性建设也可以针对某一具体领域或功能点开展。

2、使用GBF概念设计

接下来,我们来看一下基于GBF的领域驱动设计理念来支持业务开发需要哪些步骤。

① 全局领域划分

图片

目前,各个域内的逻辑复杂度不高,为了避免过度设计,我们根据指导原则进行了聚合设计,共划分了8个领域。

这些领域可以支持后续领域复杂度的提升,同时也可以进行域的拆分。

领域建设的指导原则主要包括以下几类:

1. 用例分析中出现的业务实体、业务规则可以进入域模型的候选集;

用例分析中出现的一个或者多个对象,可以进入域模型的候选集。

对应种类:业务实体、业务规则;

正例:

运营规则描述了在商品、内容上的加工与干预逻辑,可以作为分域的参考实体。

对照可参考淘系的限购域;

反例:

搜索、推广对应的实体为doc,业务应用体现在商品、poi的id列表及标签。

doc实体自身没有业务属性,属于系统内功能实现层的模型。

引擎召回的标签属性归属于poi或者商品主体,故doc不能作为域模型的参考实体;

2. 领域建立的前提条件是明确稳定的实体与稳定的行为;

反例:面向场景、行业设计领域,由于场景、行业的不可枚举(发散、不收敛),会导致域的不稳定。

比如酒店域、旅游域;

3. 如果多个实体间是归属关系,需要合并成同一个域;

归属关系定义:在没有父实体存在的前提下,子实体无明确业务语义。

正例:营销时有活动规则、优惠规则,运营时有干预规则。

脱离营销活动、运营的限定下,规则自身没有明确的业务语义。规则需要分别归属至对应的父实体;

4. 如果多个实体间为引用关系则各自成域;

这里的引用关系定义为:多个实体同时在多个用例中出现,任意实体的业务语义不依赖于其他实体的补充。

正例:商铺会挂接在poi上,在大部分业务场景下会同时出现。

挂接关系属于引用关系的一种;

反例:参见归属关系正例;

5. 分域时需要参考组织结构关系;

优惠和活动是否合为一个域?答案是肯定的。

因为它们之间的业务行为逻辑耦合度较高,比如秒杀活动使用优惠券等方式进行支持。

同时在信息工程视角,优惠与活动都属于营销中心;

② 统一领域建设

过去,团队在软件开发中主要集中在链路的抽象和复用上,而忽视了对领域管理的重视。

这种做法可能会在链路发生变化时导致各领域之间的调用和依赖关系需要调整,同时也会引起协议的变化(系统模块间的解耦不够清晰),进而造成不同应用之间出现逻辑上相似但无法复用的代码。

以POI卡为例,当我们开始使用抽象管理的方式来添加领域对象时,我们可以梳理出如上图所示的结构关系。

通过这种关系,我们可以清晰地定义各领域的数据结构和管理规范,以及领域间的交互行为。

当POI的核心领域和业务领域被明确划分后,标准业务能力层的Process可以直接被引用,两者之间仅剩下了引用关系,而无需复杂的加工逻辑。这样,POI的领域模型和方法就与标准能力和场景解耦了。

③ 系统分层,标准输入输出

gbf应用分层

 gbf对象分层

④ 接口行为统一,差异定制实现

⑤ portal整体架构
5.1结构

5.2 流程

3、使用GBF开发

使用GBF实现一个简单逻辑,以us的金刚位调用querysug下游为例

1、在repository包里定义fetcher的输入reqParam、输出 PO,在repo impl中创建声明式fetcher

@FetcherClient(name = "sug-fetcher")
public interface SugFetcher {
    @FetcherMethod(name = "quicklink")
    QuerySugResult<QuickLinkPO> getQuickLink(@RequestParam QuickLinkReqParam reqParam);
}

对接口服务的调用使用声明的方式编写代码,能够直观表达接口参数和协议的同时还可以借助业脉平台方便的批量导出文档。

2、在repository中定义repo接口,在impl中定义实现

public interface QuickLinkRepo {
    @Annotation(title = "获取金刚位")
    QuickLinkPO getQuickLink(QuickLinkReqParam reqParam);
}
@Service
public class QuickLinkRepoImpl implements QuickLinkRepo {
    @Resource
    SugFetcher sugFetcher;
    @Override
    public QuickLinkPO getQuickLink(QuickLinkReqParam reqParam) {
        return Optional.ofNullable(sugFetcher.getQuickLink(reqParam))
            .map(QuerySugResult::getData)
            .orElse(null);
    }
}

多这一层的目的是为了简单处理fetcher返回的结果,例如poi的merge逻辑、特殊情况下对fetcher实现缓存等操作

3、定义金刚位的DO和入参(这里入参没有特殊参数直接使用了基类),定义金刚位的DomainService接口,和对应的实现类

@DomainService(name = "运营领域金刚位服务", domain = OperationDomain.class)
public interface IQuickLinkDomainService {
    QuickLinkDO getQuickLink(BaseRequest baseRequest);
}
@Service
public class QuickLinkDomainService implements IQuickLinkDomainService {
    @Resource
    QuickLinkRepo QuickLinkRepo;
    @Resource
    IQuickLinkAbility quickLinkAbility;
    @Override
    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)作为策略的键,这样针对不同的行业就可以有不同的实现类,从而实现差异化的同时保持输入输出的统一。

尽管在某些情况下,这种差异化可能从长远来看并不必要,但作为短期到中期的解决方案,它是一个理想的选择。

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);
    }
}

这一步的ability定义和上一步的domain方法是紧密相关的,ability定义后一般会有一个默认实现,如果不同场景和行业有差异(例如hotel)就需要在us-app-hotel这个工程里写对应的Action实现ability。

GBF底层是通过包名的方式(com.amap.xxx.app.hotel.action)结合application.yaml的配置(biz-id映射的package)去路由到对应的Action实现,场景这块则是根据@Scenario注解来作区分,这也是GBF基于业务身份场景做策略的实现方式。

5、实现NodeService

@ProcessNode(name = "QuickLinkService")
public class QuickLinkNodeService extends AbstractCommonNodeService<QuickLinkDO, BaseRequest>{
    @Autowired
    IQuickLinkDomainService quickLinkDomainService;
    @Override
    public QuickLinkDO doInvoke(BaseRequest request) {
        return quickLinkDomainService.getQuickLink(request);
    }
}

Node的定位可参照GBF的相关资料,这里逻辑比较简单,也会存在逻辑复杂的场景,例如走merger链路的feed流,走营销获取活动信息盘货信息再走merger找回的运营位。

6、Process注册Node

//在ProcessConfig中声明Node
BaseNodeConfigBuilder quickLinkNode = new NodeConfigBuilder()
    .preJoinPoint(new JoinPoint<PortalParam, BaseRequest>() {
        @Override
        public BaseRequest join(PortalParam request) {
            return request;
        }
    }).nodeBeanName(BeanUtil.getBeanName(QuickLinkNodeService.class));
//processConfig注册Node
@Bean
public ProcessConfig portalProcess() {
    return new ProcessConfigBuilder()
        .request(Request.class)
        .response(CommonResponse.class)
        .nodes(userNode)
        .nodes(filterTabNode)
        .nodes(feedNode)
        .nodes(bannerNode)
        .nodes(quickLinkNode)
        .build();
}

7、接口注册

@RestController
@RequestMapping("/process")
public class ProcessController {
    @GetMapping("/portal/{biz}")
    @ResponseBody
    public Result<Response> portal(@PathVariable 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实现一个简单完整的流程就完成了。

四、总结

基于领域建设的思想,使用GBF构建业务能力能够给US带来以下几点优势:

  • 帮助us沉淀各领域的业务,标准化流程链路,明确业务逻辑

  • 需求拆解后能够有对应的领域收拢,各领域间无耦合,使问题更聚焦

  • 领域沉淀的能力方法能够很好的复用,不同场景间的功能节点基于领域能力去构建,构建好的能力可以在其他场景(附近页、会场、泛搜等)直接复用,提高开发效率

  • 进一步强化能力逻辑的表达,借助业脉系统可视化流程构建专业知识库,帮助开发同学快速、直观的了解业务,进而提高技术对业务的scense,有全局观的同时又能够方便把握细节,提高产品和技术、技术和技术之间的沟通效率

  • 可针对某个领域或功能节点进行稳定性建设,减少不同功能节点之间的依赖,提高整体稳定性

业脉对业务流程的表达

图片

图片

信息工程服务是结构化数据+策略的体现在标准能力的基础上,我们构建了“主题场景能力”。

不同场景、行业构成的多样化页面可以通过抽象出具体的功能节点来支持,例如金刚位、banner、feed流、优惠、广告、营销位、评论等。

每个功能节点都是不同领域能力组合的结果,同时在不同场景和行业中可以展现出不同的逻辑。

GBF基于业务身份(行业)和场景作为路由的键,通过不同的实现类来支持不同行业和场景的差异。

在Process层,我们可以对各个业务功能节点进行组织和编排,从而形成标准的业务能力。

 

  • 13
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

之乎者也·

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值