面向对象编程(OOP)实战论
前言
本文将通过一个实际的案例,通过需求分析、代码编写、OOP 优化的方式来阐述 OOP 在软件开发中的实际用处
本文案例将使用 C 语言进行编写,以此表明 OOP 是一种泛用的编程思想,并不是只能应用在 C++/python 之类的面向对象语言上。Linux 内核就用到了大量的面向对象的思想,但它是完全使用 C 语言编写的
本文编写时尽可能考虑到了无面向对象基础的读者,但推荐无基础读者在观看本文之前先看 面向对象编程(OOP)的C语言实现
需求分析
自动轮显模式需求
自动轮显:电能表上电默认显示方式为自动循环显示模式,表计在运行超过轮显周期后自动切换到下一屏的显示,如当前只设置了一个轮显项,则不切换。
1
2
3
4
5
6
7
8
9
10
11
12
13
// 无封装实现
uint8_t autoScrollMode_scrollIntervals; // 轮显周期(秒)
uint8_t autoScrollMode_page; // 当前屏号
uint8_t autoScrollMode_maxPage; // 最大屏数
void autoScrollMode_nextPage()
{
autoScrollMode_page++;
if(autoScrollMode_page >= autoScrollMode_maxPage)
{
autoScrollMode_page = 0;
}
return;
}
至此,我们完成了该需求的一种实现方式
使用封装优化
但是可以发现这些变量名称比较长,不直观,易出错,我们可以考虑将它们封装一下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef struct {
uint8_t scrollIntervals; // 轮显周期(秒)
uint8_t page; // 当前屏号
uint8_t maxPage;
} AutoScrollMode;
// 每次加 1,溢出时归 0,0 表示第一屏
void AutoScrollMode_nextPage(AutoScrollMode* this)
{
this->page++;
if(this->page >= this->maxPage)
{
this->page = 0;
}
return;
}
// 使用时
AutoScrollMode autoScrollModeObject;
AutoScrollMode_nextPage(&autoScrollModeObject);
通过该例,可以引申出 2 个基本概念:
- 类(Class):定义了一件事物的抽象特点。类的定义包含了数据的形式以及对数据的操作
- 对象(Object):是类的实例(Instance)
现在介绍面向对象的第一个关键概念:
- 封装:将彼此之间有联系的变量和函数放在一起
比如你去餐馆吃饭,老板给你介绍他们家的招牌菜,用的是什么食材、工艺、调料等等细节,描绘的栩栩如生。但是光听描述可填不饱肚子,就让老板赶紧把菜做出来。老板对菜的介绍就是类,是一种抽象的概念,用来描述一个事物(对应了程序中不占用内存的概念),根据描述做出来的菜,就是对象,可以真正用来吃的。而且菜可以做多份,服务给各个不同的客人,对应了对象可以有多个,每个可以用于不同目的。
使用 C++ 实现的封装:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct AutoScrollMode {
uint8_t scrollIntervals; // 轮显周期(秒)
uint8_t page; // 当前屏号
uint8_t maxPage;
void nextPage();
};
AutoScrollMode::nextPage()
{
page++;
if(page >= maxPage)
{
page = 0;
}
return;
}
c语言可以实现类似的语法,但要做绑定,比较麻烦:
1
2
3
4
5
6
7
8
9
typedef struct {
uint8_t scrollIntervals; // 轮显周期(秒)
uint8_t page; // 当前屏号
uint8_t maxPage;
void (*nextPage)(AutoScrollMode*);
} AutoScrollMode;
AutoScrollMode autoScrollMode{.nextPage = AutoScrollMode_nextPage};
C++ 中的类的成员函数默认包含一个
this 指针
,函数实现中的类成员变量也默认有this->
的引用。C 语言完全可以实现面向对象,只是有些不太方便的地方,C++ 就是为了解决这个问题的。
按键轮显模式需求
键显模式: 从第一个键显项开始显示。键显模式下不自动切换显示项,此时按显示按键,在键显项中按顺序切换显示项。超过键显时间(可通信配置,单位为秒)未按键,电能表切换到自动轮显模式,并从第一个轮显项开始显示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct{
uint8_t timeout; // 超时时间(秒)
uint8_t page; // 当前屏号
uint8_t maxPage;
} KeyScrollMode;
// 每次加 1,溢出时归 0,0 表示第一屏
void KeyScrollMode_nextPage(KeyScrollMode* this)
{
this->page++;
if(this->page >= this->maxPage)
{
this->page = 0;
}
return;
}
对对象进行操作,完成需求:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
AutoScrollMode autoScrollMode;
KeyScrollMode keyScrollMode;
int main()
{
void* currMode = &autoScrollMode; // 当前模式
// 系统参数
uint32_t now;
bool keyPressed;
uint8_t idleTime;
while(true)
{
if(currMode == &autoScrollMode) // 如果当前是AutoScrollMode
{
if(now % autoScrollMode.scrollIntervals == 0)
{
// 翻页间隔时间到达
AutoScrollMode_nextPage(&autoScrollMode);
}
}
else if(currMode == &keyScrollMode) // 如果当前是KeyScrollMode
{
if(keyPressed == true) // 按键被按下
{
KeyScrollMode_nextPage(&keyScrollMode);
keyPressed = false;
idleTime = 0;
}
else
{
idleTime++;
// 超时时间到达,返回autoScrollMode
if(idleTime > keyScrollMode.timeout)
{
currMode = &autoScrollMode;
}
}
}
}
}
可以发现代码中有很多的重复:page、maxPage 两个成员变量是重复的,nextPage 函数的实现也是重复的。
使用继承优化
为了解决代码重复问题,我们对相同点做抽象:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typedef struct {
uint8_t page;
uint8_t maxPage;
} ScrollMode;
void ScrollMode_nextPage(ScrollMode* this)
{
this->page++;
if(this->page >= this->maxPage)
{
this->page = 0;
}
return;
}
typedef struct {
ScrollMode super;
uint8_t scrollIntervals;
} AutoScrollMode;
typedef struct {
ScrollMode super;
uint8_t timeout;
} KeyScrollMode;
main 函数也做相应修改:
1
2
3
4
5
- AutoScrollMode_nextPage(&autoScrollMode);
+ ScrollMode_nextPage(&autoScrollMode.super);
- KeyScrollMode_nextPage(&keyScrollMode);
+ ScrollMode_nextPage(&keyScrollMode.super);
从该实例中也能发现封装是继承的基础。
需求扩展
新增一个类型“低功耗按键显示模式”,要求每隔一定时间自动翻页,且超过一定时间后能自动退出该模式:
1
2
3
4
5
typedef struct {
ScrollMode super;
uint8_t timeout; // 超时时间(秒)
uint8_t scrollIntervals; // 轮显周期(秒)
} LowPowerAutoScrollMode;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
LowPowerAutoScrollMode lowPowerAutoScrollMode;
else if(currMode == &lowPowerAutoScrollMode) // 如果当前是KeyScrollMode
{
if(now % lowPowerAutoScrollMode.scrollIntervals == 0)
{
// 翻页间隔时间到达
ScrollMode_nextPage((ScrollMode*)currMode);
}
idleTime++;
// 超时时间到达,返回autoScrollMode
if(idleTime > lowPowerAutoScrollMode.timeout)
{
currMode = &autoScrollMode;
}
}
多态的应用
现在我们添加显示页面的逻辑,每个模式要显示的内容不同,键显模式需要显示当前页号,单独写出它们的实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 自动滚动显示模式获取显示项
void AutoScrollMode_displayPage(AutoScrollMode* this)
{
static const char* displayList[] = {
"autoScrollModePage1",
"autoScrollModePage2",
"autoScrollModePage3",
// ...
};
printf("%s\n", displayList[this->super.page]);
return;
}
// 按键滚动显示模式获取显示项
void KeyScrollMode_displayPage(KeyScrollMode* this)
{
static const char* displayList[] = {
"keyScrollModePage1",
"keyScrollModePage2",
"keyScrollModePage3",
"keyScrollModePage4",
// ...
};
printf("%d:%s\n", this->super.page, displayList[this->super.page]);
return;
}
使用时:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void display(void* currMode)
{
if(currMode == &autoScrollMode)
{
AutoScrollMode_displayPage((AutoScrollMode*)currMode);
}
else if(currMode == &keyScrollMode)
{
KeyScrollMode_displayPage((KeyScrollMode*)currMode);
}
}
int main(
while(true)
{
// ...
// 屏幕每两秒刷新一次
if(now % 2 == 0)
{
display(currMode);
}
}
)
还能不能使用继承呢?不行,它们相似之处比较少,难以进行抽象。所以我们使用面向对象中最重要的概念:多态。首先在父类中创建一个虚函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct ScrollMode {
uint8_t page;
uint8_t maxPage;
void (*displayPage)(ScrollMode*); // 虚函数(virtual function)
};
// 无论传进来的currMode是哪个对象,都会调用其对应的displayPage的实现
void display(void* currMode)
{
((ScrollMode*)currMode)->displayPage((ScrollMode*)currMode);
return;
}
int main()
{
// 绑定虚函数
autoScrollMode.super.displayPage = AutoScrollMode_displayPage;
keyScrollMode.super.displayPage = KeyScrollMode_displayPage;
// ...
}
这样做的好处是,display 函数更加简洁,且后续新增显示模式时,display 函数都无需改动。
C 语言对多态的实现比较繁琐,每次定义一个对象都要重新绑定其虚函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
struct ScrollMode {
uint8_t page;
uint8_t maxPage;
virtual void displayPage(); // virtual关键字修饰虚函数
};
struct AutoScrollMode: public ScrollMode{
// ...
virtual void displayPage(){
// ...
return;
}
};
struct KeyScrollMode: public ScrollMode{
// ...
virtual void displayPage(){
// ...
return;
}
};
AutoScrollMode autoScrollMode;
KeyScrollMode keyScrollMode;
int main()
{
// 无需手动绑定
// autoScrollMode.super.getAttribute = AutoScrollMode_getAttribute;
// keyScrollMode.super.getAttribute = KeyScrollMode_getAttribute;
}
void display(void* currMode)
{
((ScrollMode*)currMode)->displayPage();
return;
}
继续扩展需求
继续完成“低功耗按键显示模式”需求,所要做的很简单,只是添加其对应的 displayPage()
虚函数的实现即可:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 按键滚动显示模式获取显示项
void LowPowerAutoScrollMode_displayPage(LowPowerAutoScrollMode* this)
{
static const char* displayList[] = {
"lowPowerAutoScrollModePage1",
"lowPowerAutoScrollModePage2",
// ...
};
printf("%d:%s\n",this->super.page, displayList[this->super.page]);
return;
}
int main()
{
// ...
lowPowerAutoScrollMode.super.displayPage = LowPowerAutoScrollMode_displayPage;
// ...
}
至此,我们使用面向对象的封装、继承、多态完成了滚动显示的需求的实现。
总结
面向对象是一种编程思想,用来解决代码重复和类型扩展难的问题。想要理解面向对象需要较多的实践,本教程旨在帮助大家初步理解面向对象的思路。当在开发中遇到重复代码或结构复杂的情况时,希望大家能想到:是否可以通过封装、继承、多态来让代码更清晰、更可维护?