在我们平时的开发中也经常会遇到一些简单的项目我们很快就完成了,并且测试着也没有任何问题,但是随着产品迭代,需求增加,项目维护起来越来越耗时,一个看似简单的需求,需要花费较多的时间去开发,出现项目延期。
项目超期的很大一部分原因都是因为代码“牵一发动全身”,当我们的项目需求在累加的过程中,功能相近的应用我们也习惯性的复制,粘贴。导致出现了很多的冗余代码,同时业务逻辑与执行操作相互交织,出现了很多奇奇怪怪的问题。
那么我们该怎么办呢??
可能我们会提到“高内聚,低耦合”
那么什么是高内聚,低耦合??
强内聚和弱耦合是相辅相成的,一个良好的设计是由若干个强内聚模块以弱耦合的方式组装起来的。
说起来好像很高大上,不好理解,其实在很多优秀的开源代码中,都体现出了这种思想。
rt-thread,linux,lwip协议栈,包括我们常用的HAL库等
我们以linux的平台总线模型为例:
可共用的部分(代码逻辑)与差异化的东西(硬件地址)分离。
当然我们平时开发的程序还没有那么庞大,还不用那么复杂,那么想实现类似的结构,基本上都使用了面向对象的思想。面向对象思想的引入可以帮我们有效的避免一些问题。(是部分欧),为了更高质量的代码,后期说一下c语言的设计模式。
C 语言是一门抽象的、面向过程的语言,C 语言广泛应用于底层开发,C 语言在计算机体系中占据着不可替代的作用,可以说 C 语言是编程的基础。
上面说C语言是面向过程的,而C++是面向对象的,然而何为面向对象,什么又是面向过程呢?
不管怎么样,我们最原始的目标只有一个就是实现我们所需要的功能,从这一点说它们是殊途同归的。过程与对象只是侧重点不同而已。
准确的说,面向对象是一种思想,和语言本身无关。
用面向过程的方法写出来的程序是一份蛋炒饭,而用面向对象写出来的程序是一份盖浇饭。
盖浇饭的好处就是”菜”“饭”分离,从而提高了制作盖浇饭的灵活性。饭不满意就换饭,菜不满意换菜。用软件工程的专业术语就是”可维护性“比较好,”饭” 和”菜”的耦合度比较低。
蛋炒饭将”蛋”“饭”搅和在一起,想换”蛋”“饭”中任何一种都很困难,耦合度很高,以至于”可维护性”比较差。
当然面向过程的性能要比面向对象高,对象需要实例化,开销比较大。炒饭还是盖饭,适合的才是最好的。
随着软件需求的变化,我们的代码不断的增加,面向对象的方法,无疑更加容易扩展,及管理维护。
我们知道面向对象的思想主要应用在C++,JAVA等高级语言,它有几个重要的特性。
封装,抽象,继承,多态。
封装性是面向对象编程的三大特性(封装性、继承性、多态性)之一,但也是最重要的特性。封装+抽象相结合就可以对外提供一个低耦合的模块。
数据封装是一种把数据和操作数据的函数捆绑在一起的机制,数据抽象是一种仅向用户暴露接口而把具体的实现细节隐藏起来的机制。
在C语言中,数据封装可以从结构体入手,结构体里可以放数据成员和操作数据的函数指针成员。当然,结构体里也可以只包含着要操作的数据。
下面以一个简单的实例作为演示。
设计一个软件模块,模块中要操作的对象是长方形,需要对外提供的接口有:
1、创建长方形对象;
2、设置长、宽;
3、获取长方形面积;
4、打印长方形的信息(长、宽、高);
5、删除长方形对象。
首先,我们思考一下,我们的接口命名大概是怎样的?其实这是有规律可循的,我们看RT-Thread的面向对象接口是怎么设计的:
我们也模仿这样子的命名形式来给我们这个demo的几个接口命名:
1、rect_create
2、rect_set
3、rect_getArea
4、rect_display
5、rect_delete
我们建立一个rect.h的头文件,在这里声明我们对外提供的几个接口。这时候我们头文件可以设计为:
这样做是没有什么问题的。可是数据隐藏得不够好,我们提供给外部用的东西要尽量简单。
我们可以思考一下,对于C语言的文件操作,C语言库给我们提供怎么样的文件操作接口?如:
FILE *fopen(const char *pathname, const char *mode);
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
我们会创建一个文件句柄(描述符),然后之后只要操作这个文件句柄就可以,我们不用关心FILE具体是怎么实现的。
什么是句柄?看一下百度百科的解释:
我们也可以创建我们的对象句柄,对外提供的头文件中只需暴露我们的对象句柄,不用暴露具体的实现。以上头文件rect.h代码可以修改为:
这里用到了void*,其为无类型指针,void *可以指向任何类型的数据。然后具体要操作怎么样的结构体可以在.c中实现:
在基于对象的编程中,封装性是最基础也最重要的内容。其对象主要包含两方面内容:属性与方法。
在基于C语言的对象编程中,可以使用句柄来表示对象,即句柄指向的数据结构的成员代表对象的属性,实际操作句柄的函数则表示对象的方法。
继承简单说来就是父亲有的东西,孩子可以继承过来。
当创建一个类时,我们不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。
这个已有的类称为基类,新建的类称为派生类。
继承在C++ 中还会细分为很多,我们就不考虑那么多了,只分享比较简单也比较实用的。
在C语言对象编程中,有两种方法实现继承:
第一种是:结构体包含结构体实现继承。
第二种是:利用私有指针实现继承。
下面依旧以实例进行分享:
假如我们要操作的对象变为长方体,长方体就可以继承长方形的数据成员和函数,这样就可以复用之前的一些代码。具体操作看代码:
可见,长方体结构体可以继承长方形结构体的数据、长方体对象相关操作也可以继承长方形对象的相关操作。这样可以就可以复关于长方形对象操作的一些代码,提高了代码复用率。
在结构体内部增加一个私有指针成员,这个私有成员可以达到扩展属性的作用,比如以上的Rect结构体设计为:
typedef struct _Rect
{
char *object_name;
int length;
int width;
void* private;
}Rect, *pRect;
这个private指针可以在创建对象的时候与其它拓展属性做绑定。比如:
想要拓展的数据为:
带拓展属性的对象创建函数:
显然,使用私有指针也是可以实现继承的一种方式。
多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。
多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。
C++中常使用虚函数实现多态。先定义一个弱的/虚的函数,其它地方再定义同名的真的函数,实际用的是真的函数。
首先,我们可以使用函数指针来模拟C++的虚函数表:
* 模拟C++的虚函数表 */
typedef struct _Ops
{
int (*area)(void);
}Ops;
/* 基类 */
typedef struct _Shape
{
Ops ops;
int width;
int height;
}Shape;
/* 真实函数*/
int rectangle_area(void)
{
printf("Rectangle class area\n");
}
/* Triangle的area函数 */
int triangle_area(void)
{
printf("Triangle class area\n");
}
/* 主函数 */
int main(void)
{
Rectangle rectangle;
memset(&rectangle, 0, sizeof(Rectangle));
rectangle.shape.ops.area = rectangle_area; /* 与自己的area函数做绑定 */
Triangle triangle;
memset(&triangle, 0, sizeof(Triangle));
triangle.shape.ops.area = triangle_area; /* 与自己的area函数做绑定 */
Shape *shape;
shape = (Shape*)&rectangle;
shape->ops.area();
shape = (Shape*)▵
shape->ops.area();
return 0;
}
父类指针shape来操作两个子类时,使用相同的接口时调用了不同的函数:
因为这里只有一个操作函数,所以就没有建立一个函数表来包装一层了。我们可以再加一个函数表,如:
C语言并不是面向对象的语言,要想完全实现与C++一样的一些面向对象的特性会比较难,但是在嵌入式开发过程中,C语言又应用广泛,而在大型项目中,一个好的软件框架可以帮助我们更有效的开发,所以面对对象的思想就显得极其重要了。
上节说了一些基本的概念和举例,但是还是不好理解??
其实我们在平时开发中也遇到很多这种应用。
面向过程
面向对象:
封装的体现: