什么是OPF
编程?
简要介绍
OPF
(O
bject P
ointer F
orwarding)编程,是一套编程思路和技巧,主要用于底层编程语言(比如C语言)。
它也可以表述为:
(*p)->f(p)
在解释原理之前,我们先展示出这张内存布局图:
VARIABLE OBJECT INTERFACE
+--------+ .---> +--------+ .---> +--------+
| ---+--' | ---+--' | f1 |
+--------+ +--------+ +--------+
| | | f2 |
| | +--------+
| |
| |
+--------+
这张图已经展示了(*p)->f(p)
的工作方式。
这个技巧的终极目的,是提供一种“自顶向下”的程序设计方式。“自顶向下”是构建大型程序的关键。
最朴素的程序设计方式,即“自底向上”的设计方式,是实现A
,然后在A
的基础上实现B
。
自顶向下的设计,则是先实现B
,然后在另外的时间,或者由另外的人,实现A
,从而让我们的B可以运行。
这听上去可能有点奇怪,A
还没实现呢,那么依赖于A
的B
要如何实现呢?
OPF
就是来解决这个问题的。
例子
假设我们需要编写一套通用的绘图函数,类似画矩形draw_rect
, 画圆圈draw_circle
,这些绘图函数要能够适应不同的设备。
如何处理呢?只要有一个通用的画点draw_point
函数,就能够在它的基础上实现画矩形,画圆,所以问题简化成了:
实现一个通用的画点draw_point
函数。
使用“OPF”技巧,我们编写这样的接口结构体:
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; /* 此字段必须是第一个字段 */
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; /* 此字段必须是第一个字段 */
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);
不少人在学习“OPF”技巧的时候,觉得复杂,难以看清全貌。 这是一种错觉,或者说心态上的障碍。
其实应该换个思路想想:
这个技巧的学习曲线,纵然不是那么平滑,但是一旦迈过,后面就是一马平川!
带着这种心理预期,只要跟着例子,把上述步骤按顺序走,就一定能得到想要的效果。
这个过程经过很多次重复之后会形成直觉,后面甚至不需要敲代码,就能在脑海中构建出程序的模样。
编码规范
“OPF”的核心思想,不涉及到编码规范。但是为了协作,一个好用的编码规范是很必要的。 为此,“OPF”拟定了如下的编码规范:
- 方法类函数,首个参数必须是方法所归属的结构体,且变量名建议使用
self
。 - 接口结构体,以
_i
结尾。比如struct painter_i
,struct iter_i
等等。 - 函数只返回状态码,计算结果通过一个指针参数传出来。
- 遵循Linux内核代码风格。
对于上述第3条,作如下解释:
很多C代码通过返回值返回计算结果,通过指针参数返回错误信息,示例如下:
int fn(int a, int b, int *error)
{
if (some_check()) {
*error = CODE_xxx;
return SOMETHING;
}
return a + b;
}
“OPF” 不使用上述风格,而是统一使用下面的风格:
int fn(int a, int b, int *result)
{
if (some_check())
return CODE_xxx;
*result = a + b;
return 0;
}
即通过返回值返回错误信息,通过指针参数返回计算结果。
且无特殊情况时,所有的函数都一致通过返回0
表达“成功”。
上面这一点,与很多返回布尔值,即用
1
表示“成功”的代码风格不同,需要注意。
这样做的好处,是可以使用统一的错误处理代码。
对于上述第4条,作如下解释:
使用Linux内核代码风格,有两个原因:
- 它使用广泛,在C语言的世界里被广泛使用。
- 它的大缩进有利于督促编写更好的模块化代码。
由于使用了Linux内核代码风格,变量,函数和类型命名都是用蛇形风格(a_b_c
),而不用驼峰(aBc
)或者Pascal风格(Abc
)。
使用结构体,枚举,联合体的时候,不使用typedef
, 而是写全类型。
迭代器
“OPF”大量使用迭代器。迭代器本身只是“OPF”思路的实践应用,但由于使用普遍,故而值得单独提出来说。
“OPF”的迭代器,只需实现两个接口: _iter_init
和 _iter_next
,
使用时,代码看上去大概是这样:
int blah(void)
{
struct xxx_iter iter;
struct element *tmp;
size_t index;
if (xxx_iter_init(&iter))
return XXX_ITER_INIT_failed;
//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;
}
以上的迭代器设计方式,是一种建议而非强制。
迭代器的合理使用,能够极大程度提高代码的可读性。同时灵活使用迭代器还可以规避大量重复代码。
使用建议
“重剑无锋,大巧不工”。OPF
是编程范式界的“拳击”,
理论和套路很少,用精简的招式,快速的反应来应对现实世界里的各种编程难题。