编程是门手艺,NGINX社区的教训分享 一文提过,业余的程序员善于整体设计和细节解决能力。本文探讨整体设计,尤其是模块化这个技能。
全能蠢才,Fabrice Bellard
FFmpeg,最弱小的流媒体库
QEMU,硬件虚拟化的虚拟机
TCC,迷你CC编译器
QuickJS,100%反对JS语法的C引擎
等等,以上皆出自一人之手,法国蠢才。
去年QuickJS曾一度刷爆技术圈,NGINX社区的哥们第一工夫举荐给我看,并以蠢才称他。
这软件开辟了我的视线。本文以它为引子探讨我认为十分重要的技能:如何组织代码。
NJS,实现语言引擎真难
私下问过Fabrice Bellard(给QJS提过patch)开发QJS的历程,答案令人惊叹,他只用了两年的业余时间。参加NJS这几年,才深知实现语言引擎有多简单。
NJS从17年开始,当初差不多实现40%。但根底曾经十分良好,后续性能开发会疾速很多。而且罕用性能都曾经反对,其中我反对了模块化,箭头函数,等罕用的。语言解析引入了LL(k)。
看似做了些不错的工作。然而跟QJS比,以台球打个比方。一个长距离很准的选手,90%的球都能打进,看似很厉害。但对一个发力十分厉害的人来说,他可能只需80%的准度,再加良好的走位,就能轻松一杆清台。
提QJS不是不可一世,这样比照很不偏心。QJS作者自身就是个JS专家,他都能用JS实现虚拟机。参加NJS的人员,包含Igor都不是真正的JS语法里手,JS的语法着实太宏大。咱们平时开发过程中,有个社区外的JS里手对咱们帮忙十分大,几乎就是JS活字典。因而在后期,只能靠着语法手册,而后实现,有些实现跟语法的实质有出入的话,又得重头再来。举个例子,晚期实现的apply和call两个语法真是让人吃尽了苦头,这也是我最早参加的,因为修复它的bug,做了重构,而后发现社区的人十分承受这种重构的做法,有种碰到知音的感觉。
QuickJS,五万行代码一个文件的软件
我会解释这种做法是正当的。此时必须提出来,前面再详加解释。
模块化,最好的代码组织形式
我在参加NJS时,第一件事就是让它反对模块化编程。NJS刚进去时我就开始关注,前面挺长一段时间,用NJS写代码只能放在一个文件里,这对代码组织是极不敌对的。先看下JS的模块化用法:
main.js
/* 自定义模块 */import foo from 'foo.js';foo.inc();/* 内置模块 */import crypto from 'crypto';var h = crypto.createHash('md5');var hash = h.update('AB').digest('hex');
foo.js
var state = {count:0}function inc() { state.count++;}function get() { return state.count;}export default {inc, get}
反对模块化之后,变得十分好用。这个大性能也是NGINX作者Igor亲自帮review和调整的,播种良多。主观讲,JS语法比Lua切实好用太多,NJS目前曾经十分稳固,只是性能没那么繁多,举荐轻量利用思考用NJS,而且社区十分沉闷,置信将来可期。
当初轻瞥一下QuickJS的源码。
JSContext *JS_NewContext(JSRuntime *rt){ JSContext *ctx; ctx = JS_NewContextRaw(rt); if (!ctx) return NULL; JS_AddIntrinsicBaseObjects(ctx); JS_AddIntrinsicDate(ctx); JS_AddIntrinsicEval(ctx); JS_AddIntrinsicStringNormalize(ctx); JS_AddIntrinsicRegExp(ctx); JS_AddIntrinsicJSON(ctx); JS_AddIntrinsicProxy(ctx); JS_AddIntrinsicMapSet(ctx); JS_AddIntrinsicTypedArrays(ctx); JS_AddIntrinsicPromise(ctx); return ctx;}void *JS_GetContextOpaque(JSContext *ctx){ return ctx->user_opaque;}void JS_SetContextOpaque(JSContext *ctx, void *opaque){ ctx->user_opaque = opaque;}
所有源代码扔进一个文件里,我看过不少软件的源码,而且是比拟残缺的。NGINX, Unit, NJS, Lua等,以集体感观而言,QuickJS是最好的。初看有点凌乱,但细看的话(可能须要很相熟JS语法),相对的巨匠之作。
如果想删除某个语法性能,在QuickJS里能够间断的从某行始终删除到另一行,间断的一块。这在其它软件是不可能做到的,要么多个文件都要删除,要么在一个文件也要删除多个不同的中央。我认为这就是模块化的精华:高内聚。
学过设计准则的同学想必都晓得软件要高内聚,低耦合。我的了解是只有做到了高内聚,低耦合就是自然而然的事件。
举个例子,要实现nginx lua模块。有两个重要的性能:nginx模块相干函数,lua封装相干函数。
适度设计形式:
ngx_http_lua_module.c/* nginx模块相干函数 */ngx_http_lua_request.c/* lua封装相干函数 */
正当形式
ngx_http_lua_module.c/* nginx模块相干函数 *//* lua封装相干函数 */
https://github.com/hongzhidao...
适度设计是一种很容易踩进去的陷井。
探讨1:
如果有更多的性能,比方http subrequest这种性能进来时怎么办?
倡议还是放在同一个文件里,不要被代码行数影响。
探讨2:
又有更多的性能,比方http share memory这种性能进来时怎么办?
是能够思考独立到另一个文件了,准则就是要找到一个服气的理由,新的性能能独立成一个高内聚的模块。有个特色是它往往会有专门的API,比方共享内存操作的get, set等。
换另一个角度看,一个文件的引入自身也是一种老本,而且比函数级别更高。每次的重构都应该带来本质的价值。这是我保持尽量放同一个文件的起因。我晚期提过几次倡议,想对njs做相似的事件,起初证实有些是适度设计的。而有些是正确的,比方把njs_vm.c分成njs_vm.c和njs_vmcode.c。一个负责虚拟机,一个负责字节码解决。
总结一下:
高内聚是最高准则。
引入新文件老本高于函数,要有本质的价值才做。
不要被代码行数影响。
合作只是一种分工,不能做为毁坏高内聚的理由。
再谈设计
后面说QuickJS的代码品质十分高,是因为他的设计令人折服。整个QJS的代码行数不到5万,实现了100%的语法,其中还包含十分硬核的大数和正则,都本人造轮子。从整个引擎的实现方面它就做了高度的形象,而且用的算法非常简单无效。举个例子,JS里对象的属性操作应该是最罕用的,比方 a['name']。a和name在语法解析时都是字符串,术语叫token。QJS用一个十分高效的hash实现,将所有JS用到字符串的都包含进去了,代码也很少。
typedef struct JSShapeProperty { uint32_t hash_next : 26; /* 0 if last in list */ uint32_t flags : 6; /* JS_PROP_XXX */ JSAtom atom; /* JS_ATOM_NULL = free property entry */} JSShapeProperty;struct JSShape { uint32_t prop_hash_end[0]; /* hash table of size hash_mask + 1 before the start of the structure. */ JSGCObjectHeader header; /* true if the shape is inserted in the shape hash table. If not, JSShape.hash is not valid */ uint8_t is_hashed; /* If true, the shape may have small array index properties 'n' with 0 <= n <= 2^31-1. If false, the shape is guaranteed not to have small array index properties */ uint8_t has_small_array_index; uint32_t hash; /* current hash value */ uint32_t prop_hash_mask; int prop_size; /* allocated properties */ int prop_count; JSShape *shape_hash_next; /* in JSRuntime.shape_hash[h] list */ JSObject *proto; JSShapeProperty prop[0]; /* prop_size elements */};
外面指针还用到负操作, 他是数学里手玩的转。
为什么NJS不能这样呢?依赖,各细节之间互相援用。软件开发中没方法的事件。
还以打球为例,那些走位和发力十分老道的球手,打法往往是简略无效的,不要奇怪为什么有些球不先击打进去,而抉择更不好打的,所有在把握之中。
设计重于实现
这是我这两年比拟大的领会。以前会感觉有这设计的功夫,早把货色实现好了,而且认为重构能解决所有的设计有余。这是没错的,问题是花了更多的工夫在走弯路。
write some code, think, write more, meditate, write a meaningful commit log, take a sleep, think again, and re-read, split/fold/re-write, think, become happy with the final result.
以上是Unit的负责人给的倡议,集体感觉这是一种可行无效的形式。NGINX的http2实现就出自他的手笔。对了,NGINX的http3行将实现。
有办法才有可行
本系列文章都会有实操办法。实际对想晋升代码的同学是很无效的形式,我集体感觉学习或写我的项目是一种形式。
utopia是我写的一个API网关框架,只有一千行代码。外面的一些设计就参考 Unit,尤其是路由局部。我理解他们的设计历程,十分优良。这是一个非常适合学习的我的项目。
设计能够聊的切实太多,远不止一文能够讲完,当前会一直的夹杂在其它章节。
[nginx-lua-module] https://github.com/hongzhidao...
[the-craft-of-programming] https://github.com/hongzhidao...
技术问题欢送issue里交换
[utopia] https://github.com/hongzhidao...
还未开源,关注公众号及时理解更新