浅谈团队代码风格/结构一致性

原创
05/14 17:34
阅读数 9.8K
图片



本文探讨了团队在代码风格与结构一致性上的长期实践与思考,分享了如何通过统一的代码规范和框架设计,提升代码可读性、可维护性,并降低团队协作与新人上手的成本。文章从问题起源、解决方案、实际收益到具体落地工具进行了全面阐述。

图片

前言


某次做月度总结的时候,回顾卓越工程的板块,有点讶异团队推进代码风格与结构一致性这件事情已经历时2.5年。过程中的发心、各种纠结、到今天的收益,感觉可以写出来作为一个分享,希望能够给有类似想法的同学一点参考。


整个事情的命题其实是很基础的:程序员(Java服务端)如何把代码写的好看,好维护。其中包括一些从一个团队角度来看的一些思考和实践。


图片
缘起

我所在的团队,一部分职责是开发维护直播业务中主播直播售卖商品的时候的CPS佣金体系,完成消费者点击商品的行为跟踪,以及下单后把订单归因到主播,以及后续的计费、扣佣过程。其中要结合平台商业策略和不同参与者之间的商业关系做跟踪、计佣,整个体系非常复杂,一个请求的处理过程涉及到相当长的过程。而其中一句话不慎写错就是很多钱的资损风险。怎么做到一个复杂的应用,让同学能够更容易理解把握,减少出错,这是从具体场景出发的初心,触发我们思考整体的代码结构。

很多年前的时候,MVC框架在流行,那个时候有一种风靡天下的代码分层结构模式:Controller->Service->DAO。这是最基础的一种代码分层结构。Controller负责Web相关的输入输出,DAO负责数据存储查询,中间的Service负责业务逻辑。


随着互联网中间件,以及前后端分离等技术发展这里面出现一些变化,Controller层演化为入口服务,可能是RPC调用、Web请求、消息处理、定时任务处理等等。DAO的旁边出现对其他分布式服务的RPC调用(RPC Consumer、KV存储、发消息等等)。这两层的代码逻辑相对来说都是简单的,其框架结构,基本由中间件或者基础框架所规范。而整个体系中最复杂的业务逻辑层(也就是原来的Service层),是没有代码结构规范的。



简单的业务处理过程中,可能只是查询、读取一下存储,当业务变得足够复杂之后,在业务处理层经常会见到很多不同的代码模式:


  • 最简单的

  • 一个服务方法,内部通过换行、分块、拆出private方法等方式,让代码容易理解

  • 为了并行缩短RT,会用线程池并行调用多个逻辑过程


  • 复杂一点

  •  会出现责任链的模式,把业务处理过程的不同逻辑分片,封装为链式调用的节点

  • 并行场景下,上述责任链模式会优化成为一个并行执行的过程,中间可能要考虑节点之间的依赖


  • 在一个大的主体业务流程中,面对不同业务有少量的差异性,如果配置不易表达,那么就会出现基于业务类型标识的策略模式,很容易出现很多的策略模式代码满天飞


上述这些点,是单个服务或者说单个研发维度看到的事实。从一个团队或者业务域来看,就会看到更多的情况:

  • 在同一个应用中,面对复杂度相似的场景,在没有规范的情况下,很容易出现多种写法
  • 团队n个应用,同学们能做到在应用里面见树木已经不容易,看通到森林就很难了,代码模式各不相同,应用维度的轮子很常见
  • 新人进入,或者人员跨领域的时候,没有可以继承的宏观代码理解模式,就是从细节从头看

上面的这些情况,触发我们思考,是不是可以在整个团队的范围内,去做业务逻辑层的代码规范,做到【书同文】。

图片

解题


作为电商相关的研发,曾经看过淘宝交易交易四大金刚等等应用的代码。可以看到,这些应用的基本代码模式,都是入口服务->bpm流程&活动->领域服务->能力->扩展点。不会有人在buy2里面再自己搞一个责任链,也不会有人在tp3的主流程里面面对某某业务特殊逻辑写一个策略模式的代码。


珠玉在前,所以去解决前面提到的问题,很好的思路就是在应用中以某个框架形式的代码约束,统一代码分层风格,并逐步推广到域内的多个应用,实现规模化的效果收益。


本质上讲,我们是在应对软件编码的复杂性,而应对复杂性的最佳手段就是抽象。所谓约束、风格,都是对复杂逻辑和实现的统一抽象,从而降低人脑认知的成本。


如何落地,按照程序员的习惯,还是造一个轮子吧。毕竟我们的场景,不论从单个业务复杂度,还是业务支撑的类型规模方面相比于业务平台,都是小小弟,所以自己造轮子的唯一目标就是要简单可落地。


我们把代码规范为如下层次(紫色部分是可选的):



  • 入口服务:一个应用提供多个入口服务,做一个完整的业务动作的调用入口;可以是HSF入口,或者Controller调用的服务,或者Mtop接口调用的服务;入口服务的基本职责是参数校验、系统内外对象转换等,核心的业务逻辑应该委托到下面的业务流程-活动,简单的场景就不用流程,而是直接调用领域服务

  • 业务流程与活动:

  • 一个入口服务背后可以对应一个流程定义,实际上是一系列Activity的集合,一般情况下就是顺序一个一个的Activity调过去,Activity可以返回处理失败阻断流程;当然也可以做复杂的编排

  • 一个业务流程里面的不同环节就是业务活动,业务活动可以依赖多个领域服务完成相关逻辑;业务活动一般是各个领域服务调用的胶水代码,一个领域的输出,在活动中可能作为下一个领域调用的输入,从而尽可能保障领域服务之间不发生依赖

  •  领域&领域服务:一个实体对象的完整的管理能力集合叫做一个领域;领域服务就是一个领域对外暴露的服务方法,领域服务和领域对象是一个系统核心要沉淀的可复用资产。

  • 领域服务的扩展模板和扩展点:一个领域服务可以通过泛型指定扩展模板,也就是这个领域服务所允许的扩展点的集合;一个领域服务在某些业务逻辑处理过程中,针对不同业务可能有不同逻辑,那么可以根据业务身份查找当前业务对应的扩展点实现;这就是对不同逻辑点上随处可见的策略模式的规范


规范后的代码分层结构如下图所示:



在建立上述规范的基础上,尽量统一业务层代码模式。同时,因为这个结构的存在,实际上可以去基于结构做代码结构可视化、监控等等的事情。


具体的代码示例可以看后面的【轮子】一节。这里看两个效果图:

团队同学做了Idea的插件,做到流程视图、入口服务、活动之间的导航,进一步提升阅读、编码的效率。截个图:



代码结构可视化,可以辅助应用代码的分析、理解、防腐治理。下面是一个截图,可以看到某个入口服务内部的基本处理过程,以及背后依赖的领域和领域服务。



图片
结果和收益

轮子是2022.06开始造的,作为技术演进的项目,推进方式上主要是两个点:新的服务、应用直接采用规范的代码模式,老的应用和服务,随着业务需求的演进和人力情况逐步迭代。整体推进其实是比较缓慢的的一个过程。到目前大约经历2.5年,代码方面的显性结果如下:

  • 12个服务端应用

  • 600+个入口服务、400+个流程,180+个领域,1k+个领域服务


在这些数字的基础之上,收益可以这么讲:

代码结构基础线的拉升:一个同学,即使是工程编码经历比较少的新同学,有规范可循的情况下,写出来的代码在基本结构上是有下限的;同时对于老鸟程序员,也有对代码结构思考的约束

代码可读性提升:实际上每一个服务,在代码里面找到流程文件一看,就基本知道大概流程,做了哪些事情。再结合可视化的代码架构视图或者IDE插件,就更容易看明白了

趋同的代码模式带来的收益:一个同学从A领域进入B领域,代码模式是一样的,从看到写,都有满满的熟悉感,降低学习成本


当然,任何轮子、规范,都要有人来使用和遵循,人是最本质的要素。


图片
轮子


代码框架是团队代码规范约束的一个载体。我认为在整个过程中最重要的是关于规范的讨论、确认、实施过程,轮子反而是其次。实际真正落到代码框架层面是比较简单的,代码也不多。


这里简单摘取一点代码来看一下:

/*** 这是一个HSF接口Provider实现,示例代码中省略了典型的try-catch结构*/@ServiceEntrance(name="归因计费服务")@Overridepublic Result<RatingDTO> rating(RatingRequest request) {    validateRatingParam(request);
    //构建流程上下文并启动处理流程    RatingProcessContext context = new RatingProcessContext();    context.setRatingRequest(request);
    ProcessResult processResult =  processEngine.start(ProcessNS.PROCESS_RATING, context);
    if(processResult.isSuccess()) {        return Result.success(buildRatingResultDTO(context));    }else{        return Result.fail(processResult.getErrorCode(), processResult.getErrorMessage());    }}
<?xml version="1.0" encoding="UTF-8"?><onion-process-config>  <!--      支持基本的逻辑编排:顺序、并行、条件、跳转、循环、分组、子流程      实际上用的最多的是最基本的顺序编排  -->  <process id="rating_process" name="内容计佣处理流程">      <node-list>          <!--归因计佣相关的activity-->          <activity name="初始化数据" bean="ratingInitActivity"/>          <activity name="在线归因" bean="ratingAttributeActivity"/>          <activity name="归因后初始化数据" bean="ratingAttributeAfterInitActivity"/>          <activity name="黑名单商品校验" bean="ratingBlackCheckActivity"/>          <activity name="按费用项计佣" bean="ratingRatioActivity"/>          <activity name="同步账房处理" bean="ratingAutoPayCenterActivity"/>          <activity name="初始化达人外部引流" bean="ratingInitOuterTrackRuleActivity"/>          <activity name="按费用项构建分佣关系" bean="ratingCommissionListActivity"/>          <activity name="按费用项构建加码分佣关系" bean="ratingExtraCommissionListActivity"/>          <activity name="扣佣结果组装" bean="ratingResultBuildActivity"/>      </node-list>  </process>
  <!-- 这里再放一个并行流程的示例 -->  <process id="itemCompleteParallelProcess" name="商品补标流程" parallel="true"           executor="itemCompleteExecutor" timeout="500">      <node-list>          <activity name="商品补标活动A" bean="itemCompleteActivityA"/>          <activity name="商品补标活动B" bean="itemCompleteActivityB"/>          <activity name="商品补标活动C" bean="itemCompleteActivityC"/>          <activity name="商品补标活动D" bean="itemCompleteActivityD"/>          <activity name="商品补标活动E" bean="itemCompleteActivityE"/>          <activity name="商品补标活动F" bean="itemCompleteActivityF">              <dependency-list>                  <!-- 整体各个活动之间并行执行,但是F要依赖A执行完成 -->                  <dependency ref="itemCompleteActivityA"/>              </dependency-list>          </activity>      </node-list>  </process></onion-process-config>
/** * 归因活动 * @author jiangxin.zcg * @date 2022/10/25 */@Componentpublic class RatingAttributeToActivity implements ProcessActivity<RatingProcessContext> {
    @Autowired    private RatingDomainService ratingDomainService;
    @Override    public ActivityResult execute(RatingProcessContext context) {        AttributeToRequest request = new AttributeToRequest();        request.setRatingRequest(context.getRatingRequest());
        //调用领域服务完成归因过程        AttributeResultDTO attributeResultDTO = ratingDomainService.attributeTo(request);        context.setAttributeToTrack(attributeResultDTO.getTrackDTO());
        //后面计费的业务实例在这里构建,根据归因结果确定了业务身份        TcpRatingInstance ratingInstance = new TcpRatingInstance();        ratingInstance.setBizInstanceId(BizInstanceId.of(context.getAttributeToTrack().getBizcode(),                context.getRatingRequest().getBizOrderId()));        ratingInstance.setRatingRequest(context.getRatingRequest());
        context.setRatingInstance(ratingInstance);
        //这里实际上因为有了归因计费的业务实例,也有了业务身份,也就是可以调用ratingDomainService的其他方法,做一些这个业务特有的扩展了
        return ActivityResult.SUCCESS;    }
}
@Domain(id = DomainNS.DOMAIN_TCP_RATING, name = "归因计费域")public class RatingDomainService implements IDomainService<TcpRatingExtTemplate> {
    /**     * 归因领域服务     */    @DomainService(name="归因服务")    public AttributeResultDTO attributeTo(AttributeToRequest request){        //找到lastClick        List<TcpTrackDTO> trackList = request.getRatingRequest().getTrackList();        TcpTrackDTO lastClick = trackList.get(trackList.size() - 1);
        AttributeResultDTO result = new AttributeResultDTO();        result.setBizcode(lastClick.getBizcode());        result.setTrackDTO(lastClick);
        return result;    }
    /**     * 计费领域服务     */    @DomainService(name="计费服务")    public List<RatingDetailDTO> ratioCalculate(RatioCalculateRequest request){        List<RatingDetailDTO> resultList = Lists.newLinkedList();        BizTypeRatingOutput bizTypeRatingOutput;        for (RatioCalculateInstance instance : request.getRatingInstance().getRatioCalculateInstanceList()) {            BizTypeRatingInput input = new BizTypeRatingInput();            input.setInstance(instance);            //不同的bizType调用扩展点做计费            bizTypeRatingOutput = this.invokeFirstMatched(instance,                    TcpRatingExtTemplate.EXT_TCP_BIZTYPE_RATING,                    ext -> ext.bizTypeRating(input));
            if(bizTypeRatingOutput != null){                resultList.addAll(bizTypeRatingOutput.getRatingDetailList());            }        }
        return resultList;    }}


上面的示例中可以看到顺序执行流程、节点并行执行流程。实际业务过程中能够覆盖绝大多数场景。但是依然会有少量更复杂的场景,需要更加复杂的处理,所以增加了下面的一些能力:

  • 子流程sub-process,类似一个private方法引用一个流程定义

  • group 是一个组,实际上是一个内联的子流程

  • process/group都支持composer属性,设定一个bean,完成自定义所有节点的执行编排,比如某个流程需要try执行前4个节点,finally执行节点5;可以结合节点的tags属性,做一些公共的composer


图片
后续的一些想法

过程中实际上会有不少的点是大家会困惑的,例如:

  • 究竟什么是领域,领域应该如何划分,领域服务的代码如何分治

  • 一些代码究竟应该写在流程-活动这一层,还是领域服务层

  • 流程中的子流程、跳转等结构越来越复杂,流程编排与直接Java编码之间的表意权衡,不能生搬硬套反而导致代码更难以理解

  • 复杂流程的Context对象本身也会存放很多数据,结构上如何做的更加清晰


当然没有银弹的解决方案,但是大家至少有一个认知是对齐的,就是我们的基本目标,是让代码写的容易理解、容易维护。每一次讨论、分歧、没那么完美的达成一致,都是价值。


后续有一些事情还可以探索,例如在规范后的各个层面的代码维度上的监控、告警,辅助问题排查和定位;以及基于当前规范约束的AI代码生成,尤其是作为规范结果的代码结构信息,都可以作为模型的知识输入。希望可以持续推进探索。


图片
团队介绍

本文作者江新,来自淘天集团-直播商业化团队。本团队致力于在电商直播业务中为主播提供有竞争力的商品,同时支撑主播佣金、坑位费商单、平台激励等各种商业模式的发展和创新。作为大团队内和资金最紧密的板块,在稳定性和资损防控方面也更加需要体系化的建设和保障。我们不断地探索和实践新的技术,让平台和主播更加高效的经营,从而打造淘宝直播的生态竞争力。






本文分享自微信公众号 - 大淘宝技术(AlibabaMTT)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

展开阅读全文
加载中
点击加入讨论🔥(4) 发布并加入讨论🔥
4 评论
10 收藏
1
分享
返回顶部
顶部