# 【C语言最佳实践】子驱动程序模式
作者:wallace-lai
发布:2022-08-02
更新:2023-11-26
## 一、设计和编码水平弱的根本原因
根本原因:**抽象能力不足**
(1)对事物的正确认知建立在归纳总结之上
(2)抽象是归纳总结的一种升华
(3)如何提高自己的抽象能力:多看多写
## 二、子驱动程序模式
子驱动程序模式在大量的稍有规模的C项目中大量应用,比如:
(1)Unix中的一切皆文件
(2)Unix/Linux内核的虚拟文件系统以及设备驱动程序
(3)MiniGUI中支持多种类型的图片格式以及逻辑字体
子驱动程序模式的一般实现套路:
(1)一套聚类接口
(2)一些公共数据组成的抽象对象(数据结构)
(3)一组函数指针组成的操作集(数据结构)
(4)针对不同子类的操作集实现
以STDIO接口的简单实现举例
(1)`file_obj`是内部的抽象对象,`file_ops`则是内部的抽象的操作集,`FILE`则是对外抽象数据结构,用户根本无需知晓其中的具体字段。
```c
struct _file_obj;
typedef struct _file_obj file_obj;
struct _file_ops {
file_obj *open(void *pathname_buf, size_t size, const char *mode);
ssize_t read(file_obj *file, void *buf, size_t count);
ssize_t write(file_obj *file, const void *buf, size_t count);
off_t lseek(file_obj *file, off_t offset, int whence);
void close(file_obj *file);
};
struct _FILE;
typedef struct _FILE FILE;
```
(2)如果只考虑基本功能,`FILE`结构里只需要有一个抽象文件对象的指针和对应的操作集即可。
```c
struct _FILE {
struct _file_ops *ops;
struct _file_obj *obj;
};
```
(3)对于`fopen`来说,它只需要将传入的文件名对应的文件打开即可。打开时返回一个文件描述符即可,因此对于该类接口的实现可以是如下的方式
```c
struct _file_obj {
int fd;
};
static file_obj *file_open(void *pathname, size_t size, const char *mode)
{
(void)size;
// ...
}
static struct _file_ops file_file_ops = {
.open = file_open;
// ...
};
```
对外提供的`fopen`接口可以这样包装
```c
FILE *fopen(const char *pathname, const char *mode)
{
FILE *file = NULL;
file_obj *obj = file_open(pathname, 0, mode);
if (obj) {
file = calloc(1, sizeof(FILE));
file->obj = obj;
file->ops = &file_file_ops;
}
return file;
}
```
(4)同理,对于`fmemopen`来说也是类似的处理
```c
#define MEM_FILE_FLAG_READABLE 0x01
#define MEME_FILE_FLAG_WRITEABLE 0x02
struct _file_obj {
void *buf;
size_t size;
unsigned int flags;
off_t rw_pos;
};
static file_obj *mem_open(void *buf, size_t size, const char *mode)
{
// ...
}
static file_obj *mem_open(void *buf, size_t size, const char *mode)
{
// ...
}
static struct _file_ops mem_file_ops = {
.open = mem_open;
// ...
};
FILE *fmemopen(void *buf, size_t size, const char *mode)
{
FILE *file = NULL;
file_obj *obj = mem_open(buf, size, mode);
if (obj) {
file = calloc(1, sizoef(FILE));
file->obj = obj;
file->ops = &mem_file_ops;
}
return file;
}
```
更进一步考虑,STDIO是带有缓冲区功能的,那么请思考以下问题:
(1)缓冲区信息应该在FILE中维护还是在file_obj中维护?
个人理解:缓冲区的信息应该在FILE中维护,它属于使用策略的一部分。
(2)当前读写位置在什么地方维护?
个人理解:当前读写位置也应该在内部的file_obj里维护,它属于最基本的信息,属于机制的一部分。
(3)子驱动程序设计的关键点
1. 抽象对象的数据结构如何确定?
2. 操作集如何取舍?
对于第三个问题,有一个一般性的指导原则,我们首先需要正确区分机制和策略
1. 机制:需要提供什么功能(放在子驱动程序里做)
2. 策略:如何使用这些功能(放在子驱动程序的上层抽象层里做)
以STDIO为例:
1. 对于STDIO而言,需要提供什么样的功能?需要提供一组**最小**的**完备**的文件操作集合`_file_ops`,如下所示。
```c
struct _file_obj;
typedef struct _file_obj file_obj;
struct _file_ops {
file_obj *open(void *pathname_buf, size_t size, const char *mode);
ssize_t read(file_obj *file, void *buf, size_t count);
ssize_t write(file_obj *file, const void *buf, size_t count);
off_t lseek(file_obj *file, off_t offset, int whence);
void close(file_obj *file);
};
```
任何不同类型的文件对象(对于内存映射文件也如此)都需要这五个操作接口。所以,这个文件操作集合就是机制。而**机制应该放到子驱动程序里做**。
2. 而在基于这组最小完备的文件操作集之上的功能,比如,带有缓冲区支持的格式化输入输出属于使用策略,对不同类型的文件对象是一样的,**应该放到抽象层去做**。