高德信息业务DDD实战 - 聊聊用领域重构胶水代码

一、背景

团队简介:高德信息业务us团队主要承接聚合下游服务(搜索、推荐、广告、离线、商服、营销等)面向端、车机等上游,持续、稳定的提供多行业、多品类、多场景的结构化数据服务。

520b88cfd53ecef18f887a34b15dec8d.png

目前高德信息业务门户有 美食、酒店、旅游、休娱、房产、商超、火客飞、周边游等行业或场景的portal页。由于每个业务的发展周期和节奏大不相同,每个细分行业下portal页的维护迭代不论是从产品、开发都是隔离的,自身团队的定位在没有严格统一的把控下比较容易产生胶水代码。这个现状对于信息业务的大目标-支撑好359行 来说是尤其困难的。

b7acce0218405f15eb8149bed478e715.png

这种隔离方式导致各业务之间存在信息茧房,也就是面条式的产品逻辑和代码,唯一的交集是代码库,各组开发同学在没有portal页顶层设计输入的情况下,难有业务全局观,不同portal页各自开发,相同模块在没有统一设计和管理的情况下面临两种问题:

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

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

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

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

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

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

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

二、目标

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

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

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

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

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

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

要达到这个目标,首先要找到业务持续演进的过程中,哪些是经常变的,哪些是相对稳定的。

506f12bd2fefea15e456f3f5c52577e5.jpeg

结合信息业务的实际情况,不难发现行业是一个可变的因素,但是场景相对来说比较固定,也就是说同样一个场景,需要同时支撑多个行业,每个行业在同一时间点,会有行业专属的发展状态、定制逻辑。

94a7ec60309b57f5d9c38462f391e909.png

当场景相对固定后,面对相同的场景,业务系统更期待的是相对稳定、统一的技术方案和链路。举个例子:portal的金刚位,业务站在金刚位自身去考虑,应该主要包括运营配置、算法推荐、广告植入这些重要功能,并且将这些特性和功能提供给每个产品或运营负责的行业阵地中去,当某个行业需要该组件新的特性时,可以统一考虑设计,从而可以将该功能延展到其他行业使用。同时当流程和链路统一后,协议也就很容易做到了通用和一致。

3efd41bb6fa889adfa352ff10958da76.png

多行业多形态的业务迭代下,产品的设计基于行业,能力的沉淀考虑不同行业间的共性和差异,进而做到不同场景的复用。业务创新、能力沉淀、场景复用可以形成有效紧密的闭环。

三、设计

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配置有干预逻辑

通过现状我们看到每个行业或场景的金刚位实现方案都是不一样的,不同的小组开发、不同的链路方案,意味着迭代维护成本居高不下,虽然目前的新方案在一个应用中统一对金刚位进行了收拢,但是只对输出进行了统一,底层的逻辑差异还在,且不同逻辑的实现无统一标准管理,不方便复用。

基于领域的系统建设,金刚位是一个具体的问题,适合放在一个地方解决实现。也正是基于领域建设这个抓手,将金刚位这个功能点统一组织、分析,拆解金刚位需要提供哪些能力,进而逻辑分类、链路归一,然后沉淀出金刚位统一的标准链路方案。

66ae5935993d545023cab95cf32ef2f3.jpeg

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

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的领域驱动设计理念去支撑业务开发,需要有哪些步骤

① 全局领域划分



4b4c1dd6a4ed5501ee319294ce212579.jpeg

目前在各个域内的逻辑相对复杂度不高,为避免过度设计,按照指导原则进行聚合设计出来8个领域,支持在后续领域复杂度提升支持进行域的拆分。领域建设的指导原则大致分下面几类:

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

用例分析中出现的一个或者多个对象,可以进入域模型的候选集。对应种类:业务实体、业务规则;

正例:运营规则描述了在商品、内容上的加工与干预逻辑,可以作为分域的参考实体。对照可参考淘系的限购域;

反例:搜索、推广对应的实体为doc,业务应用体现在商品、poi的id列表及标签。doc实体自身没有业务属性,属于系统内功能实现层的模型。引擎召回的标签属性归属于poi或者商品主体,故doc不能作为域模型的参考实体;

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

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

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

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

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

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

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

正例:商铺会挂接在poi上,在大部分业务场景下会同时出现。挂接关系属于引用关系的一种;

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

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

优惠和活动是否合为一个域?是。业务行为逻辑耦合度较高,例如:秒杀活动使用优惠券的方式支持。同时在信息工程视角,优惠与活动都属于营销中心;

② 统一领域建设

810bb3ecd4a4aa10c03f70fa114a14c2.png

团队以前抽象和复用的角度主要关注点在链路上,缺少领域的管理,容易导致当链路发生变化时,各领域间的调用和依赖关系发生调整,协议也容易发生变化(系统模块间解耦不够清晰),不同应用之间出现逻辑相似度高但无法复用的代码。拿poi卡这个例子看,当抽象管理的维度添加领域对象后,可以归纳出如上图的结构关系,借此可以明确各领域的数据结构表达和管理规范每个领域之间的行为。当poi的核心领域和业务领域明确后,标准业务能力层的Process可以直接引用,两者之间只剩下引用关系,不再有任何的复杂加工逻辑。Process对应着不同的场景,自此poi的领域模型和方法对标准能力和场景解耦。

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

gbf应用分层

f1eb2347373abca124f986aa7d905d41.png

gbf对象分层

c8bbe0e4a6e9357a46182a92f25691b4.png

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



6aacf79d034ab9d02fef586c958e883f.jpeg

⑤portal整体架构

5.1结构

1110e84a0b16a3b8c331eb1ee6cabe48.png

5.2 流程

d44710df83904b68196e0d0b85e5745b.png

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)作为策略的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);
    }
}

这一步的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,有全局观的同时又能够方便把握细节,提高产品和技术、技术和技术之间的沟通效率;

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

业脉对业务流程的表达:

88712548d736622e44bb59519f39bc88.png

5da32e61f0473e42805d3a19d45f3248.png

信息工程服务是结构化数据+策略的体现,在标准能力的基础上建设“主题场景能力”,不同场景、行业组成的各式各样的页面可以由进行抽象出具体的功能节点,例如 金刚位、banner、feed流、优惠、广告、营销位、评论等等,每个功能节点是不同领域之间能力的组合,同时在不同场景和行业又可以有不同的逻辑。gbf基于业务身份(行业)和场景作为路由的key,同一个接口不同实现类来支撑不同行业和场景的差异,然后在Process层对各业务功能节点进行组织编排,从而形成标准的业务能力。

关注「高德技术」,了解更多

推荐阅读

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值