设计模式的分类
设计模式如下分为创建型模式,结构型模式,行为型模式。
创建型模式
1、抽象工厂模式(Abstract Factory)
2、建造者模式(Builder)
3、工厂方法模式(Factory Method)
4、原型模式(Prototype)
5、单例模式(Singleton)
结构型模式
1、代理模式(Proxy)
2、桥接模式(Bridge)
3、组合模式(Composite)
4、装饰者模式(Decorator)
5、外观模式(Facade)
6、适配器模式(Adapter)
7、享元模式(Flyweight))
行为型模式
1、职责链模式(Chain of Responsibility)
3、解释器模式(Interpreter)
4、迭代器模式(Iterator)
5、中介者模式(Mediator)
6、访问者模式(Visitor)
7、观察者模式(Observer)
8、状态模式(State)
9、策略模式(Strategy)
10、模板方法模式(Template Method)
11、备忘录模式(Memento)
设计模式最开始是针对面向对象语言提出的。经典的书如《设计模式:可复用面向对象软件的基础》,《java与模式》。《设计模式》的作者俗称”GOF”, gang of four,书中从面向对象的设计中精选出23个设计模式,这个也成了后来设计模式的范例。
C语言设计模式的书非常少,目前没有看到合适的。有一本《C嵌入式编程设计模式》,作者douglass。书一共6章,里面没有太多高含金量的内容。书的主要问题在于没有真正把设计模式应用到C,而是把一些基本的嵌入式开发注意点包装成模式,中断算一种设计模式,轮询算一种设计模式,互斥算一种模式。照这个套路,C函数指针使用肯定也算一种模式,强制类型转换肯定也得算。我觉得是为了出书而滥造模式。这本书就状态机模式描述比较多些。这人还合写了另外一本书叫《Real-Time Design Patterns》
本专题尝试简单讲解设计模式的思路,在嵌入式中的应用和样例代码入手,特别是注意设计模式在linux内核和实际开发中的应用,总结出真正在C语言和嵌入式开发中有生命力的少数几个模式。C语言实现设计模式的几个利器有结构体,函数指针,利用数组实现多态。
样例代码全部以C代码实现,可能会穿插讲解java样例以方便对比为什么C实现模式演化成这个样子以及和面向对象语言的差异。
C语言应用设计模式的误区
在C语言开发中应用设计模式有几种现象,分析如下
用C模拟面向对象
用C模拟面向对象,用结构体模拟类,用结构体包含看作继承。这种做法在特定需要的场合少量使用尚可,但是如果为了模拟面向对象的特性就南辕北辙了。
用很多特定的宏来将C在形式上封装为C++
用C模仿面向对象和设计模式有另外一种流派,就是用很多特定的宏来封装。我非常反对这种用法,因为基本上对于程序员而言,已经对C语言的阅读和书写造成了严重的干扰,不能为了面向对象而面向对象,C语言自然有其简洁高效的一面。
盲目追求设计模式
随着设计模式的思想的普及,很多设计师对设计模式盲目崇拜和过分追求,为了用模式而用模式,削足适履。对于嵌入式开发常用的C语言而言,少了很多面向对象的特点,经典的23个设计模式肯定不会都适用,而且在实际工作中,其实也不会为了模式而模式。而且从技术上讲23个设计模式当时是围绕面向对象提出的,有的模式偏向于逻辑,那么可以被C借鉴,有的是为了解决面向对象本身的集成,关联等问题,那就没有必要借鉴。
非典型模式章节开始介绍23种设计模式里在C语言退化的,不适用的。这些模式在使用中很难想到其实也是一种设计模式,代码的实现也没有什么特点。开发人员在设计时并不需要特意考虑这些模式,随遇而安即可。
了解这些模式,只是加强一下开发人员的全局观,在开发中能润物无声的应用和衍生。
原型模式(Prototype) 介绍
某些对象的结构比较复杂,但是我们又需要频繁的使用它们。通过复制这些原型创建新的对象。
比如写一个内核发送大量数据包的程序,比较好的办法就是做好一个标准数据包,然后不停的复制,再发送出去。
深复制和浅复制
熟悉内核网络实现的人一定知道,如果按照复制数据包的做法,最好的办法是使用skb_clone函数而不是skb_copy函数。原因是,内核里表示数据包由skb和data部分组成。Skb只是管理的句柄,而data是数据包的真实数据。如果是同样的数据包发送,只需要复制句柄,保持对data的引用就可以。这个就是浅复制。
图表 1浅复制skb_clone
可以看出,浅复制skb_clone只是复制了sk_buff句柄,而下面的packet data storage指向的是同一个。
如果发送数据包要按照顺序变换数据包IP头的序列号和校验码。那么就需要同时复制句柄和data作为新的数据包,使用skb_copy函数。
图表 2深复制skb_copy
可以看出,深复制skb_copy同时复制了sk_buff句柄和下面的packet data storage。
单例模式(Singleton) 介绍
单例模式就是确保某一个类只有一个实例,并且提供一个全局访问点。在C语言的实现中,最常见的使用方法有且仅有一个:作为全局变量存在的控制数据。在嵌入式驱动开发中经常使用,比如driver定义,device定义。例子如下。
static struct platform_driver dwc3_exynos_driver = {
.probe = dwc3_exynos_probe,
.remove = __devexit_p(dwc3_exynos_remove),
.driver = {
.name = "exynos-dwc3",
},
};
在ISP里用一个全局变量保存camera的属性,都可以算是单例模式。
但是C语言里,由于这种方法太常见,除了一个全局变量,也没有别的代码,所以从来没有人认为这是一个设计模式。
组合模式(Composite)
组合模式也称为合成模式,有时候又成为部分-整体(part-whole)模式。
先看看在面向对象里的定义。组合模式将对象组织到树结构里,可以用来描述整体和部分的关联。合成模式可以使客户端将单纯元素和符合元素同等看到。
用C语言翻译上面的话,就是把数据按照树结构组织起来,访问的时候能将叶子节点和中间节点同等处理,用的是递归的方法。
剩下的,大家参考数据结构的书吧。在C里面,这东西算数据结构,不算设计模式。
享元模式(Flyweight)
享元模式以共享的方式支持大量的细粒度对象。享元模式把对象属性分为内部状态和外部状态。内部状态是对象本质属性,不可改变。外部的可以随着环境改变。
享元模式最常见的在编辑器的实现里。如字母a,内部状态就是a本身,外部状态是位置,字体。这样就能共享一个a对象,在编辑器里实现不同的表现。
在C里,并没有发现这个模式有什么应用的场景。如果有人知道,请留言。
工厂模式和抽象工厂模式(Factory)
工厂方法模式定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个,也就是说工厂方法模式让实例化推迟到子类。
对于C开发者,工厂模式更通俗一点理解就是,客户并不直接malloc一个对象A或者B,而是调用一个工厂函数,输入A或者B的类型。由工厂函数返回对象A或者B
对于C语言,没有类的约束,创建对象也不过是一个malloc。靠输入不同的参数决定产生不同的对象,谈不上模式。实际应用也很少。
抽象工厂模式用来成体系的对象,C语言也用不上。
工厂模式样例代码
cap_factory生产不同的帽子
#define RED_TYPE 0x01
#define BLUE_TYPE 0x02
struct cap
{
int type;
void (*process_cap)(struct cap*);
};
void process_red_cap(struct cap* pcap)
{
printf("This is a red cap!\n");
}
void process_blue_cap(struct cap* pcap)
{
printf("This is a blue cap!\n");
}
struct cap* cap_factory(int type)
{
struct cap* pcap = (struct cap*)malloc(sizeof(struct cap));
memset(pcap, 0, sizeof(struct cap));
if(RED_TYPE == type)
{
pcap->type == RED_TYPE;
pcap->process_cap = process_red_cap;
}
else
{
pcap->type == BLUE_TYPE;
pcap->process_cap = process_blue_cap;
}
return pcap;
}
解释器模式(Interpreter)介绍
解释器模式就是定义语言的语法,并且建立一个解释器来解释该语言中的句子。比如用C写一个xml语言的解析器,这个开发就算是解释器模式。显然,C里面这个算是一个开发项目,不是设计模式。
另外一个常见的使用场景是C语言学习课程里,编写一个计算器。需要将用户输入的算式的字符串解析并表达出来。但是编程和语法上也没有什么特殊的。
在正常项目里也许有少量的地方根据字符串的语法含义做一些处理,但是场景非常少,用有意义的字符串做为控制参数显然不如用整数或者结构体。
迭代器模式(Iterator) 介绍
迭代器模式是将迭代元素的责任交给迭代器,而不是对象,可以在不需要知道该聚合对象的内部结构就可以实现该聚合对象的迭代。
比如一组元素,可能是链表组成的,可能是树状结构。可以写一个迭代器函数,屏蔽具体元素组织结构的差异,遍历全部的元素,那么就算迭代器模式。显然,C里面并不太需要这种方式。一个元素的组织结构,是在设计时综合考虑效率,内存空间,场景,就已经确定了。如果一定要实现迭代器,最多也就是封装一个函数。在C实现里,这种也不算是设计模式,需求也不明显。
备忘录模式(Memento)介绍
备忘录模式又叫做快照模式(shapshot)。在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。
在vmware虚拟机上用过linux的人一定对快照功能非常清楚。在android之前,一般的嵌入式linux的代码都不多,在性能强的vmware虚拟机上安装linux编译,调试就能完成绝大部分任务。对于写内核程序的开发者而言,可能一点错误就导致内核崩溃,需要重启。所以一般会在调试之前用vmware保存一个snapshot。崩溃之后不用重启,直接恢复快照(snapshot)就原地满血复活, 回到快照那一刻的状态。
对于C语言,如果有的过程数据需要保留,那么小的用临时变量,大的可以分配一片内存。一般规模和需求都很小。也没有一个特别的操作可以称为模式。这种使用只在低功耗,寄存器保存恢复时使用,但是实现上没有任何特别的代码,都是整片的赋值。由于寄存器的特殊性,基本上不用memcpy,曾经发生过bug,因为嵌入式系统下memcpy未必是按照通常理解的word赋值,而是替换库文件的memcpy函数改写之后的。
策略模式(Strategy)介绍
开发中可以通过很多种不同的方式来完成一件事情,这里的每一种方式都可以称作为一种策略。比如计算ISP的自动曝光,有两种不同的算法,也可以称为不同的策略。策略模式就是可以算法和策略但是外界不需要修改的设计模式。很显然,只要算法和其他组件间的接口保持一致就可以。替换自动曝光算法,外部不修改代码就可以运行。
再比如,有一个算法比较复杂,还在开发的过程中,主流程里很可能用相同的接口进行打桩。桩和实际算法在运行时可以动态用函数指针动态配置,那么这个不算通常意义的算法更替,但也算是用到的策略模式的思想。但是很显然,对于C语言,这种肯定不算是一种明显的设计模式,而是一种函数或者功能替换,也没有特别的语法和架构支持,最多也就是使用函数指针方便替换策略。
很多帖子和书都拿替换排序算法为例,
中介者模式(Mediator)介绍
中介者模式也翻译成调停者模式,就是用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用。
图表 1 相互引用的模型
图表 2中介者模式
在C里面,这个也很难算是一种设计模式,只能算是一种思路吧。中介者模式和门面模式看起来有点像,但差别还是很明显的。门面模式只是为了封装系统内细节,对外提供套餐服务。而中介者模式并不提供套餐,而是隔离,协调各对象的行为。现实中,项目经理就担任着中介者的角色。
桥接模式(Bridge)介绍
如果某个系统能够从多个角度来进行分类,且每一种分类都可能会变化,那么我们需要做的就是讲这多个角度分离出来,使得他们能独立变化。桥接模式将继承关系转化成关联关系。
更文艺的说法是,将”抽象化和实现化解耦”。 抽象化在面向对象就是将对象共同的性质抽取出去而形成类的过程。桥接模式中的所谓脱耦,就是指在一个软件系统的抽象化和实现化之间使用关联关系(组合或者聚合关系)而不是继承关系,从而使两者可以相对独立地变化,这就是桥接模式的用意。
Java与模式书上举了飞机的例子。飞机有两个属性,制造商(波音和空客)和用途(运输机和客机)。书上把飞机作为抽象类,制造商作为具体实现类。不过如果再增加一个旋翼和固定翼的属性呢?桥梁模式没法解决超过2个维度的分类集成问题。
C就没有真正意义上的继承,所以我觉得,纯粹用面向对象继承改关联的思路看桥接模式比较局限,C不存在这个模式的需求。如果有不同的因素,那多维数组是一个选择。