目录
- 导言
【摘要】软件编写模式是开发过程中的重要经验总结。灵活运用设计模式,一方面利于我们编写高质量的代码,另一方面也方便我们对代码进行维护。
软件编写模式是开发过程中的重要经验总结。灵活运用设计模式,一方面利于我们编写高质量的代码,另一方面也方便我们对代码进行维护。
设计模式最开始是针对面向对象语言提出的。经典的书如《设计模式:可复用面向对象软件的基础》,《java与模式》。《设计模式》的作者俗称”GOF”,gangoffour,书中从面向对象的设计中精选出23个设计模式,这个也成了后来设计模式的范例。
C语言设计模式的书非常少,目前没有看到合适的。有一本《C嵌入式编程设计模式》,作者douglass。书一共6章,里面没有太多高含金量的内容。书的主要问题在于没有真正把设计模式应用到C,而是把一些基本的嵌入式开发注意点包装成模式,中断算一种设计模式,轮询算一种设计模式,互斥算一种模式。照这个套路,C函数指针使用肯定也算一种模式,强制类型转换肯定也得算。我觉得是为了出书而滥造模式。这本书就状态机模式描述比较多些。这人还合写了另外一本书叫《Real-TimeDesignPatterns》,我也感觉内容非常稀,那些内容根本不能称为设计模式(DesignPatterns)。我以后不会看他的书了。
本专题尝试简单讲解设计模式的思路,在嵌入式中的应用和样例代码入手,特别是注意设计模式在linux内核和实际开发中的应用,总结出真正在C语言和嵌入式开发中有生命力的少数几个模式。C语言实现设计模式的几个利器有结构体,函数指针,利用数组实现多态。
本专题讲解模式会按照如下格式:
u 模式的介绍
u 模式的应用场景
u 样例代码
u 模式总结
样例代码全部以C代码实现,可能会穿插讲解java样例以方便对比为什么C实现模式演化成这个样子以及和面向对象语言的差异。
1.1 C语言应用设计模式的误区
在C语言开发中应用设计模式有几种现象,分析如下
1.1.1 用C模拟面向对象
用C模拟面向对象,用结构体模拟类,用结构体包含看作继承。这种做法在特定需要的场合少量使用尚可,但是如果为了模拟面向对象的特性就南辕北辙了。
1.1.2 用很多特定的宏来将C在形式上封装为C++
用C模仿面向对象和设计模式有另外一种流派,就是用很多特定的宏来封装。我非常反对这种用法,因为基本上对于程序员而言,已经对C语言的阅读和书写造成了严重的干扰,不能为了面向对象而面向对象,C语言自然有其简洁高效的一面。
1.1.3 盲目追求设计模式
随着设计模式的思想的普及,很多设计师对设计模式盲目崇拜和过分追求,为了用模式而用模式,削足适履。对于嵌入式开发常用的C语言而言,少了很多面向对象的特点,经典的23个设计模式肯定不会都适用,而且在实际工作中,其实也不会为了模式而模式。而且从技术上讲23个设计模式当时是围绕面向对象提出的,有的模式偏向于逻辑,那么可以被C借鉴,有的是为了解决面向对象本身的集成,关联等问题,那就没有必要借鉴。
- 各章节链接
1.3 设计模式的分类
设计模式如下分为创建型模式,结构型模式,行为型模式。
创建型模式
1、抽象工厂模式(AbstractFactory)
2、建造者模式(Builder)
3、工厂方法模式(FactoryMethod)
4、原型模式(Prototype)
5、单例模式(Singleton)
结构型模式
1、适配器模式(Adapter)
2、桥接模式(Bridge)
3、组合模式(Composite)
4、装饰者模式(Decorator)
5、外观模式(Facade)
6、享元模式(Flyweight)
7、代理模式(Proxy)
行为型模式
1、职责链模式(ChainofResponsibility)
2、命令模式(Command)
3、解释器模式(Interpreter)
4、迭代器模式(Iterator)
5、中介者模式(Mediator)
6、备忘录模式(Memento)
7、观察者模式(Observer)
8、状态模式(State)
9、策略模式(Strategy)
10、模板方法模式(TemplateMethod)
11、访问者模式(Visitor)
- 建造者模式
【摘要】模式介绍建造者模式将复杂产品的构建过程封装分解在不同的方法中,使得创建过程非常清晰。它隔离了复杂产品对象的创建和使用,使得相同的创建过程能够创建不同的产品。若几个产品之间存在较大的差异,则不适用建造者模式面向对象里的建造者模式,对于C语言,就无需这么复杂了。比如用C构建一个网络数据包,需要构建Dmac域,smac域,长度域,IP等各
2.1 模式介绍
建造者模式将复杂产品的构建过程封装分解在不同的方法中,使得创建过程非常清晰。它隔离了复杂产品对象的创建和使用,使得相同的创建过程能够创建不同的产品。若几个产品之间存在较大的差异,则不适用建造者模式。
面向对象里的建造者模式,对于C语言,就无需这么复杂了。
比如用C构建一个网络数据包,需要构建Dmac域,smac域,长度域,IP等各层头。如果代码写在一个函数里,那么会很长很复杂。可以把Dmac域,smac域,长度域合并到二层头的构建函数,ip的各个域写到一个函数。
2.2 建造者模式实现
2.2.1 原始函数样例
structpacket
{
intpart_a[4];
intpart_b[4];
intpart_c[4];
}
voidoriginal_func(structpacket*pkt)
{
pkt->part_a[0]=1;
pkt->part_a[1]=3;
pkt->part_a[2]=4;
pkt->part_a[3]=7;
pkt->part_b[0]=3;
pkt->part_b[1]=5;
pkt->part_b[2]=7;
pkt->part_b[3]=9;
pkt->part_c[0]=4;
pkt->part_c[1]=5;
pkt->part_c[2]=1;
pkt->part_c[3]=2;
}
2.2.2 建造者模式函数样例
下面的例子很简单,但是实际上用建造者模式是因为各部分的建造函数可以复用,建造出某类型产品的不同的具体实例,同时有利于模块化,避免过长的函数。
voidbuilder_parta(structpacket*pkt)
{
pkt->part_a[0]=1;
pkt->part_a[1]=3;
pkt->part_a[2]=4;
pkt->part_a[3]=7;
}
voidbuilder_partb(structpacket*pkt)
{
pkt->part_b[0]=3;
pkt->part_b[1]=5;
pkt->part_b[2]=7;
pkt->part_b[3]=9;
}
voidbuilder_partc(structpacket*pkt)
{
pkt->part_c[0]=4;
pkt->part_c[1]=5;
pkt->part_c[2]=1;
pkt->part_c[3]=2;
}
voidbuilder_func(structpacket*pkt)
{
builder_parta(pkt);
builder_partb(pkt);
builder_partc(pkt);
}
2.3 模板方法模式(TemplateMethod)介绍
如果做某几件事情的主要方法都差不多,仅有小部分的不同,那么相同的部分可以提取出来成为父类,不同的部分可以做成不同的子类。这种思路叫做模板方法模式。
图表1建造者模式和模板方法模式对比
和建造者模式切分构建和流程方法类似,模板方法模式首先也要对方法进行切分。建造者模式切分的每个部分都是一个没有继承关系的类,组合起来作为builder类。而模板方法模式的父类实现了相同部分的方法,而子类扩展实现不同的方法。模板方法模式的子类包含了整套的方法。
对于C语言,由于不存在继承,所以建造者方法和模板方法模式就可以混用。每一个part可以成为函数,组合起来成为builder函数,而替换不同的part,可以变成不同的builder。
2.4 模式实现总结
对于C语言开发者来说,通常是在构造复杂的数据结构时候会想到建造者模式。比如核间通信消息,进程间通信消息。ISP里面的request消息,就隐性用了建造者模式。
自行编写伪数据包发送代码也非常适合用建造者模式。把数据包的不同层的头部信息用不同的函数进行构造。
- 适配系列模式
【摘要】模式介绍:适配系列模式在《设计模式》中提出的23种模式,其中适配器模式(Adapter),装饰者模式(Decorator),代理模式(Proxy)都属于原始功能到目标功能之间的桥梁。在面向对象里的设计里,这3种由于类的继承等面向对象特性,有比较明显的不同。在C语言里这些区别明显减弱,而且在实际的开发中,也没有这么多约束,所以统称为适配系列模式。以下引用设计模式的一些。
3.1 模式介绍
在《设计模式》中提出的23种模式,其中适配器模式(Adapter),装饰者模式(Decorator),代理模式(Proxy)都属于原始功能到目标功能之间的桥梁。
在面向对象里的设计里,这3种由于类的继承等面向对象特性,有比较明显的不同。在C语言里这些区别明显减弱,而且在实际的开发中,也没有这么多约束,所以统称为适配系列模式。
以下引用设计模式的一些定义和说明。
3.1.1 适配器模式Adapter
将两个不同接口的类来进行通信,在不修改这两个类的前提下用中间件来完成这个衔接的过程。这个中间件就是适配器。所谓适配器模式就是将一个类的接口,转换成客户期望的另一个接口。
3.1.2 装饰者模式Decorator
如果通过继承和组合的方式来给一个对象添加行为,会造成关系复杂,过多的功能造成类爆炸。装饰者模式动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更加有弹性的替代方案。
3.1.3 代理模式Proxy
代理对象可以在客户端和目标对象之间起到中介的作用,这样起到了的作用和保护了目标对象的,同时也在一定程度上面减少了系统的耦合度。
3.1.4 共同点和区别
这3个模式听起来都很类似,都是在原对象上增加或者强化一些功能。按面向对象里的说法,代理模式和被代理的对象拥有完全相同的接口,不增加和强化功能。比如给一个取火车票的函数写一个代理函数,虽然接口一样,但是增加了取票人合法性,票的合理性等功能。把这个称为不增加和强化,我认为都是语义上的,而不是技术上的。
而且由于与C语言不是类继承,接口是可以改的。比如取***函数增加一个时间入参功能,判断是不是合理的取票时间,如果根据接口改变就称这个不是代理模式了,那设计模式这个语义也没有什么意义了。从内核实现的观察者模式实现就知道,C语言里是灵活的,不因为改变了某些不影响架构的细节就换了一个模式。
装饰模式不改变接口,但是会强化一些离原功能远一些的功能。但是同样由于C语言的灵活性,多远算远,怎么样才算强化都是难以量化的,就算改了一个入参强化,也不能说这个和装饰模式差别很大,况且由于C语言不存在类继承保持父类接口这回事,所以装饰模式也没有什么特殊的。
适配器模式将两个不同接口的类来进行通信,也就是说,接口是不一致的,同上面的分析,在C语言里没有什么实质性区别,只不过对原接口改变多少的程度有差异而已。
3.2 适配系列模式实现
没什么特别的,在C里面,适配系列模式是一种隐性的模式,最常见的称呼是封装下接口,开发这就在不知不觉中使用了适配系列模式。下面举一个简单的例子。
3.2.1 原函数样例
也就是被适配和代理的函数
intoriginal_process(inta,intb)
{
//dosomething
ret=//xxx
returnret;
}
3.2.2 传统适配模式函数样例
可以看出传统适配模式将(inta,intb)接口适配成了(intc,intd,inte),并且返回值也做了适配。
intadapt_process(intc,intd,inte)
{
inta,b;
intret;
//dosomething
a=(c+d)%e;//someformulaincludesc,d,eorsomeotheroperation
b=d+e;
ret=original_process(a,b)+1;
//returnsomethingaccroidingtoret
returnret;
}
3.2.3 传统装饰者模式函数样例
传统意义上的装饰者模式不会改变接口,但是会增加功能。
intdecorator_process(inta,intb)
{
inta1,b1;
//dosomethingoptimizationorotherfunction
a1=//dosomethingwithaorb
b1=//dosomethingwithaorb
ret=original_process(a1,b1)
//dosomethingmore,likeoptimize
//returnsomethingaccroidingtoret
}
3.2.4 传统代理模式函数样例
传统代理模式既不能改变接口,也不能增加功能,通常只能做一些管控。
intproxy_process(inta,intb)
{
intret;
//checka,b
if(a<5)
{printf("error:ashouldbotlessthan5\n");}
if(b<0)
{printf("error:bshould>=0\n");}
ret=original_process(a,b)
returnret;
}
3.2.5 C语言适配系列模式
如上分析,C语言在实际开发中,没有这么多传统模式限制,可以混合所有适配类模式的功能。
intc_adapt_process(charc,chard,chare)
{
inta,b;
intret;
//checkc,d
if(c<5)
{printf("error:cshouldbotlessthan5\n");}
if(d<0)
{printf("error:dshould>=0\n");}
a=(c+d)%e;//someformulaincludesc,d,eorsomeothersoperation
b=d+e;
//dosomethingoptimizationorotherfunction
ret=original_process(a,b)+1;
//returnsomethingaccroidingtoret
returnret;
}
3.3 模式实现总结
非常常用的设计模式,使用中都是自然而然的,没有想到其实也是几种退化的面向对象设计模式。。
- 外观模式
【摘要】外观模式(Facade)介绍外观模式也叫门面模式。外观模式就是提供一个统一的接口,用来访问子系统中的一群接口。外观模式定义了一个高层接口,让子系统更容易使用。如下图,是使用外观模式后将子系统的使用变得更加简单。说起来比较复杂,实际上在日常生活中经常能遇到。比如部门安排出差,需要去淘宝买出差洗漱用品,一共有4中东西要选择。洗面奶,牙膏,洗发水,润肤露。消费者可以自己一个
4.1 外观模式(Facade)介绍
外观模式也叫门面模式。外观模式就是提供一个统一的接口,用来访问子系统中的一群接口。外观模式定义了一个高层接口,让子系统更容易使用。如下图,是使用外观模式后将子系统的使用变得更加简单。
说起来比较复杂,实际上在日常生活中经常能遇到。比如部门安排出差,需要去淘宝买出差洗漱用品,一共有4中东西要选择。洗面奶,牙膏,洗发水,润肤露。消费者可以自己一个买,也可以买套餐。把上图大方框外的小方块看成不同的消费者,里面的小方块看成洗面奶之类。Façade就是店家提供的不同套餐选择的那个接口。
4.2 外观模式实现
4.2.1 传统内部原始接口
#defineBIG_BOTTLE0
#defineMID_BOTTLE1
#defineSMALL_BOTTLE2
char*toiletries_type[]=
{
"bigbottle",
"middlebottle"
"smallbottle"
};
voidbuy_face_soap(inttype)
{
printf("buy%sfacesoap\n",toiletries_type[type]);
}
voidbuy_shampoo(inttype)
{
printf("buy%sshampoo\n",toiletries_type[type]);
}
voidbuy_toothpaste(inttype)
{
printf("buy%stoothpaste\n",toiletries_type[type]);
}
voidbuy_bodylotion(inttype)
{
printf("buy%sbodywash\n",toiletries_type[type]);
}
4.2.2 原始函数样例
在普通模式里,外部client可以随意调用内部的接口。
voidclient_buy_normal()
{
buy_face_soap(MID_BOTTLE);
buy_face_soap(MID_BOTTLE);
buy_shampoo(BIG_BOTTLE);
buy_shampoo(MID_BOTTLE);
buy_toothpaste(SMALL_BOTTLE);
buy_toothpaste(MID_BOTTLE);
buy_bodylotion(BIG_BOTTLE);
}
4.2.3 外观模式函数样例
实现了两个封装好的接口pack_for_woman和pack_for_man。不允许外部client调用内部接口,只允许调用封装好的facade接口pack_for_woman和pack_for_man。
voidpack_for_woman()
{
buy_face_soap(MID_BOTTLE);
buy_shampoo(BIG_BOTTLE);
buy_toothpaste(SMALL_BOTTLE);
buy_bodylotion(BIG_BOTTLE);
}
voidpack_for_man()
{
buy_face_soap(MID_BOTTLE);
buy_shampoo(MID_BOTTLE);
buy_toothpaste(MID_BOTTLE);
}
voidclient_buy_with_facade()
{
pack_for_woman();
pack_for_man();
}
在原始函数和外观模式实现里,买的东西都一样。
4.3 模式实现总结
引入外观模式,是客户对子系统的使用变得简单了,减少了与子系统的关联对象,实现了子系统与客户之间的松耦合关系。但是,灵活性变差了,客户不能自由选择子系统内部的接口,只能使用封装好的一套接口。
实际生活里,客户并不是需要选子系统内部接口。比如DIY电脑就相当于普通的模式,消费者会买cpu,主板等各个组件。而品牌PC就类似于门面模式,只能购买特定个型号。对于手机就更不用说了,没有消费者能买soc,flash来装手机。
5.状态机模式
【摘要】状态机模式是C语言实现相当常用的模式,也是能够在C语言下体现出来的最显性的模式之一。在面向对象里,状态模式允许一个对象在内部状态改变的时候改变其行为。
5.1 模式介绍
状态(state)模式是C语言实现相当常用的模式,也是能够在C语言***现出来的最显性的模式之一。在面向对象里,状态模式允许一个对象在内部状态改变的时候改变其行为。
状态用法很多,最常见的是状态机,分为无限状态机和有限状态机。
有限状态机finite-statemachine,FSM,输入集合和输出集合都是有限的,并只有有限数目的状态。一般说到状态机即是对有限状态机的简称。
无限状态机infinite-statemachine,ISM,输入和输出集合无线,状态数目无限的状态机。
在C语言里,状态模式有且仅有一种经典用法,就是有限状态机(FSM)的实现。实现的方式极为突出明显,大部分情况都能直接照搬框架。
状态机最常见的使用场景是实现协议。通常协议会有几个核心状态机描述。
5.1.1 有限状态机
状态机示例1
有一个灯,按下开按钮,就会开灯,按下关按钮就会关灯。这就是一个很典型的简单的有限状态机。简单的描述有2个状态,关灯[STATE_OFF],亮[STATE_LIGHT_ON]。有两个事件,开和关按钮。这两个事件促使状态机间的转换。
状态机示例2
有一个灯,按下开按钮,就会开灯,按下关按钮就会关灯。和一般等不同的是,两次开之间的灯的明暗不一样。也就是说,第一次开的时候,是高亮,关灯后,再开是低亮,下次再开是高亮,循环往复。
这就是一个很典型的简单的有限状态机。简单的描述有3个状态,关灯[STATE_OFF],高亮[STATE_HIGH_LIGHT],低亮[STATE_LOW_LIGHT]。
5.2 简单状态机模式实现
以状态机示例1为目标,如果用if/switch/case来,就没有什么设计和模式的意义,那只是最普通的流程开发技能。以下是简单状态机模式实现,适用于转移条件单一,对结果很确定的状态机。
#defineSTATE_OFF0
#defineSTATE_LIGHT_ON1
#defineSTATE_MAX2
#defineEVETN_BTN_OFF
#defineEVETN_BTN_ON
#defineEVETN_MAX
intlight_fsm_simple_table[STATE_MAX][EVETN_MAX]=
{
[STATE_OFF][EVETN_BTN_OFF]=STATE_OFF,
[STATE_OFF][EVETN_BTN_ON]=STATE_LIGHT_ON,
[STATE_LIGHT_ON][EVETN_BTN_OFF]=STATE_OFF,
[STATE_LIGHT_ON][EVETN_BTN_ON]=STATE_LIGHT_ON
};
intlight_fsm_event(intcur_stat,intevent)
{
intnext_state;
next_state=light_fsm_simple_table[cur_stat][event];
}
intmain()
{
intlight_state=STATE_OFF;
inteve1=EVETN_BTN_OFF;
inteve2=EVETN_BTN_ON;
light_state=light_fsm_event(light_state,eve1);
printf("nowlightstateis%d\n",light_state);
light_state=light_fsm_event(light_state,eve2);
printf("nowlightstateis%d\n",light_state);
}
以上代码有几个要点
1.状态转移数组。由于简单模式某种状态下发生某事件的结果是确定的,所以数组的值就是下一个状态。
2.需要一个状态处理的封装函数light_fsm_event。里面除了转移状态,可以增加扩展处理。不然简单模式应用就很局限。比如可以在light_fsm_event里面加入
If(next_state==STATE_LIGHT_ON)
{printf("lightison";}
5.3 普通状态机模式实现
大型一点的项目,比如复杂协议的实现,一个状态转移到下一个状态的情况是比较复杂的,无法用当前状态和事件简单确定,所以一般需要函数。
以下代码实现了状态机示例二,为样例代码,未运行实验过。
#defineSTATE_DEPEND4
#defineSTATE_OFF0
#defineSTATE_HIGH_LIGHT1
#defineSTATE_LOW_LIGHT2
#defineSTATE_MAX3
#defineEVETN_BTN_OFF
#defineEVETN_BTN_ON
#defineEVETN_MAX
intlast_state=STATE_LOW_LIGHT;
intlast_light_state=STATE_LOW_LIGHT;
struct{
int(*func)();
intnext_state;
}light_fsm[STATE_MAX][EVETN_MAX]=
{
//STATE_OFF
{
{lfsm_ignore,STATE_MAX},/*EVETN_BTN_OFF*/
{lfsm_btn_on,STATE_DEPEND},/*EVETN_BTN_ON*/
}
//STATE_HIGH_LIGHT
{
{lfsm_btn_off,STATE_OFF},/*EVETN_BTN_OFF*/
{lfsm_ignore,STATE_MAX},/*EVETN_BTN_ON*/
}
//STATE_LOW_LIGHT
{
{lfsm_btn_off,STATE_OFF},/*EVETN_BTN_OFF*/
{lfsm_ignore,STATE_MAX},/*EVETN_BTN_ON*/
}
}
intlfsm_ignore(intcur_stat,intevent)
{
printf("invalidstateorevent\n");
return0;
}
intlfsm_btn_on(intcur_stat,intevent)
{
if(last_light_state==STATE_HIGH_LIGHT)
{
returnSTATE_LOW_LIGHT;
}
elseif(last_light_state==STATE_LOW_LIGHT)
{
returnSTATE_HIGH_LIGHT;
}
else
{
printf("invalidstate\n");
returnSTATE_MAX;
}
}
intlfsm_btn_off(intcur_stat,intevent)
{
last_light_state=cur_stat;
return0;
}
intlight_change_state(intcur_stat,intnext_state,intevent)
{
//iflightonhasspecialhandling
if(next_state=STATE_HIGH_LIGHT)
{
printf("rejoice,nowbrightlight\n")
};
//otherstatechangerelatedhandlings,maybeusecurrentstateandnextstate,oreventtype
last_state=cur_stat;
cur_stat=next_state;
return0;
}
intlight_event_happen(intevent)
{
//iflightonhasspecialhandling
if(event=EVETN_BTN_OFF)
{
printf("someoneturnofflight\n");
}
//othereventtyperelatedhandlings
return0;
}
intlight_fsm_event(intcur_stat,intevent)
{
intnext_state,next_state_tmp;
next_state_tmp=*(light_fsm[cur_stat][event].func);
if(next_state_tmp==STATE_MAX)
{
printf("fsmerror\n");
return-1;
}
if(light_fsm[cur_stat][event].next_state==STATE_DEPEND)
{
next_state=next_state_tmp;
}
else
{
next_state=light_fsm[cur_stat][event].next_state;
}
light_change_state(next_state,cur_stat,event);
light_event_happen(event);
}
intmain()
{
intlight_state=STATE_OFF;
light_fsm_event(light_state,EVETN_BTN_OFF);
light_fsm_event(light_state,EVETN_BTN_ON);
light_fsm_event(light_state,EVETN_BTN_OFF);
light_fsm_event(light_state,EVETN_BTN_ON);
}
普通模式的状态机的几个关键点
1.状态机数组由状态事件处理函数+下一个状态数组代替简单模式的下一个状态的数组
2.由于在特定模式特定事件发生时,有的情况不能确定下一个状态的跳转,有的情况可以。所以下一状态有个特殊值为STATE_DEPEND。如果遇到这个值,就从状态变化函数里获得下一个状态。否则按照状态机数组设定的状态。
3.设定一个状态STATE_MAX用来表示错误事件,加上一个lfsm_ignore函数来处理这种情况。比如本例中,设定EVETN_BTN_ON不可能在开灯的时候发生。
4.状态机里除了状态机数字函数执行,有两类通用的函数,不参与主要的状态机运行,但是对状态机有影响。一类和特定的状态或状态转移有关,另外一类是和特定的事件有关。在样例代码里分别以light_change_state和light_event_happen来表示。
5.一般情况下,有一个全局变量保存当前状态和上一个状态。
面向对象语言实现状态机通常是一个状态的抽象父类,每个状态有一个子类和一个实例。C语言里状态转移表的函数指针是通过状态子类的成员函数实现。其他的写法思路比较接近。
5.4 复杂状态机模式实现
最常见的复杂状态机是为了实现网络协议。比如OSPF,可以参见我写的另外一篇文章ZEBRA中FSM编写总结.doc
5.5 模式实现总结
1.项目开发里最常见的使用为普通状态机,网络协议使用的复杂状态机也是在普通状态机上添加一些特性而来,基本特征是非常类似的。
2.C语言实现状态机的模式是非常固定的。状态转移表和核心的状态转移函数是核心。普通状态机的几个要素,不管在初始设计中有没有使用到,建议都写上。
6.责任链模式
【摘要】责任链模式需要触发的对象组成一条链,发送者将请发送者将请求发给链的第一个接收者,并且沿着这条链传递,直到有一个对象来处理它或者直到最后也没有对象处理而留在链末尾端。
6.1 模式介绍
责任链将需要触发的对象组成一条链,发送者将请求发给链的第一个接收者,并且沿着这条链传递,直到有一个对象来处理它或者直到最后也没有对象处理而留在链末尾端。
图表1责任链模式流程图
责任链在C语言里也是实现形式非常明显的模式。最典型的责任链有linux内核的中断处理机制的纯软件部分和内核网络netfiler的HOOK机制。这两者均强化了责任链机制,重点在引入了责任优先级方法和增加了通过/终结两种处理结果。
责任链模式的最重要的数据结构是handler链表。事件发生时,handler链表上的回调函数会被以此调用。优先级决定了那个handler会被先调,哪些会被后调用。在扩展特性里,每个handler可以有不处理和处理完之后继续交给下一个handler两种选择。如果该事件最后没有被消费,会有一个异常处理函数。如果责任链上任意一个handler消费了事件,那么就不传给下一个handler,直接结束。
逻辑上和责任链模式最相近的一个设计模式为观察者模式。流程图如下。观察者模式和责任链模式的最大的差别在于,事件会被通知到每一个平等的handler,而不是逐级处理。也不存在优先级的说法,也不会出现事件没有处理需要异常函数收尾。
图表2观察者模式流程图
6.2 责任链模式实现
责任链模式事件怎么触发不要紧,关键就是handler的数据结构组织和处理逻辑。
6.3 责任链节点定义
//两类处理结果,子类可以扩展
#defineCHAIN_PASS0
#defineCHAIN_STOP1
typedefint(*chain_func)(char*buf);
structchain_ops_node{
structlist_headlist;//内核链表标准结构
chain_func*handler;//handler的回调函数
intpriority;//优先级
};
6.4 责任链和处理函数
//全局的责任链
structlist_headchain_global_list;
//具体的处理函数
intchain_handler1(char*buf)
{
//dosomething
if(/*someconditions*/)
{
returnCHAIN_PASS;
}
returnCHAIN_STOP;
}
intchain_handler2(char*buf)
{
//dosomething
if(/*someconditions*/)
{
returnCHAIN_PASS;
}
returnCHAIN_STOP;
}
//封装成节点
structchain_ops_nodenode1=
{
.handler=chain_handler1,
.priority=0
}
structchain_ops_nodenode2=
{
.handler=chain_handler2,
.priority=1
}
6.5 注册和反注册函数
特别注意,一般是需要信号量锁定的,因为很可能链条上的数据正在执行。内核里喜欢用rcu锁,可以避免资源互斥引起cpu浪费。
intchain_register(structchain_ops_node*node)
{
//lockchain_global_list
//addnodeintochain_global_listaccordingtopriority
//unlockchain_global_list
return0;
}
intchain_unregister(structchain_ops_node*node)
{
//lockchain_global_list
//deletenodeintochain_global_list
//unlockchain_global_list
return0;
}
6.6 调用流程
intmain()
{
structlist_head*node;
structchain_ops_node*node_func;
charbuf[16];
chain_register(&node1);
chain_register(&node2);
//somethinghappend,shouldtriggerresponsibilitychain
//fillbufwithevent
list_for_each(node,&chain_global_list)
{
node_func=(structchain_ops_node*)node;
if(node_func.handler(buf)==CHAIN_STOP)
{break;}
}
return0;
}
6.7 内核的责任链模式实例
内核里最典型的就是内核中断处理和和内核网络netfiler的HOOK机制。而内核网络netfiler的HOOK机制的责任链模式体现更为完整充分。所以本文以netfiler的HOOK机制为例讲解。
6.7.1 handler的格式
内核的hook链就是责任链模式的handler链。nf_hook_ops就是handler链的一个handler节点。
structnf_hook_ops{
structlist_headlist;//内核链表标准结构
/*Userfillsinfromheredown.*/
nf_hookfn*hook;//handler的回调函数
structmodule*owner;//模式无关,可忽略
u_int8_tpf;//协议族,用来区分事件处理的,可以看作辅助标记。作为单链可忽略。
unsignedinthooknum;//挂在在哪个hook链上,netfiler的hook设计支持多hook链条,不过同一类事件只是触发一个hook链条的函数。所以从设计模式上讲这里只是同时实现了4条互不相干的责任链模式的handler链。作为单链可忽略。
/*Hooksareorderedinascendingpriority.*/
intpriority;//优先级
};
比如如下定义:
staticstructnf_hook_opsnf_nat_ops[]__read_mostly={
/*Beforepacketfiltering,changedestination*/
{
.hook=nf_nat_in,
.owner=THIS_MODULE,
.pf=NFPROTO_IPV4,
.hooknum=NF_INET_PRE_ROUTING,
.priority=NF_IP_PRI_NAT_DST,
},
…
…
}
6.7.2 handler的注册
intnf_register_hook(structnf_hook_ops*reg)
非常简单的操作,在锁的保护下,将handler节点加入到链表nf_hooks[reg->pf][reg->hooknum]上。插入链表的顺序由priority决定,升序排列。
从这里可以看出,链表是二维的,区分了协议族和hooknum(网络通路的位置)。本质上,对于一个固定handler,可以认为只是和一个handler链条发生关系。
6.7.3 事件触发的处理函数
以IPV4的NF_INET_PRE_ROUTINGhook为例,ip_rcv函数最后会调用nf_hook_slow,遍历链表调用handler函数。Handler返回处理结果有NF_ACCEPT,NF_STOLEN,NF_DROP等好几种。nf_hook_slow会根据这些结果决定接着调用链表上下一个handler还是终止等一系列动作。
linux内核遍历的方法基本上都是list_for_each_xxx函数。
6.8 模式实现总结
1.责任链模式在内核的实现很普遍,实现代码典型而简单,都是先定义各异handler的链表节点,包含list结构体,优先级,回调处理函数3个要素即可。更复杂的责任链模式实现只不过多条链,但是单个链的属性是没有改变的。而netfilter的链已经算比较复杂的,所以绝大部分编码学习到这个水平就足够了。
2.每个handler的处理结果根据需要定义,总体上讲都是继续和不继续两种。
7.观察者模式
【摘要】观察者模式定义了对象之间的一对多依赖关系,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并且自动更新。在这里,发生改变的对象称之为观察目标,而被通知的对象称之为观察者。一个观察目标可以对应多个观察者,而且这些观察者之间没有相互联系,所以么可以根据需要增加和删除观察者,使得系统更易于扩展。
7.1 模式介绍:观察者模式(Observer)
观察者模式定义了对象之间的一对多依赖关系,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并且自动更新。在这里,发生改变的对象称之为观察目标,而被通知的对象称之为观察者。一个观察目标可以对应多个观察者,而且这些观察者之间没有相互联系,所以么可以根据需要增加和删除观察者,使得系统更易于扩展。
图表1观察者模式流程图
观察者模式在C语言里也是实现形式非常明显的模式。逻辑上和责任链模式最相近的一个设计模式为观察者模式。观察者模式和责任链模式的最大的差别在于,事件会被通知到每一个handler,而不是逐级处理。也不存在优先级的说法,也不会出现事件没有处理需要异常函数收尾。一个Observer是否注册和执行不应该影响其他的Observer。而在责任链模式上,前面的责任handler在传递给下一个handler时,是可以改变事件相关变量。
但是在C语言实现上,观察者模式的handler绝大部分也是按照链表来组织的,在代码执行上,实际上相当于遍历链表。和责任链模式的区别在于每个handler没有优先级,没有权力决定是否停止遍历,最后事件也不需要被handler消费掉,也就是没有异常函数。
所以从C语言代码实现上讲,观察者模式可以看作责任链模式的特例。
1.无优先级
2.不能修改随事件而来的变量。比如在netfilter使用责任链模式就修改了随事件而来的数据包。
3.每个handler/observer只能无条件把事件传给observer链表的下一个节点。
图表2观察者模式和责任链模式对比
左边是责任链模式,右边是观察者模式的内核代码实现流程。
7.2 观察者模式实现
7.2.1 观察者节点定义
//不需要处理结果
typedefint(*observer_func)(char*buf);
structobserver_ops_node{
structlist_headlist;//内核链表标准结构
observer_func*handler;//handler的回调函数,没有优先级
};
7.2.2 观察者链和处理函数
//全局的观察者链
structlist_headobserver_global_list;
//具体的处理函数
intobserver_handler1(char*buf)
{
//dosomething
return0;
}
intobserver_handler2(char*buf)
{
//dosomething
return0;
}
//封装成节点
structobserver_ops_nodenode1=
{
.handler=observer_handler1,
}
structobserver_ops_nodenode2=
{
.handler=observer_handler2,
}
7.2.3 注册和反注册函数
特别注意,一般是需要信号量锁定的,因为很可能链条上的函数正在执行。内核里喜欢用rcu锁,可以避免资源互斥引起cpu浪费。
intobserver_register(structobserver_ops_node*node)
{
//lockobserver_global_list
//addnodeintoobserver_global_list
//unlockobserver_global_list
return0;
}
intobserver_unregister(structobserver_ops_node*node)
{
//lockobserver_global_list
//deletenodeintoobserver_global_list
//unlockobserver_global_list
return0;
}
7.2.4 调用流程
不检查观察者结果,必须全部遍历完。
intmain()
{
structlist_head*node;
structobserver_ops_node*node_func;
charbuf[16];
observer_register(&node1);
observer_register(&node1);
//somethinghappend,shouldtriggerresponsibilityobserver
//fillbufwithevent
list_for_each(node,&observer_global_list)
{
node_func=(structobserver_ops_node*)node;
node_func.handler(buf);
}
return0;
}
7.3 内核的观察者模式实现
7.3.1 观察节点模型
structnotifier_block{
int(*notifier_call)(structnotifier_block*,unsignedlong,void*);//观察者回调函数
structnotifier_block__rcu*next;//链表结构
intpriority;//优先级,内核里这个属于扩展的用法。
};
下面的例子。
staticstructnotifier_blockarp_netdev_notifier={
.notifier_call=arp_netdev_event,
};
最后调用notifier_chain_register注册arp_netdev_notifier到netdev_chain链表上。
7.3.2 事件触发的处理函数
那么当网络接口状态发生变化时,就通过call_netdevice_notifiers(NETDEV_PRE_UP,dev);调用通知所有注册的observer回调函数。
函数简化如下。里面需要注意的只有一点,返回结果可能会有NOTIFY_STOP_MASK,允许某个observer停止遍历调用。从这个意义讲,observer既有优先级又能阻止调用,观察者模式和责任链模式的区别就很小了。
staticint__kprobesnotifier_call_chain(structnotifier_block**nl,
unsignedlongval,void*v,
intnr_to_call,int*nr_calls)
{
intret=NOTIFY_DONE;
structnotifier_block*nb,*next_nb;
nb=rcu_dereference_raw(*nl);
while(nb&&nr_to_call){
next_nb=rcu_dereference_raw(nb->next);//取下一个observer
ret=nb->notifier_call(nb,val,v);
if((ret&NOTIFY_STOP_MASK)==NOTIFY_STOP_MASK)
break;
nb=next_nb;
}
returnret;
}
7.4 模式实现总结
总体用法和责任链模式类似,而在内核里实现的观察者模式其实并没有那么“纯粹”,而是扩展了优先级特性和可停止特性。这个破坏了Observer之间的独立性,因为原则上,一个Observer是否注册和执行不应该影响其他的Observer,内核的扩展这就使观察者模式变成了责任链模式模式。
8.命令模式
【摘要】模式介绍:命令模式(command)命令模式的解释如下:向对象发送一个请求,但是并不知道该请求的具体接收者是谁,具体的处理过程是如何的,只知道在程序运行中指定具体的请求接收者即可,对于这样将请求封装成对象的我们称之为命令模式。所以命令模式将请求封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象。同时命令模式支持可撤销的操作。命令模式的C语言实现也是非常显性的。
8.1 模式介绍:命令模式(command)
命令模式的解释如下:
向对象发送一个请求,但是并不知道该请求的具体接收者是谁,具体的处理过程是如何的,只知道在程序运行中指定具体的请求接收者即可,对于这样将请求封装成对象的我们称之为命令模式。所以命令模式将请求封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象。同时命令模式支持可撤销的操作。
命令模式的C语言实现也是非常显性的。命令发送方不通过直接调用的方式,而是通过发一个命令消息给接收方,让接收方执行操作。C语言里采用命令模式的最常见的原因是核间通信,进程间交互。如果是核间通信,通常是把命令按协定的格式封装在消息数据包里。如果是进程间通信,通常封装成一个结构体,把参数带过去。命令的通道通常是队列。
8.2 命令模式实现
8.2.1 实现流程
C语言命令模式经典方式如下,和面向对象是有明显的不同的。下图的invoker表示发命令的实体,而handler表示执行命令的实体,这个和面向对象的命令模式里的含义不一样。
图表1C语言命令模式示意图
图表2面向对象命令模式
C语言实现的命令模式核心数据结构是命令。发布命令的是invoker,多个invoker将命令封装起来,送到队列里。有一个函数或者线程称为receiver,检查队列里是否有没有处理的命令。由receiver负责调用各个handler。另外一个被经常使用的辅助数据结构是命令码数组,在如果invoker和handler运行于不同的环境,这种做法几乎是必选,如核间通信,内核和应用态通信。命令码作为索引,handler调用函数作为元素,Receiver根据不同的命令码调用handler。
也有不使用消息队列的C语言实现。
如果invoker和handler运行于相同的环境,可能直接把handler的回调函数的指针挂在命令结构体上,receiver可以直接调用handler的回调函数。很显然,不同的运行环境是没法这么做的。所以命令码数组是一个更为通用,封装性更好的方法。
面向对象的命令模式并没有提及到命令的消息队列,也没有提及命令码数组。消息队列本身并不是命令模式的一部分,而是在C语言实现里经常会用到的,特别是命令和执行不再同一个运行环境。命令码数组对于面向对象来说可以用多个子类来实现,所以也不体现出来。
8.2.2 命令模式的示例代码
以下代码为伪码。
命令码的定义
#defineCMD_10
#defineCMD_21
#defineCMD_MAX2
命令封装结构体
#defineCMD_LEN256
structcmd_msg
{
intcmd_code;
charbuf[CMD_LEN];//如果是不同环境的,只能用buffer数组,否则可以用指针
};
命令的实际处理函数
typedefint(*cmd_func)(char*buf);
intcmd1_handler(char*buf)
{
return0;
}
intcmd2_handler(char*buf)
{
return0;
}
命令码数组
命令码数组有两种方式,一种是将命令码作为数据的索引。另外一种情况是由于命令码太大,有一些特殊的规定,没法作为索引。所以在一个结构体里封装命令码和handler,最后实现一个结构体数据,这个在复杂的内核实现里会出现。
下面是简单的命令码,就是函数指针数组。
cmd_funccmd_table[]=
{
cmd1_handler,
cmd2_handler,
};
Invoker和receiver
Invoker的工作很简单,填充命令命令封装结构体,将其放入队列。
intinvoker1()
{
structcmd_msgcmd1_case;
memset(&cmd1_case,0,sizeof(cmd1_case));
cmd1_case.cmd_code=CMD_1;
//sendcmd1_casetoqueue
return0;
}
intinvoker2()
{
structcmd_msgcmd1_case;
memset(&cmd1_case,0,sizeof(cmd1_case));
cmd1_case.cmd_code=CMD_2;
//sendcmd1_casetoqueue
return0;
}
Receiver的工作就是监视命令队列,取出命令调用handler。
intcmd_receiver()
{
structcmd_msg*cmd_case;
while(1)
{
//getcmd_casefromqueuewhilequeueisnotempty
(*cmd_table[cmd_case->cmd_code])(cmd_case->buf);
}
return0;
}
命令队列有很多形态,比如IPC通道,用信号量,也能不要队列直接调用,总之就是让命令交到reciever手上然后分发调用handler。
伪码main程序:
intmain()
{
invoker1();
invoker2();
cmd_receiver();
return0;
}
8.2.3 内核的实现例子
内核有非常多的例子,典型的是wirelessextension的接口。上层应用通过ioctl下发命令到内核,内核解析后,调用相应的wirelessextension内核侧处理函数。这就是典型的不同运行环境的命令模式。参数是buffer,带命令码而不是直接发送函数指针。
/*--------------------------IOCTLLIST--------------------------*/
typedefint(*iw_handler)(structnet_device*dev,structiw_request_info*info,
void*wrqu,char*extra);
/*WirelessIdentification*///命令码
#defineSIOCSIWCOMMIT0x8B00/*Commitpendingchangestodriver*/
#defineSIOCGIWNAME0x8B01/*getname==wirelessprotocol*/
#defineSIOCSIWNWID0x8B02/*setnetworkid(pre-802.11)*/
#defineSIOCGIWNWID0x8B03/*getnetworkid(thecell)*/
#defineIW_HANDLER(id,func)\
[IW_IOCTL_IDX(id)]=func
//命令码数组
staticconstiw_handlerwl_handler[]=
{
IW_HANDLER(SIOCSIWCOMMIT,(iw_handler)wireless_commit),
IW_HANDLER(SIOCGIWNAME,(iw_handler)wireless_get_protocol),
IW_HANDLER(SIOCSIWFREQ,(iw_handler)wireless_set_frequency),
IW_HANDLER(SIOCGIWFREQ,(iw_handler)wireless_get_frequency),
…
}
//典型的receiver
staticintioctl_standard_iw_point(xxx)
{
…
{
/*CheckneedforESSIDcompatibilityforWE<21*/
switch(cmd){
caseSIOCSIWESSID://没法用索引,所以用了switchcase
caseSIOCGIWESSID:
caseSIOCSIWNICKN:
caseSIOCGIWNICKN:
if(iwp->length==descr->max_tokens+1)
essid_compat=1;
elseif(IW_IS_SET(cmd)&&(iwp->length!=0)){
charessid[IW_ESSID_MAX_SIZE+1];
unsignedintlen;
len=iwp->length*descr->token_size;
if(len>IW_ESSID_MAX_SIZE)
return-EFAULT;
err=copy_from_user(essid,iwp->pointer,len);
if(err)
return-EFAULT;
if(essid[iwp->length-1]=='\0')
essid_compat=1;
}
break;
default:
break;
}
…
}
可以看出,由于内核命令码是有特别含义的,所以不能作为索引,只能receiver干脆用switchcase。在ioctl_standard_iw_point函数里就是用switchcase。
8.3 模式实现总结
命令模式也是C语言实现的显性的设计模式,角色分为发布命令的invoker,分派命令的receiver和实际执行命令的handler。命令队列和命令码数组是核心的辅助元素。命令码数组目前只有两种类型。命令队列的实现类型就非常多,甚至未必是队列形式,需要设计人员根据经验把握。
9.访问者模式
【摘要】访问者模式(Visitor)介绍把对象数据和操作分离,使操作可以独立演化。一旦这些操作需要修改的话,接受这个操作的数据结构可以保持不变。访问者模式是适用于那些数据结构比较稳定的模式。这个算是在C里面退化的不是那么厉害的一种模式思想,或者说这种方法和C实现天然结合而不成为模式。因为C里面本来就很少将数据和访问方法封装在一起,数据的组织形式是数据结构的范畴,访问函数是代码。
9.1 访问者模式(Visitor)介绍
把对象数据和操作分离,使操作可以独立演化。一旦这些操作需要修改的话,接受这个操作的数据结构可以保持不变。访问者模式是适用于那些数据结构比较稳定的模式。这个算是在C里面退化的不是那么厉害的一种模式思想,或者说这种方法和C实现天然结合而不成为模式。因为C里面本来就很少将数据和访问方法封装在一起,数据的组织形式是数据结构的范畴,访问函数是代码流程设计的范畴。
9.2 面向对象实现的访问者模式
以下是用面向对象实现的访问者模式。两个帽子子类实例的不包含特有访问方法,也就是说,设计上只是想把这两个子类当作数据。帽子有两种访问方法price_show和size_show。
这里变化的因素有两个,不同的帽子和不同的访问方法(price和size)。解决不同帽子的方法是增加不同的帽子具体类。解决不同的访问方法是不同的具体访问方法子类price_show和size_show。这就是所谓的把对象数据和操作分离。
可以看出,虽然是对象数据和操作分离,但是数据的父类也必须提供了统一的访问接口,只不过不需要在子类里有特定的访问接口。
如果增加别的访问方法,就继续增加访问抽象父类和子类,并且修改帽子的抽象父类。
publicabstractclassVisitor{
publicabstractvoidvisitor(capAa);
publicabstractvoidvisitor(capBb);
}
publicclassprice_showextendsVisitor{
publicintvisitor(capAa){
returna.getPrice();
}
publicintvisitor(capBb){
returnb.getPrice();
}
}
publicclasssize_showextendsVisitor{
publicintvisitor(capAa){
returna.getsize();
}
publicintvisitor(capBb){
returnb.getsize();
}
}
publicabstractclasscap{
protectedintsize;
protectedintprice;
publiccap(intsize,intprice){
this.size=size;
this.price=price;
}
publicintgetsize(){
returnthis.size;
}
publicintgetPrice(){
returnthis.price;
}
publicabstractvoidaccept(Visitorvisitor);
}
publicclasscapAextendscap{
publiccapA(intsize,intprice){
super(size,price);
}
publicvoidaccept(Visitorvisitor){
visitor.visitor(this);
}
}
publicclasscapBextendscap{
publiccapB(intsize,intprice){
super(size,price);
}
publicvoidaccept(Visitorvisitor){
visitor.visitor(this);
}
}
publicclassshop{
List<cap>list=newArrayList<cap>();
publicvoidaccept(Visitorvisitor){
Iterator<cap>iterator=list.iterator();
while(iterator.hasNext()){
iterator.next().accept(visitor);
}
}
publicvoidaddcap(capcap){
list.add(cap);
}
publicvoidremovecap(capcap){
list.remove(cap);
}
}
publicclassClient{
publicstaticvoidmain(int[]args){
capa=newcapA(38,201);
capb=newcapB(41,95);
shopshop=newshop();
shop.addcap(a);
shop.addcap(b);
Visitorprice_show=newprice_show();
Visitorsize_show=newsize_show();
shop.accept(price_show);
shop.accept(size_show);
}
}
9.3 C实现的访问者模式
structcap
{
intsize;
intprice;
}
structcapshop[]=
{
[0]={
.size=38,
.price=201
},
[1]={
.size=41,
.price=95
},
}
intclient()
{
inti;
for(i=0;i++;i<ARRAY_SIZE(shop))
{
printf("cap%dsize%d",i,shop[i].size);
}
for(i=0;i++;i<ARRAY_SIZE(shop))
{
printf("cap%dprice%d",i,shop[i].price);
}
}
对比两者的代码,可以明显看出,C实现里从来不在用于存放数据的结构体里放函数。也就是说,面向对象里数据的抽象父类提供的getsize子类的方法是不存在的。
如果想要访问数据,根据数据的组织形式,直接操作就可以。从这个意义上讲,如果C语言实现里想要增加访问方法,根本不用修改数据相关的东西,这个比向对象里还需要修改数据的父类更为纯粹。这个才是更干净的访问者模式,也可以换个角度说,C里面根本不存在访问者模式
10.非典型模式
【摘要】上一章为止,C语言里显性和隐性的设计模式都已经介绍完了。非典型模式章节开始介绍23种设计模式里在C语言退化的,不适用的。这些模式在使用中很难想到其实也是一种设计模式,代码的实现也没有什么特点。开发人员在设计时并不需要特意考虑这些模式,随遇而安即可。了解这些模式,只是加强一下开发人员的全局观,在开发中能润物无声的应用和衍生。原型模式(Prototype)介绍
上一章为止,C语言里显性和隐性的设计模式都已经介绍完了。
非典型模式章节开始介绍23种设计模式里在C语言退化的,不适用的。这些模式在使用中很难想到其实也是一种设计模式,代码的实现也没有什么特点。开发人员在设计时并不需要特意考虑这些模式,随遇而安即可。
了解这些模式,只是加强一下开发人员的全局观,在开发中能润物无声的应用和衍生。
10.1 原型模式(Prototype)介绍
某些对象的结构比较复杂,但是我们又需要频繁的使用它们。通过复制这些原型创建新的对象。
比如写一个内核发送大量数据包的程序,比较好的办法就是做好一个标准数据包,然后不停的复制,再发送出去。
10.1.1 深复制和浅复制
熟悉内核网络实现的人一定知道,如果按照复制数据包的做法,最好的办法是使用skb_clone函数而不是skb_copy函数。原因是,内核里表示数据包由skb和data部分组成。Skb只是管理的句柄,而data是数据包的真实数据。如果是同样的数据包发送,只需要复制句柄,保持对data的引用就可以。这个就是浅复制。
图表1浅复制skb_clone
可以看出,浅复制skb_clone只是复制了sk_buff句柄,而下面的packetdatastorage指向的是同一个。
如果发送数据包要按照顺序变换数据包IP头的序列号和校验码。那么就需要同时复制句柄和data作为新的数据包,使用skb_copy函数。
图表2深复制skb_copy
可以看出,深复制skb_copy同时复制了sk_buff句柄和下面的packetdatastorage。
10.2 单例模式(Singleton)介绍
单例模式就是确保某一个类只有一个实例,并且提供一个全局访问点。在C语言的实现中,最常见的使用方法有且仅有一个:作为全局变量存在的控制数据。在嵌入式驱动开发中经常使用,比如driver定义,device定义。例子如下。
staticstructplatform_driverdwc3_exynos_driver={
.probe=dwc3_exynos_probe,
.remove=__devexit_p(dwc3_exynos_remove),
.driver={
.name="exynos-dwc3",
},
};
在ISP里用一个全局变量保存camera的属性,都可以算是单例模式。
但是C语言里,由于这种方法太常见,除了一个全局变量,也没有别的代码,所以从来没有人认为这是一个设计模式。
10.3 组合模式(Composite)
组合模式也称为合成模式,有时候又成为部分-整体(part-whole)模式。
先看看在面向对象里的定义。组合模式将对象组织到树结构里,可以用来描述整体和部分的关联。合成模式可以使客户端将单纯元素和符合元素同等看到。
用C语言翻译上面的话,就是把数据按照树结构组织起来,访问的时候能将叶子节点和中间节点同等处理,用的是递归的方法。
剩下的,大家参考数据结构的书吧。在C里面,这东西算数据结构,不算设计模式。
10.4 享元模式(Flyweight)
享元模式以共享的方式支持大量的细粒度对象。享元模式把对象属性分为内部状态和外部状态。内部状态是对象本质属性,不可改变。外部的可以随着环境改变。
享元模式最常见的在编辑器的实现里。如字母a,内部状态就是a本身,外部状态是位置,字体。这样就能共享一个a对象,在编辑器里实现不同的表现。
在C里,并没有发现这个模式有什么应用的场景。如果有人知道,请留言。
10.5 工厂模式和抽象工厂模式(Factory)
工厂方法模式定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个,也就是说工厂方法模式让实例化推迟到子类。
对于C开发者,工厂模式更通俗一点理解就是,客户并不直接malloc一个对象A或者B,而是调用一个工厂函数,输入A或者B的类型。由工厂函数返回对象A或者B
对于C语言,没有类的约束,创建对象也不过是一个malloc。靠输入不同的参数决定产生不同的对象,谈不上模式。实际应用也很少。
抽象工厂模式用来成体系的对象,C语言也用不上。
10.5.1 工厂模式样例代码
cap_factory生产不同的帽子
#defineRED_TYPE0x01
#defineBLUE_TYPE0x02
structcap
{
inttype;
void(*process_cap)(structcap*);
};
voidprocess_red_cap(structcap*pcap)
{
printf("Thisisaredcap!\n");
}
voidprocess_blue_cap(structcap*pcap)
{
printf("Thisisabluecap!\n");
}
structcap*cap_factory(inttype)
{
structcap*pcap=(structcap*)malloc(sizeof(structcap));
memset(pcap,0,sizeof(structcap));
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;
}
returnpcap;
}
10.6 解释器模式(Interpreter)介绍
解释器模式就是定义语言的语法,并且建立一个解释器来解释该语言中的句子。比如用C写一个xml语言的解析器,这个开发就算是解释器模式。显然,C里面这个算是一个开发项目,不是设计模式。
另外一个常见的使用场景是C语言学习课程里,编写一个计算器。需要将用户输入的算式的字符串解析并表达出来。但是编程和语法上也没有什么特殊的。
在正常项目里也许有少量的地方根据字符串的语法含义做一些处理,但是场景非常少,用有意义的字符串做为控制参数显然不如用整数或者结构体。
10.7 迭代器模式(Iterator)介绍
迭代器模式是将迭代元素的责任交给迭代器,而不是对象,可以在不需要知道该聚合对象的内部结构就可以实现该聚合对象的迭代。
比如一组元素,可能是链表组成的,可能是树状结构。可以写一个迭代器函数,屏蔽具体元素组织结构的差异,遍历全部的元素,那么就算迭代器模式。显然,C里面并不太需要这种方式。一个元素的组织结构,是在设计时综合考虑效率,内存空间,场景,就已经确定了。如果一定要实现迭代器,最多也就是封装一个函数。在C实现里,这种也不算是设计模式,需求也不明显。
10.8 备忘录模式(Memento)介绍
备忘录模式又叫做快照模式(shapshot)。在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。
在vmware虚拟机上用过linux的人一定对快照功能非常清楚。在android之前,一般的嵌入式linux的代码都不多,在性能强的vmware虚拟机上安装linux编译,调试就能完成绝大部分任务。对于写内核程序的开发者而言,可能一点错误就导致内核崩溃,需要重启。所以一般会在调试之前用vmware保存一个snapshot。崩溃之后不用重启,直接恢复快照(snapshot)就原地满血复活,回到快照那一刻的状态。
对于C语言,如果有的过程数据需要保留,那么小的用临时变量,大的可以分配一片内存。一般规模和需求都很小。也没有一个特别的操作可以称为模式。这种使用只在低功耗,寄存器保存恢复时使用,但是实现上没有任何特别的代码,都是整片的赋值。由于寄存器的特殊性,基本上不用memcpy,曾经发生过bug,因为嵌入式系统下memcpy未必是按照通常理解的word赋值,而是替换库文件的memcpy函数改写之后的。
10.9 策略模式(Strategy)介绍
开发中可以通过很多种不同的方式来完成一件事情,这里的每一种方式都可以称作为一种策略。比如计算ISP的自动曝光,有两种不同的算法,也可以称为不同的策略。策略模式就是可以算法和策略但是外界不需要修改的设计模式。很显然,只要算法和其他组件间的接口保持一致就可以。替换自动曝光算法,外部不修改代码就可以运行。
再比如,有一个算法比较复杂,还在开发的过程中,主流程里很可能用相同的接口进行打桩。桩和实际算法在运行时可以动态用函数指针动态配置,那么这个不算通常意义的算法更替,但也算是用到的策略模式的思想。但是很显然,对于C语言,这种肯定不算是一种明显的设计模式,而是一种函数或者功能替换,也没有特别的语法和架构支持,最多也就是使用函数指针方便替换策略。
很多帖子和书都拿替换排序算法为例,
中介者模式(Mediator)介绍
中介者模式也翻译成调停者模式,就是用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用。
图表1相互引用的模型
图表2中介者模式
在C里面,这个也很难算是一种设计模式,只能算是一种思路吧。中介者模式和门面模式看起来有点像,但差别还是很明显的。门面模式只是为了封装系统内细节,对外提供套餐服务。而中介者模式并不提供套餐,而是隔离,协调各对象的行为。现实中,项目经理就担任着中介者的角色。
10.10 桥接模式(Bridge)介绍
如果某个系统能够从多个角度来进行分类,且每一种分类都可能会变化,那么我们需要做的就是讲这多个角度分离出来,使得他们能独立变化。桥接模式将继承关系转化成关联关系。
更文艺的说法是,将”抽象化和实现化解耦”。抽象化在面向对象就是将对象共同的性质抽取出去而形成类的过程。桥接模式中的所谓脱耦,就是指在一个软件系统的抽象化和实现化之间使用关联关系(组合或者聚合关系)而不是继承关系,从而使两者可以相对独立地变化,这就是桥接模式的用意。
Java与模式书上举了飞机的例子。飞机有两个属性,制造商(波音和空客)和用途(运输机和客机)。书上把飞机作为抽象类,制造商作为具体实现类。不过如果再增加一个旋翼和固定翼的属性呢?桥梁模式没法解决超过2个维度的分类集成问题。
C就没有真正意义上的继承,所以我觉得,纯粹用面向对象继承改关联的思路看桥接模式比较局限,C不存在这个模式的需求。如果有不同的因素,那多维数组是一个选择。
11.表驱动模式
【摘要】表驱动是一种在C语言里常见的编程模式,从表里面查找信息而不使用逻辑语句(if和case)。核心操作是将输入因素作为直接或者间接的索引,到数组里找到直接的结果或者对应的处理(通常是函数指针)。
11.1 模式介绍
在传统的23种面向对象设计模式里,并没有表驱动这种模式。这种模式是强烈依赖数组或者多维数组的一种设计模式,不涉及类,继承等关系,所以在C语言等非面向对象编程里得到了广泛的应用。
表驱动是一种在C语言里常见的编程模式,从表里面查找信息而不使用逻辑语句(if和case)。核心操作是将输入因素作为直接或者间接的索引,到数组里找到直接的结果或者对应的处理(通常是函数指针)。
表驱动实质上把逻辑和数据进行了分离。因素和结果之间的映射关系能够全部存放到数组里,而不是混杂在if,else的流程代码里。当映射关系发生改变的时候,只需要改变数组就可以,不需要修改代码。管理和维护起来非常方便。甚至可以把数据作为配置文件存放到硬盘上,需要的时候读取进来,避免了代码重新编译。
11.1.1 模式分类
根据输入因素的不同,按照“代码大全”一书的分类,有如下三种。
直接访问
直接访问是最基本最本质的一种方式。前提条件是输入因素是整数,或者是非常简单的一对一转化成能作为数组元素序号。
索引访问
输入元素由于某些原因,难以作为数组的序号。将输入通过函数转化为序号,接下来和直接访问模式一样。
阶梯访问
输入元素是分段的范围,不好转化为数组的序号。
根据输入因素的个数,可以分为一维表查找和多维表查找。
11.2 模式实现
11.2.1 直接访问模式
大部分资料拿查月份有多少天来作为例子说明。月份的数字就是自然数,月份减一恰好就可以作为数字的序号。先用这个比较俗的例子示意下。后续在举一个实际开发中典型的例子。
普通代码
intget_days_in_month(intmon)
{
intdays;
if(1==mon){days=31;}
elseif(2==mon){days=28;}
elseif(3==mon){days=31;}
elseif(4==mon){days=30;}
elseif(5==mon){days=31;}
elseif(6==mon){days=30;}
elseif(7==mon){days=31;}
elseif(8==mon){days=31;}
elseif(9==mon){days=30;}
elseif(10==mon){days=31;}
elseif(11==mon){days=30;}
elseif(12==mon){days=31;}
returndays;
}
表驱动设计的代码
Days_in_Month[12]={31,28,31,30,31,30,31,31,30,31,30,31};
intget_days_in_month(intmon)
{
returnDays_in_Month[mon-1];
}
从上可以看到,查月份有多少天这个例子属于典型的直接访问模式,一维表查找的例子。
直接模式,多维数组-实际编程样例
在实际编程中,极少遇到这种输入因素本身就是数字的情形。一般是把输入的因素分类,假设为N类,然后用宏从0开始定义,以宏为序号。通常还需要设定一个MAX,来表示异常。
假设有如下场景:
某设备的工作流有几个因素决定,(acq_cnt,zsl,cap_nzsl,small_line_buffer)。acq_cnt表示摄像头个数,一般就1个或者2个。Zsl表示是否支持zsl,cap_nzsl是另外一个特性,small_line_buffer和设备buffer大小相关。
普通代码如下
intget_uc(intacq_cnt,intzsl,intcap_nzsl,intsmall_line_buffer)
{
intdf_type=0;
switch(acq_cnt)
{
case1:
{
if(zsl)
{
if(small_line_buffer<xxx)
{
df_type=DF_S1;
}
else
{
df_type=DF_M1;
}
}
else
{
if(cap_nzsl)
{
if(small_line_buffer<xxx)
{
df_type=DF_S3;
}
else
{
df_type=DF_M3;
}
}
else
{
if(small_line_buffer<xxx)
{
df_type=DF_S2;
}
else
{
df_type=DF_M2;
}
}
}
break;
}
case2:
{
if(zsl&&(small_line_buffer<xxx))
{
df_type=DF_D1;
}
else
{
if(cap_nzsl)
{
df_type=DF_D3;
}
else
{
df_type=DF_D2;
}
}
break;
}
default:
{
loge("acquirecameracount=%d",acq_cnt);
break;
}
}
returndf_type;
}
以上代码看起来就很烦,容易出错。如果因素改变了,还要改代码。但是用表查找还有几个问题。摄像头个数acq_cnt好说,用acq_cnt-1当作需要就可以。这个方法在月份的例子用过。small_line_buffer是个麻烦,因为不可能按照buffer的byte数作为序号。不过逻辑上只需要判断两个状态就可以,不如定义新的small_line_buffer为(老small_line_buffer<xxx),转化为0,1。老small_line_buffer<xxx为1,不满足条件新的small_line_buffer就为0。Zsl和cap_nzsl规定为0,1状态就可以了。
特别注意的是,这里的small_line_buffer改造,就有索引模式的意味了,因为buffer大小的数值是有很多的取值,但是small_line_buffer最后只有1个值,这就违反了直接模式里的输入因素和索引一对一的关系。
另外两个参数,zsl和cap_nzsl是特性,显然原始含义也不是整数,而是各自表示两种状态。这就需要把状态定义成从0开始的宏或者枚举。我一般习惯宏。
经过改造后,表驱动如下
flow_type_edf_type_tab[2][2][2][2]=
{
[0][0][0][0]=DF_M2,
[0][0][0][1]=DF_MAX,
[0][0][1][0]=DF_M3,
[0][0][1][1]=DF_S3,
[0][1][0][0]=DF_M1,
[0][1][0][1]=DF_S1,
[0][1][1][0]=DF_MAX,
[0][1][1][1]=DF_MAX,
[1][0][0][0]=DF_MAX,
[1][0][0][1]=DF_D2,
[1][0][1][0]=DF_MAX,
[1][0][1][1]=DF_D3,
[1][1][0][0]=DF_MAX,
[1][1][0][1]=DF_D1,
[1][1][1][0]=DF_MAX,
[1][1][1][1]=DF_MAX
};
因为原逻辑中,有好几个排列组合没有df_type合法值,那么就定义一个DF_MAX来表示。改造完的表驱动代码如下。
small_line_buffer=1:0?line_buffer<xxx;
df_type=df_type_tab[g_msg_mgr.acq_cnt-1][zsl][cap_nzsl][small_line_buffer];
11.2.2 索引访问模式
采用索引模式通常有几个原因
1.输入因素取值范围太大,而且大部分都是没有用的,或者很多值对应的结果影响是相同的。这样会造成数组的大量浪费,或者大到内存无法满足。
2.没法量化,比如有的输入值是浮点。
有的观点认为,索引就一种编程形式:因为原因素取值太多,那就再建立一个查找数组表,输入为原始因素,输出为一个中介序号。数据密集存放在数据里,靠中介序号来访问。这样相当于二级直接访问的叠加。中介表的每项只不过是一个输出索引的大小,通常远小于原始数据项的大小。
不过这种名词上的争议对编程没有帮助。一般来讲,可以把难以作为序号的输入因素转化为可以作为数组序号认为是一种没有定数的方法,不必称为一种模式。处理超大范围的输入有不同的方法,不一定就是靠一个中介的表驱动。如上面设备工作流例子中对smalllinebuffer的处理。当然也可以把buffersize作为序号,放一个size大小的数组,输出结果是0,1,但是还是太麻烦。
直接模式和索引模式在查找结果数据的方式上没有本质不同,和阶梯模式都有明显不同。
举个汽车管理的例子,按照重量来区分汽车类型。具体数值是否符合汽车专业的定义不影响代码的讨论。这个例子既可以用来演示索引访问模式,又可以演示阶梯访问模式。虽然对这个例子而言最好的方式是阶梯访问模式。
0-1000kg | 微型汽车 | 需要交费10元,绿色通道 |
1001kg-2000kg | 小型汽车 | 需要交费15元,拿票 |
2001kg-3000kg | 中型汽车 | 需要交费20元,停车审查 |
3001kg-5000kg | 重型汽车 | 需要交费50元,走特殊宽通道,检查轮胎。 |
可以看到,写一个5000个元素的数组,每一项表示类型,和采取的动作,如交费,审查等函数操作。太浪费了。那么可以先做一个长度为5000的char数组,输出结果只有4个值,微型,小型,中型和重型。
然后再写一个数组,长度只有4,将汽车类型定义为宏,值从0-3。数组的内容就是交费,审查等函数指针。
#defineMINI_CAR0
#defineSMALL_CAR1
#defineMIDDLE_CAR2
#defineHEAVY_CAR3
#defineMAX_CAR4
intweigth2type[]=
{
MINI_CAR,
MINI_CAR,
...
SMALL_CAR
MIDDLE_CAR,
MIDDLE_CAR,
...
HEAVY_CAR
};
voidcar_action1()
{
printf("10,expresslane");
}
voidcar_action2()
{
printf("15,ticket");
}
voidcar_action3()
{
printf("20,check");
}
voidcar_action4()
{
printf("50,speciallane,checktire");
}
void(*func[3])()=
{
car_action1,
car_action2,
car_action3,
car_action3
}
intcar_type=weigth2type[wight];
(*func[car_type])();
索引的另外一个好处是,以不同的方式来生成索引,可以从不同的方面来便捷访问数据。比如一个数组存放了姓名,年龄,体重,血压。可以根据体重年龄等来生成索引。在嵌入式实际编码中,这种场景用的不多。
11.2.3 阶梯访问模式
当输入值是一段很大的范围,尤其是浮点时,有些时候无法转化为确定的索引来查表。干脆就放弃直接查表的机会,而且把范围上下限按照次序放在表里,用for循环对比上下限的进行判断。
阶梯模式的输入只能是范围,这个和索引模式有所不同。索引在应付比较离散,没有大小判断规律的输入时也是有效的。
还是用索引模式的汽车例子,不考虑异常情况。
intweigth2type[4]
{
1000,
2000,
3000,
5000
}
for(i=0;i<4;i++)
{
if(weight<=weigth2type[i])
{
(*func[i])();
}
}
11.3 经典应用
表驱动最常见在状态机设计模式里作为状态转移数组,在命令设计模式里作为命令码数组。
可以参见
设计模式的C语言应用-状态机模式
https://bbs.huaweicloud.com/blogs/a4e37991c45811e7b8317ca23e93a891
设计模式的C语言应用-命令模式
https://bbs.huaweicloud.com/blogs/90b909e8c81711e7b8317ca23e93a891
11.4 模式总结
表驱动模式总体思路就是把逻辑关系写到数组里,采用序号获得结果。当代码里出现较多的if,else或者switch的时候,会大幅增加圈复杂度,就需要考虑改写为表驱动了。
参考
1 参考链接