编程中要注意的工程要点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 真的太不友好了..