Filosfino

观察世界,出走青铜时代、梦马臆想

编程中要注意的工程要点

发布于 # 编程 # 工程

咕咕,我来了,带着写了一年的 typescript,不留一个 enum

库的依赖管理

库的依赖管理问题是我开始写非业务代码后遇到的第一个工程问题,它的复杂性在于保证依赖解析正确的前提下,怎么减少共用包的资源占用(存储、解析)。

python / js 在引包的时候不会带上版本,只用包名称来 resolve,版本控制放在项目中,让项目环境来保证版本正确性。

每种语言都需要一个合适的包管理器。

好处包括

  1. 升级依赖时,如果版本是兼容的,那么代码不需要改,对于项目管理来讲是很简便的
  2. 节省存储、运行时资源,同一个库可以被多个使用者共享(动态链接的方式

坏处有

  1. 环境管理的复杂性带到开发时、发布时、运行时。首先开发者需要工具来管理开发时依赖、固定依赖库的版本、方便地定制库的 resolve 方式(比如 resolve 到一个本地路径,一个网络地址等等);发布的时候需要维护发行版本和底层依赖的版本的对应关系、兼容关系、需要保证 deterministic build;运行时用户需要确保本机环境已经具备运行的依赖条件,引申出去的需求有环境隔离、同一个库的多个版本怎么 resolve 等等。
  2. 依赖约定规则来定义兼容性,比如语义版本 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)。很好地解决了上一章节中的开发时、发布时、运行时问题。

用库还是用服务

简单对比一下两者

用库还是用服务还需要看是否对运行环境有要求,拿 js 来讲,node 跑在后端,浏览器跑在前端,虽然是同一个语言,但运行环境差别很大(还有 es version 的差别、各个浏览器对 es 支持度的差别, 代码加载方式的差别等等…),写代码时就需要注意这些分支判断。像 rust 就有 conditional compilation 的概念,支持用 cfg 注解来区分 build target, os, platform, big/litter endian 等等。

参数管理

可以是 lib 的参数,builder 参数,API 的参数,实例的参数等等

版本号与向后兼容

前面讲的库的依赖管理、服务、参数管理都会涉及到版本号与向后兼容性,目前业界用的比较多的是 semver,约定了大版本号的更迭会出现部分 break(指去除或者修改了 API),大版本号小于 1 表示尚不稳定。一般稳定后发新功能尽量避免 break 已有的 API,如果必须 break 的话,要给到对接方一个显眼的迁移方式,一般放在 release note 或者代码注释以及迁移文档中。

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