博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
C 语言面向对象的封装方式(示例)
阅读量:4110 次
发布时间:2019-05-25

本文共 3954 字,大约阅读时间需要 13 分钟。

前一篇文章,我介绍了C语言编程常见的两种代码组织方式:

1)函数和数据结构分离

 2)封装

并从原理上讲述这两种方式的根本区别。

大型项目中,推荐采用封装的方式,有利于团队协作和每个模块独立演进。

 

本文,给出一个代码示例,具体展示这两种方式在代码实现上的差别。

业务场景描述如下:

对于数据库、文件系统、存储系统等,数据通常以页(Page)为单位,在数据文件中进行组织。服务进程以页为最小IO单位从磁盘上读出,并在内存中缓存这个页面。后续业务过程如果读取的数据在这个页中,则直接从内存页获取数据。如果要写入新数据,或者要更改原来的数据,则需要找到剩余空间能满足新写入数据大小的Page,然后在目标页中进行写入或者数据修改的动作。由于页上可能会有并发读写操作,因此需要注意加锁。

一个数据页(Page)的内部结构,示例如下:(PostgreSQL的数据页)

PageHeader 是页头部的控制信息,其结构如下:

我们实现数据写入(insert)和scan过滤两种场景,看用两种不同的代码组织方式,在代码实现上的差别(代码进行了简化,不考虑事务等复杂因素)。(方式一) 函数和数据结构分离
调用者的逻辑:
向Page中写入一行数据:
​​​​​​​

int insert_row(...) {    // 计算要写入的行所需的空间大小    int row_size = ....;    // 找到剩余空间满足这个要求的Page    page_header_t* phpage = find_page(row_size);    // 加锁,避免并发写    lock_write(phpage);    // 写入row header    char* pbegin = (char*)phpage + pheader->pd_upper - row_size;    row_header_t* phrow = (row_header_t*)pbegin;    phrow->len = row_size - sizeof(row_header_t);    phrow->xxx  = ....;    // 写入row的各个列数据    char* pcol = (char*)phrow + sizeof(row_header_t);    memcpy(pcol, ....);    ....    // 找到空闲的itemid槽位    item_id_t* pitemid = (item_id_t*)((char*)phpage + sizeof(page_header_t));    while (LP_UNUSED != pitemid->flags) pitemid++;    // 写入row对应的槽位信息    pitemid->offset = (char*)phrow - (char*)phpage;    pitemid->LP_NORMAL;    // 解锁    unlock_write(phpage);    return SUCCESS;}

读取page中的各行记录,根据条件过滤(比如 where id>2):

int scan_row(page_header_t* phpage, filter...) {    // 加读锁    lock_read(phpage);    // 遍历itemid, 只查找有效行    item_id_t* pitemid = (item_id_t*)((char*)phpage + sizeof(page_header_t));    while (LP_UNUSED != pitemid->flags) {        if (LP_NORMAL == pitemid->flags) {    // 有效行            row_header_t* phrow = (row_header_t*)((char*)phpage + pitemid->offset);            // 判断此行是否满足过滤条件            ......        }        pitemid++;    }    // 解锁    unlock_read(phpage);}

 

从上面的数据读写两个函数,我们可以看出,在函数和数据结构分离的模式下的特点

1)数据结构的内部成员,对调用者都是可见的,都是可读写的;
   上例中,page_header_t, item_id_t, row_header_t这些数据结构内部的成员,调用者都是知道的,并且可以直接读写的。
2)数据结构之间的关系,调用者也是知道的;
   上例中,page_header_t的后面就是一系列row_id_t, 这些row_id_t的后面是空闲空间,每行数据从page后面逐个向前分配空间;
3) 调用者根据1)和2)的信息,自己来编写自己的业务逻辑,对这些数据结构的成员进行读写控制,最终形成各种业务函数。
4)需要多少个业务函数,是调用者来决定的。调用者可以根据业务需求,随时增减业务函数

 

这种模式下,需要数据结构保持长期稳定状态。一旦数据结构发生变化,那么调用者设计的各种业务函数,基本都需要修改,哪怕只是把数据结构的一个成员名字修改了,也会导致大量的函数无法通过编译。

 

而工程实践中,需求变更是常态,尤其是做有技术竞争力的产品,对底层数据结构、实现逻辑进行大幅度优化改进,也是经常发生的。

因此,我们更希望用封装模式,来组织代码。
这两种方式的根本区别,用下图来展示:

 

在封装的设计下,代码上有了一些显著的变化:

1)数据结构的成员,对调用者不可见。调用者无法直接对数据结构的成员变量进行读写,只能通过特定的函数来操作这些数据结构。
   这些操作函数,是数据结构的设计者提供的,我们把这些操作函数叫接口函数。
2)接口函数,不仅屏蔽掉数据结构的成员变量,还屏蔽掉数据结构之间的关系,甚至有些数据结构调用者根本就不知道其存在。
3)接口函数,是面向使用者进行设计,而非面向底层数据结构进行设计。对业务场景进行分析,提取共性,进行接口抽象,最终形成接口函数。
4)接口函数的函数名、参数类型和返回值,都要充分体现业务语义,屏蔽底层数据结构的具体实现细节。

再回到对数据页(Page)进行读写操作的例子上,我们用封装的思想,重新设计一下代码
服务层:
1. 数据结构体 page_header_t,item_data_t, row_header_t 的成员结构无需调整,但我们需要把它们的定义放到.c文件中,这样调用者就不能直接访问他们的成员了。可以用指针,但不能用指针访问其成员。
2. 设计接口函数,封装业务语义,主要有:
写入行:
1) 为了写入,要获取满足空间的页面,返回已经锁定的页面
   page_header_t* find_page_for_write(int row_size);
2) 写入row数据
   int write_row_to_page(page_header_t* phpage, void* pdata, int row_size);
3)释放锁定的页面
   int release_page_for_write(page_header_t* phpage);

读取page,返回各个行数据:

1)准备扫描page(内部加读锁)
page_scan_t page_scan_begin(page_header_t* phpage);
2)扫描下一行, 返回0 表示找到一个有效行
int page_scan_next(page_scan_t* pscan, void** prow, int* row_size);
3) 结束扫描(内部释放锁)
int page_scan_end(page_scan_t* pscan);
业务层:
调用者的代码,重新设计为:

int insert_row(...) {    // 计算要写入的行所需的空间大小    int row_size = ....;    // 找到剩余空间满足这个要求的Page    page_header_t* phpage = find_page_for_write(row_size);        // 写入row    int iret = write_row_to_page(phpage, pdata, row_size);        // 释放页面    iret = release_page_for_write(phpage);    return iret;}

 

int scan_row(page_header_t* phpage, filter...) {    page_scan_t scan = page_scan_begin(phpage);        void* prow = NULL;    int row_size = 0;    while (!page_scan_next(&scan, &prow, &row_size)) {        // 应用filter,判断此行是否满足条件        ......    }    page_scan_end(&scan);    return SUCCESS;}

 

我们看到,通过封装,调用者的代码逻辑更加清晰简洁,并且与底层数据结构充分解耦,各自可以独立演化,互相不影响。

因此,大型项目,强烈推荐采用封装的方式进行代码组织设计。
 

转载地址:http://rvlsi.baihongyu.com/

你可能感兴趣的文章
《达芬奇的人生密码》观后感
查看>>
论文翻译:《一个包容性设计的具体例子:聋人导向可访问性》
查看>>
基于“分形”编写的交互应用
查看>>
《融入动画技术的交互应用》主题博文推荐
查看>>
链睿和家乐福合作推出下一代零售业隐私保护技术
查看>>
Unifrax宣布新建SiFAB™生产线
查看>>
艾默生纪念谷轮™在空调和制冷领域的百年创新成就
查看>>
NEXO代币持有者获得20,428,359.89美元股息
查看>>
Piper Sandler为EverArc收购Perimeter Solutions提供咨询服务
查看>>
RMRK筹集600万美元,用于在Polkadot上建立先进的NFT系统标准
查看>>
JavaSE_day12 集合
查看>>
JavaSE_day14 集合中的Map集合_键值映射关系
查看>>
Day_15JavaSE 异常
查看>>
异常 Java学习Day_15
查看>>
JavaSE_day_03 方法
查看>>
day-03JavaSE_循环
查看>>
Mysql初始化的命令
查看>>
day_21_0817_Mysql
查看>>
day-22 mysql_SQL 结构化查询语言
查看>>
MySQL关键字的些许问题
查看>>