在编程当中,当我们掌握了编程语言的基础知识、经过一段时间编程练习之后会发现三个问题:

  • 项目复杂。最直接的就是代码越写越多,夸张到所有代码堆积在一个文件,比如单文件 10 w 行代码。这是因为没有培养好的代码习惯、没有软件设计与重构的一些理念,而如果代码素养好一点,我们通常会把代码拆分成几个文件,并根据目录进行分类。

  • 没有规范。接上一条,也许代码多了我们会开始试着将代码拆分成几个文件,但其实没有一个统一的思想,我们还是容易继续在拆分出的文件中重新写出个 10 w 行代码的单文件,并在需要修改代码的时候东翻西翻找,浪费了时间。

  • 重复写代码。我们会发现当想要实现几个不同功能的时候,它们的代码是有很大部分一样的结构,比如接口开发中的抽象类。我们很希望有一个模板,也会自己去做一些可以复用的代码模板。

为了解决这三个常见的问题,于是有越来越多的开发者或团队开发了解决方案,框架。其他开发者可以直接使用这个框架,基于作者设计好的项目架构、文件结构和代码规范,能够在短时间内开发项目更清晰,性能更优越,维护更简单的软件。

而有些框架要么因为太简单,要么因为太复杂,而不能直接拿来用。除非现有的框架中的功能是和我们业务中的需求是完全匹配,而且其中的代码不是复杂、能够很快阅读完,这种框架直接拿来用是非常合适的。但实际中有一个和需求比较契合的框架也是很难找的、甚至没有的,找得到的话那都是运气好的情况了。尤其是大公司,做大了之后,肯定会有和别人不一样自己的定制需求,这个就不在今天讨论的框架范畴内了。

今天来聊聊一些偏主观点的内容,怎么判断框架的优劣?这里我们不讨论具体某个框架、代码写得好不好。有几个框架设计时需要考虑的问题,如果框架符合了,那么说明可能会是优秀的项目:代码规范、自动化、平台化、集成化、组件化、插件化、通用化。如果我们在企业中是扮演企业自研框架的设计者,考虑这几个问题也是很有帮助的。

代码规范

不考虑一个框架的工程质量,如果我们亲自去看,仅凭自己的感受,亲自去看代码的其实比较容易发现代码写得是否好。这里以 Golang 的框架为例,一个代码规范的框架,是老老实实规规矩矩用上静态代码检查工具 GolangCI-Lint 并且遵守着 Go 社区的规范。而不规范的框架虽然声势浩大的,但注释写得很少,代码很乱,随处可见不知道哪里来的下划线的命名(Golang 中的命名是以小驼峰命名法)。

自动化

  • Layout 代码自动生成(DDD/Clean Atch)
  • 服务上线自动发布
  • 自动生成接口文档
  • 服务接入 SDK 自动生成
  • 常见 code snippet(boilerplate)内置在 CLI 工具内
  • 不要让用户去复制粘贴,我们来帮他自动写好

我们既然要写一个框架,那么写新接口的时候,涉及到大量的重复代码,比如在入口写一个 Controller 或者在 logic 中写一个入口又或者在 Dao 里面又要写一些入口之类的东西等等。这些东西其实都是应该有自动的工具去生成的,通过模板生成工具去实现。

第二点说的是公司中内部的交接需要打通,一旦服务上线之后,就会自动发布到服务注册系统里,不再需要我们人工去做一大堆的申请流程。

自动生成接口文档,这个功能在 Golang 领域最早发现于 Beego 框架。可以直接把 Swagger 集成到里面,通过注解的方式,能把我们开发的接口相应的文档都生成出来。目前,在公司内,有些开发不一定会用到 HTTP,那就需要有一套专门的接口定义,就比如 Thrift 定义的 IDL,gRPC 的 Protobuf。有些新兴公司会用到 gRPC,这种情况还是用 pb 去做服务的定义,因此就需要把 pb 和客户端的生成尽量都做出平台,并且写一些注释,然后自动生成接口文档,做好自动化。

第四点是说如果我们想接入一个外部的服务,不能说要到处找怎么接入,最好是有平台支持,通过平台能够搜到相关的服务接口,直接在接口的网页上能直接把我们的 SDK 生成好,并且最好是能够把我们的代码系统也打通,直接就能把相应的代码生成在我们的代码库中,然后用个简单的 git pull 就好了。

最后是常见的模板代码 code snoppet 最好就内置在 CLI 工具里面,同时不要让程序员去其他项目中不断寻找,然后粘贴过来,这是比较容易出处的,不太建议。

平台化

  • IDL 在平台中管理
  • 接口文档可搜索
  • 服务上线/部署流水线化
    • 举例:
      • step1:修改服务名,服务级别为 p0、p1;
      • step2:选择依赖资源,如 db、redis、mq 或外部服务;
      • step3:选择服务部署集群,高可用要求
      • step4:部署

平台化是指我们的框架在公司里面和开源社区很不一样。如果我们公司使用 Thrift,服务定义用了 IDL,所有的 IDL 一般需要在一个统一的平台中去管理。也就是说能够把公司中所有的接口都找得到。

通过平台进行集中管理接口,最好还是应该有一个专门的接口文档可以去做检索。比如当我需要用一个接口,发现了它在什么地方能够实现,这时候接口出现了问题,你就能通过平台去找到相应的负责人。这种方式在大公司内是比较基本的操作了,但对于中小公司来说还是欠缺的。

第三点是整个服务的上线和部署都需要流水线化,不需要再登录服务器去运行一些部署命令。

我们来看一个实际的例子:

image-20211222225018573

假如现在我们想上线一个服务,只需要跟着平台的指引,简单的点点鼠标就好了。

集成化

  • 框架提供所有基础设施 SDK(log, tracing, config center, orm/sql builder, es sdk, clickhourse sdk, mq, etc…)
  • 开箱即用,核心依赖无需外部站点寻找
  • 专门的 organization 下维护其他非核心依赖
  • 解决用户的选择困难症

集成化说的是要把公司内所有的基础设施都做成开发者都可以使用的 SDK,定期进行更新,并且放到一个集中的地方,让大家能够去看。可以是公司专门的 organization 下专门去管理,或者对于特别核心的组件如 MySQL 之类的应该和框架放在一起,这样用户在选择的时候不用去考虑哪个组件和哪个组件版本之间如何搭配(版本搭配是个比较麻烦的事情)。

组件化

  • 稳定需求,沉淀为统一组件
  • 公司内历次故障经验都应尽量沉淀为避免/解决问题的组件(可以是重试组件中的规则,也可以是静态扫描工具中的一个 linter)
  • 不要让每个人都必须读一遍《Google SRE》书才能做好稳定性

组件化是说,如果你处于公司部门内的框架小组或者稳定性小组,那么我们可以通过学习国内或国外的一些先进经验,并且应该把这些先进的经验直接沉淀为开发组同事日常能够用的组件或库,直接就可以调用或简单配置就可使用。比如限流,可能有分布式限流或自适应限流。最终达到的效果就是不需要每个人都去买一本书然后完整地读完,了解诸如稳定性之后才能够做开发,应该是大家不需要读书也能把稳定性做好。(《SRE》 这本书极力推荐)。

其次是公司内历次发生的故障也应该沉淀成为相应的组件。要么是框架中的稳定性组件,要么是可以变为静态扫描工具中的 linter。这部分工作大多数公司做得不太好。

插件化

  • 面向接口编程
  • 组件以 plugin 形式提供(不是 Go 语言的 plugin)

image-20211222225459858

插件化的功能,对于不管是做业务还是做框架的开发者来说都需要考虑的。比如图中所示,是由框架 go-micro 很好地运用了这个思想。它把其内部的组件都抽象成各种各样的接口,通过接口的形式能让对应的组件变成插件。

插件的好处是可以通过依赖反转的方式可以让业务逻辑不需要强依赖于任何一个外部的组件。比如存储中,调用存储逻辑时只调用了 save 的接口,这个 save 接口可以是 MySQL,也可以是 ES,也可以是任何的其他第三方存储。如果说有一天需要切换逻辑,把数据存储到其他的、性能更高更快的存储,有新的接口,那么就需要保证切换的时候不影响核心代码。

任何需要做切换、有选择的东西都可以做出插件化的方式,做一个简单的抽象。

通用化

  • 主要针对开源框架
  • Leave options open by Uncle Bob
  • 让用户有选择的权利(比如喜欢 etcd/zk 做注册中心,就非要用),可以通过插件化来达成
  • Go-micro 是很好的范例(但注意不要直接抄别人的代码,否则开源容易被人锤)。

对于企业内部框架来说,通用并不是一定要追求的目标。也就是说,如果你是企业中自研框架的开发者,那么通用化不是框架设计需要考虑的问题。

通用化主要针对的是开源框架。如果你在公司内有一个框架项目,同事间都有口皆碑。你想通过开源框架来提高你的技术影响力,在社区创造影响力,同时能够让公司的品牌也能够受益,这种时候就需要考虑到一些问题。

公司中的框架需要把所有的核心依赖都放在框架中,由框架去管理核心依赖的版本,我们在框架层面直接把所有依赖的冲突的问题都解决掉。但是开源的话,社区里的要求又是不一样的。对框架要求最多的就是定制性,比如这个公司想用 Kafka,另一个公司想用 RocketMQ。这些公司当中如果业务只能用 RocketMQ,而你的框架仅支持 Kafka 那就不合适了。

因此,开源框架最重要的是能够做通用。这个通用和插件化的思想很类似,都是通过一些 Interface 之类的,把你的框架做到任何大家想定制的地方,并且快速开发一套新的东西出来。

通用化还有个好处是说方便扩展框架生态。比如 RocketMQ 过了两三年出了一套新的,如果能够在 SDK 中实现你的接口,那么就能够很方便地集成到现有的框架中。

总结

对于企业场景来说,框架越大越全、核心的东西越是自动化,那么是越好的。

对于开源场景来说,我们需要给予用户更多的选择权。

OK,今天的文章讲解了一些设计层面的东西,下期还会深入一些 web 的框架实现去讲一讲它们的原理与设计,敬请期待!