作者:autoca,发布于2012-1-12
模块化C代码与UML对象模型之间的映射(1)——类、单例类及实用类
背景信息
日子一天天过去,业余时间不多,为了避免生活华而不实,碌碌无为,我计划抽空把工作和学习中散落在笔记本和书本某些角落的总结整理出来,放到博客上备忘,同时也希望有机会和技术圈的朋友交流,增长自己的见识,我的qq:18005491。
主题:《面向对象的模块化C编程》
计划提纲:
《模块化C代码与UML类图、时序图、状态图的映射》
《理解模块化》
《UML图示辅助C程序设计是否必要》
《用C练习设计模式》
《PP、TDD的利与弊》
1、有关Modular C的基本知识。建议参考Rober Strandh 的《Modular C ——How to Write Reusable and Maintainable Code using the C Language》,网上还有不少文献。
2、有关UML的基本知识。请参考UML相关书籍,例如Joseph Schmuller的《UML基础、案例与应用(第三版)》。
3、有关面向对象程序设计的基本知识。网上唾手可得。
4、有关模块与类在概念的差异本文不讨论,暂且视为等同的。
关键字:C语言;模块化;UML;面向对象;Modular C;OOPC;OOC
1、类
1、UML示例
图1是一个简单的类图。
MODULE是类的名称;
Create和Destroy分别用于创建和销毁对象;
Serve和ImplementService都是类的方法。
“+”和“-”号用于修饰方法是public还是private的;
Client_TEST是模块的用户。
图1
2、C代码示例
///////////////////////////////////////////////////////////////////////////////
//module.h
#ifndef __MODULE__
#define __MODULE__
struct _MODULE;
typedef struct _MODULE MODULE;
#ifdef __cplusplus
extern "C" {
#endif
MODULE *MODULE_Create(void);
void MODULE_Destroy(MODULE *self);
int MODULE_Serve(MODULE *self);
#ifdef __cplusplus
}
#endif
#endif /* __MODULE__ */
///////////////////////////////////////////////////////////////////////////////
//module.c
#include "module.h"
struct _MODULE
{
int privateData;
};
static int module_ImplementService(MODULE *self)
{
return ++self->privateData;
}
MODULE *MODULE_Create(int data)
{
MODULE *module = (MODULE *)malloc(sizeof(MODULE));
module->privateData = data;
return module;
}
void MODULE_Destroy(MODULE *self)
{
if (self != NULL)
{
free(self);
}
}
int MODULE_Serve(MODULE *self)
{
return (self != NULL) ? module_ImplementService(self) : 0;
}
///////////////////////////////////////////////////////////////////////////////
//test_module.c
#include "module.h" //客户要使用MODULE这个模块
TEST(test_module, case_serve_ok)
{
MODULE *module = MODULE_Create(1); //把MODULE类实例化,即创建对象
EXPECT_EQ(2, MODULE_Serve(module)); //使用MODULE提供的服务
MODULE_Destroy(module); //销毁对象
}
说明:
1、TEST是模块的客户(Client)之一。建议让测试作为新增模块的第一个客户。
2、本例中MODULE类理论上可以被实例化任意多次(当然实际上会受到内存的限制),然而某些应用可能要求某个类只能实例化一次,另外还有一种类型的类叫实用类,请看接下来的介绍。
2、单例类
单例类顾名思义就是类只能有一个实例。说到单例,有个设计模式不得不提,那就是单例模式(Singleton):保证一个类仅有一个实例,并提供一个访问它的全局访问点。
关于单例的实际例子就在我们身边,例如,单例就像谈恋爱要专一一样,在我们心里有一个女朋友类,它应该只能有一个实例^_^。再举个靠谱点的例子,请试一下多次运行Windows任务管理器,在界面上是不是只有一个窗口?我猜这就是单例模式的应用。
单例类可进一步分为饿汉式和懒汉式两种,这里暂不展开介绍。
1、UML示例
如图2为单例类。
2、C代码示例
说明:
图2-1和图2-2皆为单例类,二者的区别在于是否运行时动态分配内存。图2-1的私有方法Create表示以动态malloc方式创建对象,需要Destroy方法;图2-2则采用静态分配的方式,无需Destroy。
对应图2-1代码如下:
///////////////////////////////////////////////////////////////////////////////
//singleton.h
struct _SINGLETON;
typedef struct _SINGLETON SINGLETON;
SINGLETON *SINGLETON_GetInstance(void);
void SINGLETON_Destroy(SINGLETON *self);
int SINGLETON_Serve(SINGLETON *self);
///////////////////////////////////////////////////////////////////////////////
// singleton.c
#include "singleton.h"
struct _SINGLETON
{
char privateData[0];
};
static SINGLETON *gInstance = NULL;
static SINGLETON *singleton_Create(void)
{
SINGLETON *singleton = (SINGLETON *)malloc(sizeof(SINGLETON));
assert(singleton != NULL);
return singleton;
}
SINGLETON *SINGLETON_GetInstance(void)
{
if (NULL == gInstance)
{
gInstance = singleton_Create();
}
return gInstance;
}
void SINGLETON_Destroy(SINGLETON *self)
{
if ((self != NULL) && (self == gInstance))
{
free(gInstance);
gInstance = NULL;
}
}
///////////////////////////////////////////////////////////////////////////////
//test_singleton.c
TEST(test_singleton, case_only_one_instance)
{
SINGLETON *instance1 = SINGLETON_GetInstance();
SINGLETON *instance2 = SINGLETON_GetInstance();
EXPECT_EQ((int)instance1, (int)instance2);
SINGLETON_Destroy(instance1);
SINGLETON_Destroy(instance2);
}
对应图2-2代码如下:
///////////////////////////////////////////////////////////////////////////////
//singleton.h
struct _SINGLETON;
typedef struct _SINGLETON SINGLETON;
SINGLETON *SINGLETON_GetInstance(void);
int SINGLETON_Serve(SINGLETON *self);
///////////////////////////////////////////////////////////////////////////////
// singleton.c
#include "singleton.h"
struct _SINGLETON
{
char privateData[0];
};
static SINGLETON gInstance = {0};
static void singleton_Init(void)
{
memset(&gInstance, 0, sizeof(SINGLETON));
}
SINGLETON *SINGLETON_GetInstance(void)
{
singleton_Init();
return &gInstance;
}
3、实用类
实用类是无实例的类,它是一组方法的集合。这是网上偶尔看到的,我还不确定“实用类”这一称谓是否通用,有待考究。不过在C程序里面这样的模块应该是最常见的了。如常用库math、string等不妨可理解为实用类。
1、UML示例
2、C代码示例
///////////////////////////////////////////////////////////////////////////////
//utility.h
int UTILITY_Serve1(void);
int UTILITY_Serve2(int para);
模块化C代码与UML对象模型之间的映射(2)——抽象类与继承
今天继续写模块化C代码与UML类图的转换,所举例子也许粗糙,主要是演示一下思路,时间允许的话我会尽量按正式的产品开发质量要求来完善代码示例。
1.4 抽象类与继承
抽象类是指继承关系树中位于树枝节点的用于被继承的类,如图1.5所示。抽象类具有以下特点:
(1)不能被实例化,所以没有Create或GetInstance方法;
(2)抽象类中方法可以没有实现体,称为抽象方法,它必须被子类重写。
(3)如果类中包含抽象方法,则必须定义为抽象类。
顺便说一下,图中void *privateData是类的私有字段,可以利用这个字段扩展功能或简单忽略之。后面有机会再举例说明。
另外,I_DEV是类DEV实现的一组操作集合,公开给用户,这就是接口,下一节再详细举例说明。
UML示例:
图1.5 抽象类
C代码示例:
///////////////////////////////////////////////////////////////////////////////
//inheritance_utils.h
#undef CONTAINING_RECORD
#define CONTAINING_RECORD(address, type, field) \
((type *)( (char *)(address) - (unsigned long)(&((type*)0)->field)))
///////////////////////////////////////////////////////////////////////////////
//DEV.h
/* 定义设备操作接口*/
struct I_DEV{
int (*Add)(struct DEV *, int somePara);
int (*Delete)(struct DEV *, int somePara);
int (*Query)(struct DEV *, int somePara);
int (*Reset)(struct DEV *, int somePara);
};
/* 定义设备类*/
struct DEV{
void (*Destroy)(struct DEV *);
void (*Method1)(struct DEV *);
void (*Method2)(struct DEV *);
const struct I_DEV *itf; /* 接口*/
void *privateData;
};
//抽象类不能实例化,没有Create
void DEV_Init(struct DEV *DEV, const struct I_DEV *itf); //注意init方法职责是初始化而非实例化
void DEV_Destroy(struct DEV *self);
void DEV_Method1(struct DEV *self);//Method1是具体方法,DEV.c中实现Method1
//Method2是抽象方法,DEV.c中没有Method2实现体
///////////////////////////////////////////////////////////////////////////////
//DEV.c
#include "DEV.h"
void DEV_Init(struct DEV *DEV, const struct I_DEV *itf)
{
assert(DEV != NULL);
memset(DEV, 0, sizeof *DEV);
DEV->itf = itf;
DEV->Method1 = DEV_Method1;
}
void DEV_Destroy(struct DEV *self)
{
if (self != NULL)
{
free(self);
}
}
void DEV_Method1(struct DEV *self)
{
printf("hello, I'm concrete method!\r\n");
}
///////////////////////////////////////////////////////////////////////////////
//WASHER_DEV.h
struct WASHER; /* 隐藏具体类的细节*/
struct WASHER *WASHER_Create(int type, int id, char *name);
//…
///////////////////////////////////////////////////////////////////////////////
//WASHER.c
#include "DEV.h"
#include “WASHER_DEV.h”
/* 定义WASHER设备类*/
struct WASHER
{
int type;
int id;
char *name;
struct DEV DEV; //继承DEV类
void *moreData;
};
/* 实现I_DEV接口*/
static int WASHER_Add(struct DEV *DEV, int somePara)
{
struct WASHER *WASHER;
WASHER = CONTAINING_RECORD(DEV, struct WASHER, DEV);
DEV->privateData = WASHER;
printf("[WASHER_Add]type = %d, id = %d, name = %s\r\n", WASHER->type, WASHER->id, WASHER->name);
return 0;
}
static int WASHER_Query(struct DEV *DEV, int somePara)
{
struct WASHER *WASHER = (struct WASHER *)DEV->privateData;
printf("[WASHER_Query]type = %d, id = %d, name = %s\r\n", WASHER->type, WASHER->id, WASHER->name);
return 0;
}
struct I_DEV WASHERItf = {
WASHER_Add,
NULL,
WASHER_Query,
NULL,
};
/* 派生类实现基类的抽象方法*/
static void WASHER_DEV_Method2(struct DEV *self)
{
printf("hello, I'm abstract method, implemented by WASHER.\r\n");
}
static void WASHER_setup_DEV(struct WASHER *WASHER)
{
DEV_Init(&WASHER->DEV, &WASHERItf);
WASHER->DEV.Method2 = WASHER_DEV_Method2;
}
static void WASHER_init(struct WASHER *self, int type, int id, char *name)
{
memset(self, 0, sizeof(struct WASHER));
self->type = type;
self->id;
self->name = name;
WASHER_setup_DEV(self);
}
struct WASHER *WASHER_Create(int type, int id, char *name)
{
struct WASHER *WASHER = (struct WASHER *)malloc(sizeof(struct WASHER));
WASHER_init(WASHER, type, id, name);
return WASHER;
}
///////////////////////////////////////////////////////////////////////////////
//test_WASHER_DEV.c
#include "WASHER_DEV.h"
TEST(test_DEV_inheritance, test_demo)
{
int type = 3;
int id = 1;
char *name = "WASHER";
struct WASHER * WASHER = WASHER_Create(type, id, name);
// Usually register WASHER first. Here’s just a demo…
//针对接口/抽象编程
struct DEV *DEV = &(WASHER->DEV);
EXPECT_EQ(0, DEV->itf->Add(DEV, 0xff));
EXPECT_EQ(0, DEV->itf->Query(DEV, 0xff));
DEV->Method1(DEV);
DEV->Method2(DEV);
}
//e.g. user: bd = add(type, id, "DEVname"); query(bd, para);
模块化C代码与UML对象模型之间的映射(3)——UML关系
下图是从StarUML工具界面截下来的,从上往下依次表示UML的关系:关联、单向关联、聚合、组合、泛化、依赖和实现。
图3 UML关系集
3.1 关联、聚合、组合
关联(association)是一种结构关系,它指明一个事物的对象与另一个事物的对象间的联系。聚合和组合是更强的关联,表示整体和部分的关系。
聚合的整体不负责部分的生命期,组合的整体负责部分的生命期。关联关系需根据实际场景来识别,例如军队和士兵的关系一般可理解为聚合,士兵退役了就和军队脱离聚合关系了。但是,如果是打仗时,士兵们必须生死与共,军队没了则士兵命也没了,则可理解为组合。
UML示例:
C代码示例:
//A关联/聚合/组合了B
struct A{
struct B *b;
void (*Create)(B *b); //方式1
};
struct A{
struct B *b;
void (*SetB)( B *b); //方式2,单B
};
struct A{
struct B *bset[N];
void (*RegisterB)(B *b); //方式3,B集
};
3.3 泛化
泛化(generalization)是一种特殊/一般的关系。也可以看作是常说的继承关系。
下面举一个用C语言实现继承的一个经典例子,从Linux内核源码中拷贝过来的。
UML示例:
图3-2 继承(泛化)
C代码示例(删节版):
struct kobject {
const char *name;
struct kobject *parent;
};
struct cdev {
struct kobject kobj; //继承kobject
const struct file_operations *ops;
dev_t dev;
unsigned int count;
};
struct scullc_dev {
void **data;
struct scullc_dev *next;
struct cdev cdev; //继承cdev
};
注意:继承不是定义结构体指针,而是定义结构体。
3.2 依赖
依赖(dependency)是两个事物之间的语义关系,其中一个事物(独立事物)发生变化,会影响到另一个事物(依赖事物)的语义。最常用的依赖关系是一个类的构造函数中用到另一个类的定义。
UML示例:
由于类的客户要依靠接口实现类的操作,所以我们把与接口的交互建模为一种依赖关系。
图 3-3 依赖
C代码示例:
//方式1
void A_Method(struct A *a)
{
B_Method(); //B是实用类(即全局的实例),这种依赖方式在我们系统中最最多
}
//方式2
void A_Method(struct A *a, const struct B *b); //B作为参数被传递
//方式3
void A_Method(struct A *a)
{
struct B *b = B_Create(); //B在A的方法中实例化
}
3.4 实现
实现(realization)是类元之间的语义关系,其中的一个类元指定了由另一个类元保证执行的契约。
下面再拿Linux设备驱动做例子。scullc_dev类实现文件操作接口file_operations。
UML示例:
图3-4 实现
C代码示例:
//实现接口函数
int scullc_open (struct inode *inode, struct file *filp)
{
struct scullc_dev *dev; /* device information */
/* Find the device */
dev = container_of(inode->i_cdev, struct scullc_dev, cdev);
/* ...*/
/* and use filp->private_data to point to the device data */
filp->private_data = dev;
return 0; /* success */
}
ssize_t scullc_read (struct file *filp, char __user *buf, size_t count,
loff_t *f_pos)
{
struct scullc_dev *dev = filp->private_data; /* the first listitem */
struct scullc_dev *dptr;
int quantum = dev->quantum;
/* ... */
return retval;
}
//…
//文件操作接口实例
struct file_operations scullc_fops = {
.owner = THIS_MODULE,
.llseek = scullc_llseek,
.read = scullc_read,
.write = scullc_write,
.ioctl = scullc_ioctl,
.open = scullc_open,
.release = scullc_release,
};
//
static void scullc_setup_cdev(struct scullc_dev *dev, int index)
{
int err, devno = MKDEV(scullc_major, index);
cdev_init(&dev->cdev, &scullc_fops); //注册接口
dev->cdev.owner = THIS_MODULE;
dev->cdev.ops = &scullc_fops;
err = cdev_add (&dev->cdev, devno, 1);
/* Fail gracefully if need be */
if (err)
printk(KERN_NOTICE "Error %d adding scull%d", err, index);
}
模块化C代码与UML对象模型之间的映射(4)——常见问题小结
下面是零零散散做的一些笔记。
1、为什么说双向关联往往是设计的坏味道?
从可复用性的角度来看,如果两个类间的关系是双向的,则每个类都需要知道对方,因此两者都不能复用。说明单向关联有助于标识可复用的类。
2、聚合组合方式会遇到对象生命期管理的问题,怎么解决?
例如,A_Create(B_Create()->IA, B),即类B实现接口IA供类A内部使用,那么IA的生命期该由谁来管理呢?常见的做法是在IA中加入Destroy接口,那么A可以由此释放IA。可是这违背了一个原则,一般内存管理是谁申请谁释放的。我知道的另一个办法是借鉴Linux设备驱动程序的做法,把IA定义为全局变量。“生命期管理是个很大的课题。”后续再慢慢研究。
3、如何区别泛化与实现关系
我是这样理解的:泛化(继承)对应抽象类;实现对应接口。
4、关联与依赖有何区别?
这个问题有很多种版本的解释,摘要如下:
依赖是比关联弱的关系,依赖是两个事物之间的语义关系,而关联代表一种结构化的关系。体现在代码中:
(1)关联有双向与单向之分。若类A与类B双向关联,则A与B相互作为对方的attribute;若类A单向关联指向类B,则在类A中存在一个attribute B b*。
(2)依赖就只有单向的。若类A依赖类B,则不会有B这个属性。类A依赖类B有三种方式:一是类B是全局的,二是类B在类A中实例化,三是类B作为参数被传递。
5、继承or聚合/组合,这是一个问题
根据不同角度的理解,设计上会做出不同的抉择,但
请慎用继承,因为:
(1)继承是过紧的耦合,每当父类变化,子类也得跟着变,违背了开闭原则。
(2)继承不支持多态,父类与子类之间的关系在编译时就静态绑定了。
“所以一般都提倡,只继承接口不继承实现,通过组合达到代码重用的目的。”
btw:关于C语言实现面向对象机制,我目前只是初探,目的是在必要的时候能够有效的驾驭和简化用C语言开发的代码复杂度。
但如果需求已经足够简单,代码已经足够清晰,那么杀鸡焉用牛刀呢。