大家好,我是Jensen。

前段时间,产研VP任命我的领导带队,组建了一个虚拟架构团队——架构委员会,发起公司级别的基础架构规整,在接下来的时间里,领导带领我们几位委员会成员,对公司的技术架构重新梳理了一遍,不断打磨出一套完善的基础架构。

距今已过去了两个多月,我们回顾过去,做一次阶段性总结。

在业务型的公司要怎么推动技术架构往前发展呢?看看我们是怎么做的,希望大家能够打开思路。

业务型公司如何推动技术架构演进?_spring

一、组建一支高效的架构团队

很多时候我们在团队内对新规则/新秩序难以推动,根本原因在于上层领导不予重视,想想,假设领导都不重视,怎么可能让所有下属都重视呢?

想要推动一件事,除了自身的本领过硬,很多时候还得靠领导的影响力加持。

这需要我们向领导提供价值点,向领导植入“现阶段做这件事是非常有必要的”这个想法,有了上层领导与各个部门技术总监的大力支持,事儿就成了一半。

虽说架构调整对业务开发来说是成本,短期内看不到收益,但从长远的发展来看,为了防止代码进一步腐化,新项目能够顺利搭建,建立一定的框架规则与秩序已是迫在眉睫。

架构团队的组建也是一门学问,一千个观众眼中有一千个哈姆雷特,如果团队人员太多就很难得出定论,公说公有理,婆说婆有理,到最后只能投票决定,但每个人的认知极限不一,很多核心问题其实不能用投票来进行。

再者,技术与业务两者密不可分,想要更好地服务于整个技术团队,在架构调整后快速落地,架构委员会还必须要有各个业务线的开发代表,让听见炮火的人参与决策

所以参与的人多了反而不行,人少才能快速决策,快速做出当下最有利的决定,后续再迭代优化

二、明确设计理念

做任何事情都得先有思路,做架构也一样,那我们的架构思路与设计初心是怎样的呢?

本着聚焦、稳定、链接、赋能、需求驱动的原则,以提高人效(技术栈统一、公共模块抽象)、前置性技术储备、打造学习型团队(技术氛围、培训分享)为目标,梳理并完善架构的范围、职责、规范、流程。

在当下,我们做架构的整体的思路就是:抓核心、练框架、筑流程

  • 抓核心

2021年,公司业务系统全部从python转移到java,也从单体走向了微服务,在过去的一年中,业务需求强劲,先后诞生了30多个微服务,接下来还会有更多。

在这样的发展背景下,各路“共性”的需求开始涌现,例如审计字段的自动插入、token过滤器,以及加解密工具类......数不胜数,而这些需求统统都纳入到了common包下(后来也尝试拆分了pub)。

如此操作下来,我们不难闻到了一丝“臭味道”——功能越多,其中的技术债务也像滚雪球那样,越滚越大,这里滋生出两个问题:

  1. 定位的失误。已经与“核心”、“基础”的定位相去甚远,更多的功能只是在减分,而不是加分。
  2. 角度的偏差。维护者没有挡住那些减分的需求,一昧地满足,虽然解决了问题,但那仅是开发者的视角出发,而不是架构的视角。

因此,我们需要对原有的common包重新审视一下:

  1. 抓住基础框架的定位,雕琢核心,抛弃冗余。
  2. 追溯问题的本质,辩证分析需求场景,去伪存真。
  • 练框架

原有的common包虽然已经具备了基础框架的味道,功能也非常“丰富”,满足项目的日常开发是没啥大问题的,但为什么没有得到合理的发展呢?

这个问题非常简单,一句话总结:地基打得不够扎实

我们看到,原common包是从通用的类开始着手封装的,一开始就是为了解决共用相同文件的问题,从本质上讲,即需要一个“公共的空间”或者一个jar包。

随着需求日益增加,这个公共空间会面临至少两个问题:

  1. 收纳困难。在同一个空间中容纳所有事物,这是非常困难的,也是不现实的,这依然是在警惕我们做事要有的放矢。
  2. 缺少精气神。即使我们为每个基础组件规划好了各自的空间,但这只是视觉上的满足,作为一个框架,应该有它内在的“精神力量”。

因此,我们不仅需要合理、清晰地规划出各个基础组件的职责,还需要凝练出一份项目编程实践的模式,而这种模式就是一种内在的、将各个组件串联起来的精神力量。

  • 筑流程

通过以上“抓核心”、“练框架”的行动,便有了我们新的基础框架ygp-base。

反观原common包的问题,我们依然能够从“失败”中得到更加宝贵的经验:

  1. 持之以恒的坚持。纵使完成了框架的开发,后期依然需要精心维护,免得后期又长“歪”了。
  2. 坚持做正确的事情。每个人对架构设计的理解和运用也是有差异的,完善的流程能够规避人为的失误,我们需要通过规范的流程管控“需求”,而不是根据个人的喜好来“创造”需求。

所谓的流程,无非是要达成以下几个基本的要求:

  1. 甄别有效需求场景
  2. 对技术点做好尽责调研
  3. 合理的封装实现

技术日新月异,长江后浪推前浪,构建合理的流程机制,才能让我们以不变应万变的姿势,迎接未来新的挑战。

三、沉淀架构原则

OK,整体思路有了,那我们做架构的原则有哪些呢?

我们总结以下6个原则:

原则1:充分讨论,达成共识

当我们进入一个新的领域或阶段之后,每个人对新事物的认知往往是不同的,彼此的观点有时候甚至是相互冲突的。

一个团队要共同完成一项框架/组件的架构工作,就需要展开讨论,充分阐述不同方案的优劣,通过利用思维导图、在线文档等工具,更高效地完成观点的整合。

这不仅仅是沟通的问题,也是架构的艺术,为了有更好的实现方案,我们需要博取众长共同完善思路,人人可架构,共同促进形成一种良性循环的工作氛围。

团队合作中也会有意见相悖的情况,如果只停留在口头上的辩论,有时候还是不够客观严谨,在面对每一次问题讨论,我们都需要进行一番“摸门”的工作,如:行业案例/场景分析/对比分析,原理/源码/示例,做好充分的调研

原则2:兼容并包,开拓思路

尊重历史的情况,但不能拘泥于历史思维。

框架层面的开发工作,除了技术的考量,更需要兼容业务的使用场景,只有在充分调研业务中存在的种种问题之后,我们才能认清形势,更加坚定地、理性的进行技术革新。

对于老项目的实施来说:

在新基础框架的迁移实施讨论过程中,如果为了降低历史项目的改动成本,而持续依赖原有的代码,虽然兼顾了历史,但实际上并不利于整体新框架的迁移工作,导致后续工作处于十分被动的境地。

而我们最终清楚的看到这些依赖仅仅是“纸老虎”,通过逐层排查以,利用IDEA工具便利的重构功能,我们依然能够做到降低旧代码的依赖,并且用较低的成本实现迁移。

对于新项目的实施来说:

考虑到老项目的改造成本,许多地方都只是在可控的范围内进行调整,而新项目的实施思路,就可以脱离老项目的影响了,既能规避历史的问题,也能把整体架构往上一个层次拔高。

原则3:多架构模式混合实践

通过对经典架构的借鉴,我们利用分层架构、微核架构思维,对common包进行了拆解,重新定义全局依赖,梳理核心契约、全局上下文,划分数据访问层、web应用层、业务网关层,支持cache、mq、log、oss等组件扩展。

原则4:践行GRASP和SOLID

在拆分common包的时候,我们利用GRASP(通用职责分配软件模式)和SOLID(单一职责、开闭、里氏替换、依赖倒置、接口隔离和迪米特)这两套设计原则,顺利的完成拆分工作。

原则5:站在巨人肩膀上,向优秀框架学习

  • 借鉴SpringCloud,引入bom机制

BOM(Bill of Materials)是由Maven提供的功能,它通过定义一整套相互兼容的jar包版本集合,使用时只需要依赖该BOM文件,即可放心的使用需要的依赖jar包,且无需再指定版本号。BOM的维护方负责版本升级,并保证BOM中定义的jar包版本之间的兼容性。

这样的问题十分明显,即这个parent在不断地变更。

其实大可不必如此,通过参照SpringCloud的方式,我们可以定义一个parent,定义子项目必须遵守的一些约定,如Java版本,编码,公共第三方依赖的版本,测试规范,maven版本检查,打包规范,发布仓库声明等,然后再定义一个bom(springcloud项目习惯用dependencies),定义自研的一整套基础包jar包版本集合。

需要注意的是:parent只有一个,但dependencies可以多个,相当于parent的扩展。

  • 借鉴SpringBoot,引入starter机制

在过去,我们使用包扫描的机制,引入common包的组件,这种方式不仅使用麻烦,而且很不灵活,甚至需要额外维护一堆繁琐的配置。

在开发新的基础框架之际,我们引入了springboot的starter机制,通过灵活的注解,我们让基础包的使用更加的灵活,例如通过@ConditionalOnMissingBean,基础框架为业务模块提供了默认的组件,如果这些组件不满足需求,业务方可以在src下直接扩展一个新的组件并注入到spring容器中,基础框架一当发现业务方自定义了同样的组件,默认组件就不会生效了,这种机制的运用,也可以大大减少配置量。

  • 借鉴Spring命名风格
  1. Starter自动装载类:类名用“AutoConfiguration”作为后缀,例如“YgpWebAutoConfiguration”。
  2. 上下文持有者:使用"ContextHolder"作为后缀,例如“UserContextHolder”,代替ThreadLocalUtils之类的命名。

向优秀的框架学习,是我们一向的宗旨。

原则6:最小使用成本

  • 提供了大量starter

通过引入starter包,可以简化业务工程的配置性工作,整体上也减少了重复性工作。

  • 约定大于配置

基础包中默认注入容器的Bean对象,都添加了@ConditionalOnMissingBean特性作为约定的“标配”;这些组件能够满足通用的需求场景,也方便对特殊业务重写替代;而相对于旧的common包,我们移除了nacos的配置控制项,大大简化了配置。

  • 见名知意

避免少见的单词术语(例如tenet),避免有歧义的缩写(例如pub)。

  • example示例

基础框架应该提供使用示例,方便新人上手;参考base-example项目,里面提供了完整的调用例子;对于老项目,我们也提供了迁移手册,按步骤操作,基本能完成common包的迁移。

以上这些核心的设计原则也是在做的过程中一点点优化总结而来,毕竟动工之前是没法考虑周全的,不要想着一步到位。

四、日常讨论与框架代码优化

虽说做技术架构没有产品提需求,也没有项目经理监督,但我们还是以做项目的姿态来进行——每天组织站立会,隔几天一起约在会议室,把问题抛出来,把进度对齐。

各个业务线都有各自的业务特点,我们只是把共有的业务特点汇总起来,把公共的技术需求抽象出来,定制的需求各自去把控,技术架构不是万能粘合剂,合适的才是最好的。

对于既有框架代码的保留原则,不断反问自己三个问题:

  1. 这个类或方法的作用是什么?
  2. 它的作用范围/作用域是什么?
  3. 它的应用场景有哪些?

通过自问自答,去确保每一行代码都是有业务价值的,这也是一开始提及的“需求驱动”的意义所在。

在这个过程中也遇到过不少非常有争议的点,我们是怎么解决的呢?

案例一:如何做好基础框架的子工程划分与权限下放?

我们在GitLab上建了单独的一个群组base,在base群组下又拆分了以下几个子工程:

1)base-dependencies

定义一整套相互兼容的jar包版本集合,主要用于自研的基础包版本定义。

2)base-tools

工具箱,包括:ID生成器、关键词脱敏器、正则校验器、重复请求拦截器、请求参数校验器等。

3)base-web

基于微服务结构的设计,每个应用都是一个小型的web服务,除了要支持来自前端的http请求,也需要兼容来自后端的rpc请求,在这两种请求过程中,存在一些共性的行为,例如请求身份信息处理、日志处理、异常打印等,web模块统一这一系列的处理。

在微服务架构中,针对各个业务服务之间的通讯(rpc请求),需要有一套统一的标准。包括身份信息传递、链路信息传递,远程服务返回的参数封装、熔断、降级等,这一系列的通讯行为,需要有一套标准统一管理,避免因为服务之间标准不统一而造成的在交互过程中调用困难。

4)base-log

日志包,统一定义日志文件的输出格式,方便日志采集。

5)base-dal

持久化作为日常开发过程中最最重要的一个模块,这个模块需要支持各种DB的持久化,例如关系型的MYSQL、ORACLE,非关系型的Elasticsearch、mangodb等,也需要有丰富的api方法适用于各种场景的需求。

虽然我们目前已经存在多种持久化方式,但是每一种持久化方式的行为都是一致的,无论是关系型的mysql还是非关系型的ElasticSearch,对于业务使用来讲,行为上还是存在共性的,例如增删改查,所以可以抽象出通用的业务行为,这些业务行为不依赖具体的持久化方式。

业务不强依赖具体的技术实现(有点整洁架构的味道),所以每一种持久化方式应该是基于可拔插方式实现,并且需要支持多种持久化方式同时共存,在重构新的api之后,也要考虑旧项目迁移的成本,尽量避免旧项目的大成本改造。

6)base-cache

缓存组件,目前只用了Redis缓存方案。

7)base-utils

通用工具集,提供使用频率较高的工具类,也可以集成apache common、google gouve、hutools等第三方工具包,做一些浅封装,原则上少于10个项目使用的工具类不提炼到此包,第三方框架的工具集大多也只是用于框架本身使用。

8)base-parent

用于定义子项目必须遵守的一些约定,如Java版本,编码,公共第三方依赖的版本,测试规范,maven版本检查,打包规范,发布仓库声明等。

9)base-core

针对框架的标准提炼出的契约模块,主要提供标准契约,限定日常开发过程中的各种明面的约定和暗里的潜规则,如身份上下文、出参入参、通用异常、Result类等,原则上契约包属于整个框架最基础的包模块,这个模块是最轻量级的,里面只需要包含基础的契约限定,不需要过多的外部依赖,包括外部技术实现依赖。

以上每个子工程都有对应的负责人,并严格控制好代码权限,新加的需求都需要经过充分调研和讨论。

案例二:对于api包依赖的契约类(Result类)该不该调整包路径?

Result类用于接口返回数据的统一包装,API包会被依赖项目引入。

激进派认为,新框架应该有自己的标准,接入新框架是需要成本的,规范接入的标准是有必要的;

保守派认为,改造后接入成本过大,风险不好控制,建议契约类都不改。

经过充分讨论后决定:Result所在包路径不改变,大原则上按照新的标准来执行,像Result这种在api包定义的契约类依赖关系太强实在是改不动,牵一发而动全身,改造成本太大。

五、团队宣讲与项目接入落地

从团队组建到现在,已经过去两个半月的时间,所谓磨刀不误砍柴功,整个过程不能说特别高效(需要兼顾业务开发),但至少保证了每周都有一定的输出,接下来就要在团队内推广了。

有人说,做架构就是做销售,把你的“产品”推销出去,落实到各个技术团队中,切实有效地帮助到技术演进,要是没点销售本领怎么做推广?

首先准备好宣讲材料,宣讲内容如:

  1. 新框架的设计思路与设计原则讲解
  2. 新旧项目接入手册,以及推荐接入新框架的项目清单
  3. 新项目Demo工程讲解

宣讲材料打磨完善后,先在内部试讲,看看哪里需要进一步优化,最后再召集各研发二级部门下的各个微服务负责人进行集体宣讲,整个过程还算顺利,难点在于怎样便于大家接纳新框架。


写在最后

在整个过程中,我们遇到了不少困难,比如大家都不是全职做架构,需要优先保证业务开发正常进行,需要不断沟通对齐大家的意见,需要从接入成本、服务规模、技术合理性等角度权衡利弊,需要组织好语言做好整个团队的宣导……这无疑是在高速公路上换轮胎,但我们都一一克服了。

总结以下几项架构实践经验:

  1. 利用影响力自顶向下推进,注重形式也注重结果
  2. 尊重业务方代表的意见,让听见炮火的人做决定
  3. 明确架构思路与设计理念/设计原则
  4. 保持日常沟通讨论,持续产出有效成果
  5. 团队宣讲、推广落地必不可少

以上浓缩了我们架构团队做架构的一些精华与最佳实践,做架构不是一成不变的,也没有想象中那么神秘,但比起业务开发,差异点还是不少的,也希望未来能够一直朝着正确的方向走。

最后,引用自己最近的一些感悟:

专注打破认知极限,实践拓宽领域思维。

纸上得来终觉浅,实践永远是检验真理的唯一标准,愿你我在检验真理的道路上策马奔腾~

老规矩,一键三连,日入上千,点赞在看,年薪百万!


本文作者:Jensen

7年Java老兵,小米主题设计师,手机输入法设计师,ProcessOn特邀讲师。

曾涉猎航空、电信、IoT、垂直电商产品研发,现就职于某知名电商企业。

技术公众号【架构师修行录】号主,专注于分享程序员日常、架构技术、职场干货,关注回复“DDD”领学习DDD领域建模。

交个朋友,一起成长!

业务型公司如何推动技术架构演进?_spring_02