Log4c中的接口实现分离 – 以Appender为例
概要
以log4c V1.2.1中的appender为例,说明接口实现的分离,及其价值。
一, 接口说明
1. 基于句柄(opaque data structure)的操作
struct __log4c_appender; // opaque data structure
typedef struct __log4c_appender log4c_appender_t; // 以后,所有客户的访问要以此类型作为句柄,通过提供的接口进行操作。(针对这种opaque data structure,只能定义这些类型的指针,不能定义值,因为定义值需要知道结构体的确切大小和field的layout状况)
可进行的操作如下:
// appender的创建和销毁接口
LOG4C_API log4c_appender_t* log4c_appender_get(
const char* a_name);
LOG4C_API log4c_appender_t* log4c_appender_new(
const char* a_name);
LOG4C_API void log4c_appender_delete(
log4c_appender_t* a_appender);
// appender的各种accessor
LOG4C_API const char* log4c_appender_get_name(
const log4c_appender_t* a_appender);
LOG4C_API const log4c_appender_type_t* log4c_appender_get_type(
const log4c_appender_t* a_appender);
LOG4C_API const log4c_layout_t* log4c_appender_get_layout(
const log4c_appender_t* a_appender);
LOG4C_API void* log4c_appender_get_udata(
const log4c_appender_t* a_appender);
LOG4C_API const log4c_appender_type_t* log4c_appender_set_type(
log4c_appender_t* a_appender,
const log4c_appender_type_t* a_type);
LOG4C_API void* log4c_appender_set_udata(
log4c_appender_t* a_appender, void* a_udata);
LOG4C_API const log4c_layout_t* log4c_appender_set_layout(
log4c_appender_t* a_appender,
const log4c_layout_t* a_layout);
// appender的操作,这些操作可以由log4c_appender_set_type进行改变
LOG4C_API int log4c_appender_open(log4c_appender_t* a_appender);
LOG4C_API int log4c_appender_append(
log4c_appender_t* a_appender,
log4c_logging_event_t* a_event);
LOG4C_API int log4c_appender_close(
log4c_appender_t* a_appender);
// debug用接口
LOG4C_API void log4c_appender_print(
const log4c_appender_t* a_appender, FILE* a_stream);
LOG4C_API void log4c_appender_types_print(FILE *fp);
2. 可定制的操作
typedef struct log4c_appender_type {
const char* name;
int (*open) (log4c_appender_t*);
int (*append) (log4c_appender_t*, const log4c_logging_event_t*);
int (*close) (log4c_appender_t*);
} log4c_appender_type_t;
所有appender的可定制操作都封装到了该结构体中。 值得注意的是三个函数的参数的类型是log4c_appender_t的指针,也就是说,这些函数被调用时,log4c_appender_type的对象未被作为参数传入。需要时可由log4c_appender_get_type函数来得到。结构体抽象了打开关闭和append操作。针对该结构体,有如下两个操作
// appender_type的操作,将所有appender_type做成hash表
LOG4C_API const log4c_appender_type_t* log4c_appender_type_get(
const char* a_name);
LOG4C_API const log4c_appender_type_t* log4c_appender_type_set(
const log4c_appender_type_t* a_type);
3. 接口描述
A. 创建,由log4c_appender_new进行创建。
B. 改变行为,通过各种accessor来改变appender的默认行为。特别是log4c_appender_set_type来改变行为, log4c_appender_set_udata来设置type所需的context;log4c_appender_set_layout来改变layout。
C. 所用的模式。可以认为是strategy模式。Appender是context,type和layout属于strategy。
二, Appender的实现
1. 句柄结构体的定义。
一接口说明中只是定义了appender所提供的接口,及句柄的未完全定义。句柄的完全定义如下:
struct __log4c_appender
{
char* app_name;
const log4c_layout_t* app_layout;
const log4c_appender_type_t* app_type;
int app_isopen;
void* app_udata;
};
各种accessor所操作的对象都能在该结构体中找到相应的对象。
2. app_udata,见好的设计思想总结的3。
3. 接口的实现
有了以上的定义,接口的实现是不言自明。
三, Appender的使用步骤
1. 读取配置文件。
2. 根据appender的名字,调用log4c_appender_get获得或创建appender对象。
3. 如果有appender type,从全局的appender type hash中获取appende type,并设定到appender 中。
4. 如果有appender layout,从全局的appender layout hash中获取layout,并设定到appender中。
5. 在调用log4c_appender_append时,如果还没有open,首先调用appender type的open函数,并进行append操作。
四, mmap type的实现
1. 定义log4c_appender_type的实例,设置名字及三个函数指针。
2. 调用log4c_appender_type_set函数,向log4c注册该mmap type可用。
3. open函数中,创建udata,并设置所需要的信息后,调用log4c_appender_set_udata设置到appender对象中。
4. close函数释放udata占用的内存资源和OS资源。
五, stream type,syslog type的实现同mmap基本一样。
六, 好的设计思想总结
1. 基于句柄的接口,做到了最大化的信息隐藏。除了该句柄和提供的接口,客户看不到实现的任何细节。这样实现可以任意的改变,只要不改变接口本身。Windows平台的CreateFile等API都是基于这样的设计理念。
2. 策略模式。Open,close,append三个操作随不同的appender type而有不同的行为。所以将操作抽象成为函数指针。并将这三个操作进一步抽象为appender type构造体。这样将复杂度分离了。避免了大而复杂的对象。这里函数指针在设计中体现出相当重要的价值。(OO的话,就是抽象基类或接口类)。意识到这种设计的好处并不困难,而真正的困难在于能够正确的提取出这样的函数指针(接口类)来隔离关注点,隔离复杂度,形成统一的抽象。幸运的是,我们不需要第一次就能够正确的分离这些关注点,我们可以渐进的修改我们的设计,直到我们认为OK为止。构建一个prototype,待我们真正的理解了问题之后,扔掉prototype,重新再做一次设计。
3. 原来我认为app_udata只是由appender type对象访问,为什么不直接放在appender type对象中,而偏偏要放在appender对象中。后来我明白了,因为appender type对象是一个服务提供者,该服务提供者可以被任何appender对象所使用。所以除了name,及三个函数之外,appender type对象不能包含特定与任何appender对象的信息。所以,如果需要与appender对象关联的特定信息时,作为解决方案,是把该信息放在appender对象中,而在appender type对象可以在必要的时候可以访问与appender对象关联的特定信息。这就是app_udata及其accessor出现的原因。(个人猜测)
4. 源代码的组织。在头文件中只放客户希望看到的接口,不能在头文件中出现实现细节相关的任何信息。