编程中要注意的工程要点2020年3月10日
咕咕,我来了,带着写了一年的 typescript,不留一个 enum
库的依赖管理
库的依赖管理问题是我开始写非业务代码后遇到的第一个工程问题,它的复杂性在于保证依赖解析正确的前提下,怎么减少共用包的资源占用(存储、解析)。
python / js 在引包的时候不会带上版本,只用包名称来 resolve,版本控制放在项目中,让项目环境来保证版本正确性。
每种语言都需要一个合适的包管理器。
好处包括
- 升级依赖时,如果版本是兼容的,那么代码不需要改,对于项目管理来讲是很简便的
- 节省存储、运行时资源,同一个库可以被多个使用者共享(动态链接的方式
坏处有
- 环境管理的复杂性带到开发时、发布时、运行时。首先开发者需要工具来管理开发时依赖、固定依赖库的版本、方便地定制库的
resolve方式(比如resolve到一个本地路径,一个网络地址等等);发布的时候需要维护发行版本和底层依赖的版本的对应关系、兼容关系、需要保证deterministic build;运行时用户需要确保本机环境已经具备运行的依赖条件,引申出去的需求有环境隔离、同一个库的多个版本怎么resolve等等。 - 依赖约定规则来定义兼容性,比如语义版本
semver
像 python 社区一般会为每个项目开一个独立的虚拟环境,通过精简依赖,隔离环境来减少环境依赖、隔离影响,但没有解决同一个库的多个版本问题(用的最新版本策略,可能 break 依赖老版本的接口),老的版本管理可能是用 requirements.txt 来做,只记录依赖的版本,比较现代一点的 pipenv 会加上源、版本、hash、多环境的区分,环境隔离的好的话,分发的时候基本就是一个静态链接了。

像 node 没有做到环境隔离(都存在 node_modules,一层层往上找,直到根目录);可以保证每个库有自己的 node_modules 来确保依赖版本正确,依赖管理很灵活,但也带来了碎片文件、占用大量存储空间、误配置时查找问题比较麻烦;支持可以用 peerDependencies 来依赖环境提供,deterministic 则是用 hash + 版本号来保证,yarn 中另加入了源的 check、monorepo 中共用库提升等等优化;支持 link 来开发时修改库的 resolve 方式。
像 deno / go 这种设计为网络领域的语言,直接用 url 来去中心化管理依赖,要达成 deterministic build 会有天生的困难,只能约定好所有发版都用新的 url 并且能提供下载,对传输层有特殊的安全要求,版本管理也会有麻烦(url 是否需要带上版本信息?)。

rust 有着比较现代的工具链 cargo。支持环境隔离;多环境;deterministic build;可以保留同一个库的多个版本,编译时会把他们重命名成不同的库,不会有冲突;方便地指定 resolve 方式(local path 或者 url);灵活的打包、分发方式(静态链接 / 动态链接,llvm)。很好地解决了上一章节中的开发时、发布时、运行时问题。
用库还是用服务
简单对比一下两者
- 库是内部依赖,服务可内可外(依赖网络)
- 库可同构,也可以带上
binding做异构、服务用中立的 IDL 来解耦- 异构的部署难度高,依赖基础设施(网络情况、服务发现等等)
- 同构的好处是可以做
lib或者很方便地做rpc,无需区分语言特性;坏处是无法把语言解耦,技术栈相对闭塞 - 异构的好处是可以在合适的场景中发挥每个语言的长处,比如用
C++做运算,用js做页面,用py/C++做数据分析,用JAVA/go做API/ 大数据等等;坏处是对团队的技术能力门槛有要求,一个体现在部署复杂性提升(需要准备多套环境),另外一个体现在对不同语言的理解和使用上有要求,对小团队来讲影响比较大
- 多端支持(比如
js的浏览器和node) - 服务引入更多环节、有网络开销、效率待评估
- 库的效率高、故障处理简单

用库还是用服务还需要看是否对运行环境有要求,拿 js 来讲,node 跑在后端,浏览器跑在前端,虽然是同一个语言,但运行环境差别很大(还有 es version 的差别、各个浏览器对 es 支持度的差别, 代码加载方式的差别等等…),写代码时就需要注意这些分支判断。像 rust 就有 conditional compilation 的概念,支持用 cfg 注解来区分 build target, os, platform, big/litter endian 等等。
参数管理
可以是 lib 的参数,builder 参数,API 的参数,实例的参数等等
- function 的哪些参数允许函数内部修改?传进来的是和
caller共享的内存吗?改了会对caller有影响吗?- 这点经常会引发一些隐藏的共享变量的
mutability问题(隐式传),在rust中看就很明白,所有权和mutability都是一眼看明白的(显式传,在签名中声明)
- 这点经常会引发一些隐藏的共享变量的
- function 的参数使用
struct还是...args?- 用
struct的好处是可以方便的override和扩展,但不适合做函数式编程的curry - 用
...args的话虽然一些optional的参数可以不传,但调用的话必须按顺序传,不能跳过;反过来讲它的好处就是可以用来做curry
- 用
- 哪些参数是全局的 / 库级别的?哪些参数是实例级别的
context,是否允许外部传入?- 比如
logger一般在生命周期内不会修改,常常使用库级别的 - 一些阈值视情况使用库级别 / 实例级别,看是否有隔离的需求,如果需要隔离的话,这些参数到哪都得带着走,往往作为一整个
context来传
- 比如
版本号与向后兼容
前面讲的库的依赖管理、服务、参数管理都会涉及到版本号与向后兼容性,目前业界用的比较多的是 semver,约定了大版本号的更迭会出现部分 break(指去除或者修改了 API),大版本号小于 1 表示尚不稳定。一般稳定后发新功能尽量避免 break 已有的 API,如果必须 break 的话,要给到对接方一个显眼的迁移方式,一般放在 release note 或者代码注释以及迁移文档中。

PS
typescript 的 Structural type system 对 enum 真的太不友好了..