什么是一点五编程
(OPF Programming
)?
简要介绍
一点五编程(O
ne P
oint F
ive Programming),是一套编程思路和技巧,有明确的定义和编写规范。
这套东西由两部分组成:
- 核心 “一” : 模块化编程,最大限度地使用结构体组织程序。
- 技巧 “点五”:
(*p)->f(p)
。
后者 “点五” 虽然重要且实用,但并不如前面的 “一” 变化万千,不足以并列,故只能算半个,即“点五”。“一点五编程”由此得名。
“一点五编程”是一套通用的编程思路,不仅可用于C
,也可以套用于TypeScript
/JavaScript
,Lua
等语言上。
“一点五编程”虽然也可以用在C++
,Java
,C#
上,但是这些语言本身支持接口概念,功能上是“一点五编程”的超集。
在这些语言上使用“一点五编程”技巧,会显得不自然。
“一”
长久以来,“模块化编程”的强大被严重低估,这个词语,被埋没在一大堆花哨的计算机术语之中,它的深度和潜力,极少被重点发掘。
有许多人认为自己理解了模块化编程,其实只是理解了各种博客对模块化编程的解释和类比,实际使用中却难以融会贯通。
这里用一个简单的程序,尝试解释下模块化编程的好处。请看下面的程序:
int blah(int *result) {
int v1 = 0;
double v3 = 0;
char v4 = 0;
long v2 = 0;
if (v1 > 10 && v2 > 2*10 && v2 < 8*10) {
if (v1 > 10)
return 1;
v2 /= 3;
v1 += v1 * v2 + 3;
if (v1 > 10)
return 1;
}
if (v3 > 2 && (v3 * v4 > v3 / v4 - 5)) {
if (v3 < 0 || v3 > v1)
return 2;
v3 += v4 * 3.1415926 + 3;
if (v3 > 10)
return 2;
}
*result = (v1 + v1 * v2 - v2) / (v3 / v4 - v4);
return 0;
}
上面的代码,阅读起来比较费神,读者无法轻易知道各变量之间的关联,或者操作细节具体对应的上层逻辑。 通常是通过大量的注释去推理程序逻辑。
然而,依靠注释终究是下策,注释和代码是分离的。如果代码重构了,而注释却忘了更新,那么过时的注释,甚至可能增加阅读难度。
如果采用模块化的编写方式,那么代码可以变成如下形式:
int blah(int *result) {
struct d1 e1 = {0, 0};
struct d2 e2 = {0, 0};
if (d1_check_with(&e1, 10)) {
if (d1_fix(&e1))
return 1;
}
if (d2_check_with(&e2, 5)) {
if (d2_fix(&e2, e1.v1))
return 2;
}
*result = d1_generate(&e1) / d2_generate(&e2);
return 0;
}
这个时候再看代码,逻辑就清楚了许多。哪些数据是紧密关联的,哪些地方会打破这些界限,一目了然。
首先,混在一起的数据,依据功能单元被放到了不同的结构体中:
struct d1 {
int v1;
long v2;
}
struct d2 {
double v3;
int v4;
}
其次,针对这些数据的操作,也随之被归分到了对应的逻辑单元:
int d1_check_with(struct d1 *self, int n) {
return v1 > n && v2 > n*2 && v2 < n*8;
}
int d1_fix(struct d1 *self) {
if (self->v1 > 10)
return 1;
self->v2 /= 3;
self->v1 += self->v1 * v2 + 3;
if (self->v1 > 10)
return 2;
return 0;
}
int d2_check_with(struct d2 *self, int offset) {
return v3 > 2 && (v3 * v4 > v3 / v4 - offset);
}
int d2_fix(struct d2 *self, int v1) {
if (self->v3 < 0 || self->v3 > v1)
return 1;
self->v3 += self->v4 * 3.1415926 + 3;
if (self->v3 > 10)
return 2;
return 0;
}
上述例子虽是简化的虚拟场景,但是相信读者能够对模块化编程的优点体会到一二。
实际上程序越是复杂,模块化编程的优势就越是明显,在日常编程中反复体味和实验,能够使功力逐步提升。
笔者编程多年,曾自负掌握了很多编程技巧和语言,但是对于“模块化编程”这个概念,却经常有新的感悟。
这个世界上,有很多看似厉害的东西,了解后觉得不过如此。同时也有一些看似简单的东西,越学越觉得深奥。
“模块化编程”这个看似朴实的主题,所蕴含的力量,也许不曾被完全知晓。
“点五”
内功心法介绍完毕,下面就是“一点五编程”的招式“点五”了。这个招式的口诀,已经写在了我们的LOGO上:(*p)->f(p)
在解释原理之前,我们先放出这张图:
VARIABLE OBJECT INTERFACE
+--------+ .---> +--------+ .---> +--------+
| ---+--' | ---+--' | f1 |
+--------+ +--------+ +--------+
| | | f2 |
| | +--------+
| |
| |
+--------+
这张图已经展示了(*p)->f(p)
的工作方式。
这个招式的终极目的,是提供一种“自顶向下”的程序设计方式。别小看“自顶向下”这个貌似土里土气的词,它是构建大型程序的关键。
最朴素的程序设计方式,即“自底向上”的设计方式,是实现A
,然后在A
的基础上实现B
,最后在B
的基础上实现最终的结果C
。
自顶向下的设计,则是先实现C
的一部分,然后在另外的时间,或者由另外的人,实现B
,最后将这个B
跟已经实现的C
组合起来,成为最终的系统。
自顶向下,和自底向上,也可以结合使用:
比如 A
-B
这一步,用自顶向下的设计,B
-C
这一步用自底向上的设计;
也可以反过来,B
-C
这一步,用自顶向下的设计,A
-B
这一步用自底向上的设计。诸如此类。
下面用一个例子进行阐述:
假设我们需要编写一套通用的绘图函数,类似画矩形draw_rect
, 画圆圈draw_circle
,这些绘图函数要能够适应不同的设备。
如何处理呢?只要有一个通用的画点draw_point
函数,就能够在它的基础上实现画矩形,画圆,所以问题简化成了:
实现一个通用的画点draw_point
函数。
使用“点五”技巧,我们编写这样的接口结构体:
typedef int (*painter_draw_point_fn_t)(void *self, struct point pos, int color);
struct painter_i {
painter_draw_point_fn_t draw_point;
};
接下来,神奇的事情就产生了,我们可以在没有实现任何屏幕驱动代码的情况下,先去实现画矩形的函数!
int draw_rect(struct painter_i **painter, struct point p1, struct point p2, int color) {
struct point c;
/// ...
for (/* 获取到下一个矩形边的点到 c 里面 */) {
/// (*p)->f(p)
(*painter)->draw_point(painter, c, color);
}
/// ...
return 0;
}
这就是所谓的“自顶向下”程序设计。先实现上层逻辑,再实现底层逻辑。
底层的部分可以由我们自己,或者我们的伙伴,去实现具体的屏幕驱动。
举个例子,我们现在先实现一套RGB屏幕的驱动:
struct rgb_screen {
struct painter_i *interface; // 此字段必须是 rgb_screen 的第一个字段
int color_type;
int color_mask;
/// ...
};
int rgb_screen_draw_point(struct rgb_screen *self, struct point pos, int color) {
/// 在RGB屏幕上画点,本函数会使用`self->color_type`和`self->color_mask`。
}
struct pointer_i rgb_screen_interface = {
.draw_point = (painter_draw_point_fn_t)rgb_screen_draw_point,
};
int rgb_screen_init(struct rgb_screen *self) {
self->interface = &rgb_screen_interface;
}
接下来,我们再来实现一套单色屏幕的驱动:
struct mono_screen {
struct painter_i *interface; // 此字段必须是 mono_screen 的第一个字段
unsigned char bit_buffer[1024*768];
/// ...
};
int mono_screen_draw_point(struct mono_screen *self, struct point pos, int color) {
/// 在单色屏幕上画点,本函数会使用`self->bit_buffer`。
}
struct pointer_i mono_screen_interface = {
.draw_point = (painter_draw_point_fn_t)mono_screen_draw_point,
};
int mono_screen_init(struct mono_screen *self) {
self->interface = &mono_screen_interface;
}
上述“底层”的代码,可以直接搭配已经写好的上层代码使用:
struct rgb_screen scr1;
struct mono_screen scr2;
/// ...
draw_rect(&scr1, p1, p2, RED);
draw_rect(&scr2, p1, p2, WHITE);
draw_circle(&scr1, p1, 10, RED);
draw_circle(&scr2, p1, 10, RED);
不少学员在学习“点五”技巧的时候,觉得复杂,难以看清全貌。这是一种错觉,或者说心态上的障碍。
其实应该换个思路想想:
“一点五编程”只有这半招,不像其他体系,需要学习很多的知识。 这半招的学习曲线,纵然不是那么平滑,但是一旦迈过,后面就是一马平川!
带着这种心理预期,只要跟着例子,把上述步骤按顺序走,就一定能得到想要的效果。
这个过程经过很多次重复之后会形成直觉,后面甚至不需要敲代码,就能在脑海中构建出程序的模样。
编码规范
“一点五编程”的核心思想,不涉及到编码规范。但是为了传承和协作,一个好用的编码规范是很必要的。 为此,“一点五编程”拟定了如下的编码规范:
- 方法类函数,首个参数必须是方法所归属的结构体,且变量名建议使用
self
。 - 接口结构体,以
_i
结尾。比如struct painter_i
,struct iter_i
等等。 - 方法命名,建议使用
所属结构体
_方法名
。比如struct my_vec
的add
方法, 函数名称为my_vec_add
。 - 函数只返回状态码,计算结果通过一个指针参数传出来。
对于上述第4条,作如下解释:
很多C代码通过返回值返回计算结果,通过指针参数返回错误信息,示例如下:
int fn(int a, int b, int *error) {
if (some_check()) {
*error = CODE_xxx;
return SOMETHING;
}
return a + b;
}
“一点五编程” 不使用上述风格,而是统一使用下面的风格:
int fn(int a, int b, int *result) {
if (some_check())
return CODE_xxx;
*result = a + b;
return 0;
}
即通过返回值返回错误信息,通过指针参数返回计算结果。
且无特殊情况时,所有的函数都一致通过返回0
表达“成功”。
上面这一点,与很多返回布尔值,即用
1
表示“成功”的代码风格不同,需要注意。
这样做的好处,是可以使用统一的错误处理代码。
迭代器
“一点五编程”大量使用迭代器。迭代器本身只是“一点五编程”思路的实践应用, 但由于使用普遍,故而值得单独提出来说。
“一点五编程”的迭代器,只需实现两个接口: _iter_init
和 _iter_next
,
使用时,代码看上去大概是这样:
int blah(void) {
struct xxx_iter iter;
struct element *tmp;
size_t index;
/// 调用 _init 初始化迭代器,迭代器的初始化可能失败,故不能忽略返回值
if (xxx_iter_init(&iter))
return XXX_ITER_INIT_failed;
/// 反复调用 _next 获取迭代器的下一个元素,元素通过传入指针获取
/// 元素下标也能通过传入指针获取(传入NULL表示不需要下标)
//while (!xxx_iter_next(&iter, &tmp, NULL))
while (!xxx_iter_next(&iter, &tmp, &index)) {
if (do_something_on_element(tmp))
return XXX_ITER_ELE_ERROR;
}
return 0;
}
以上的迭代器设计方式,是一种建议而非强制。
迭代器的合理使用,能够极大程度提高代码的可读性。同时灵活使用迭代器还可以规避大量重复代码。
使用建议
“一点五编程”的格言是“重剑无锋,大巧不工”。它是编程范式界的“拳击”, 理论和套路很少,用精简的招式,快速的反应来应对现实世界里的各种编程难题。