模块化编程


之前一直以为C语言的模块化编程就是将借口暴露在h文件中,具体实现放在c文件中。只暴露接口,不感知具体实现方法一方面是为了代码升级方便,另外一个是为了保护实现算法不被人轻易获取到。但是一篇文章让我发现原来模块化还可以这么做,对于模块化粗浅的理解让我觉得汗颜,因此对于模块化我需要重新学习,Linux是模块化实现的一个典型案例,所以C语言实现模块化编程值得研究一下。这片文章算是我学习模块化的一个学习笔记,欢迎指正。

从一篇文章说起

xusong.lie的一篇文章我对模块化有了进一步的认识,所以关于模块化我打算先从这篇文章说起。
模块化编程是指程序核心部分定义好功能的接口,而具体的实现留给各个模块去做。使用C语言进行模块化编程编程的例子:
定义Car结构体,其中包含两个函数指针。

#ifndef __CAR_H
#define __CAR_H
struct Car
{
void (*run)();
void (*stop)();
};
#endif

truck模块的具体实现:


#include <stdlib.h>
#include <stdio.h>
#include "car.h"
static void run()
{
printf("I am Truck, running...\n");
}
static void stop()
{
printf("I am Truck, stopped...\n");
}
struct Car truck = {
.run = &run,
.stop = &stop,
};

van模块的具体实现:

#include <stdlib.h>
#include <stdio.h>
#include "car.h"
static void run()
{
printf("I am Van, running...\n");
}
static void stop()
{
printf("I am Van, stopped...\n");
}
struct Car van = {
.run = &run,
.stop = &stop,
};

主模块实现:

#include "car.h"
extern struct Car van;
extern struct Car truck;
struct Car *car;
void register_module(struct Car *module)
{
car = module;
}
int main(int argc, char *argv[])
{
register_module(&truck);
car->run();
car->stop();
return 0;
}

接下来对上述源码进行编译,然后执行验证其结果:

cc -c car.h truck.c van.
cc -o car main.c *.o

若对主模块进行稍许的修改就可以让van模块运行而不需要修改任何其他东西

#include "car.h"
extern struct Car van;
extern struct Car truck;
struct Car *car;
void register_module(struct Car *module)
{
car = module;
}
int main(int argc, char *argv[])
{
register_module(&van); //更换模块
car->run();
car->stop();
return 0;
}

运行结果如下:

cc -c car.h truck.c van.
cc -o car main.c *.o
./car
I am Van, running...
I am Van, stopped...

以上就是C语言模块化的简单示例,完整的反映了模块化的过程,但是这个过程对于模块加载确实不够灵活(需要修改源码重新编译)。是否能有更好的方法不需要修改任何代码的情况下实现模块的加载和应用呢?这个问题的答案是动态加载模块,下面在Linux环境下进行实现模块化的动态加载。
头文件还是跟上面一个例子相同,未做任何修改

/* car.h */
#ifndef __CAR_H
#define __CAR_H
struct Car
{
void (*run)();
void (*stop)();
};
#endif

truck模块在上一个例子的基础上添加了get_module函数,主要作用是将struc Car truck变量传给主模块(mian)

/*truck.c*/
#include <stdlib.h>
#include <stdio.h>
#include "car.h"
static void run()
{
printf("I am Truck, running...\n");
}
static void stop()
{
printf("I am Truck, stopped...\n");
}
struct Car truck = {
.run = &run,
.stop = &stop,
};
struct Car *get_module()
{
return &truck;
}

van模块的修改于truck模块相似,添加了get_module函数与主模块交流

/*van.c*/
#include <stdlib.h>
#include <stdio.h>
#include "car.h"
static void run()
{
printf("I am Van, running...\n");
}
static void stop()
{
printf("I am Van, stopped...\n");
}
struct Car module = {
.run = &run,
.stop = &stop,
};
struct Car *get_module()
{
return &module;
}

主模块main主要添加了注册模块机制reg_module来调用各个模块的get_module函数,另外采用了标准库中的dlopen和dlsym函数来动态加载库文件

/* main.c */
#include "car.h"
#include <dlfcn.h>
#include <stdlib.h>
#include <stdio.h>
struct Car *car;
void reg_mod(struct Car*(*get_module)()) {
car = get_module();
}
struct Car *register_module(void *handle, char *module_name)
{
struct Car *(*get_module)();
//void *handle;

handle = dlopen(module_name, RTLD_LAZY);
printf("inner handle[0] %p\n", handle);
if (!handle) {
return NULL;
}
get_module = dlsym(handle, "get_module");
if (dlerror() != NULL) {
dlclose(handle);
return NULL;
}
//dlclose(handle);
struct Car *tmp = get_module();
//dlclose(handle);
return tmp;
}

若将库文件名作为参数传到主模块中,则可以在不修改任何代码的情况下实现模块的动态使用。这个方法看起来十分适合软件工程中使用,减小了代码的编译工作量,同时还能够尽最大可能的重用模块,一个设计完善的模块非常有助于其重复使用。至此,我初步了解到模块化包含的东西原来这么多,并不是我当初简单理解的头文件和实现文件分开那么简单。这当中体现了某些编程思想,值得我们学习和借鉴,当然通过初步的了解我直观的理解是模块应该可以提高编写代码的效率,同时模块化也能够帮助我们更好的对软件进行针对性的测试,卸掉其他模块单独测试一个模块就能够发现其中是否存在问题。
下面我们从编程范式,编程框架,模块化特点与实现等来学习模块化。

编程范式与模块化

编程范式(programming paradigm)指的是计算机编程的基本风格或典范模式,通俗点讲,如果说每个编程者都在创造虚拟世界,那么编程范式就是他们置身其中自觉不自觉采用的世界观和方法论。编程是为了解决问题,而解决问题可以有多种视角和思路,其中普适且行之有效的模式被归结为范式。比如我们常用的“面向对象编程”就是一种范式。由于着眼点和思维方式的不同,相应的范式自然各有侧重和倾向,因此一些范式常用‘oriented’来描述。换言之,每种范式都引导人们带着某种的倾向去分析问题、解决问题,这不就是“导向”吗?如果把一门编程语言比作兵器,它的语法、工具和技巧等是招法,它采用的编程范式则是心法。编程范式是抽象的,必须通过具体的编程语言来体现。它代表的世界观往往体现在语言的核心概念中,代表的方法论往往体现在语言的表达机制中。一种范式可以在不同的语言中实现,一种语言也可以同时支持多种范式。比如,PHP可以面向过程编程,也可以面向对象编程。任何语言在设计时都会倾向某些范式,同时回避某些范式,由此形成了不同的语法特征和语言风格。抽象的编程范式须要通过具体的编程语言来体现。范式的世界观体现在语言的核心概念之中,范式的方法论体现在语言的表达机制中。一种语言的语法和风格与其所支持的编程范式密切相关。

 编程范式的分类

类型 语言支持 描述
过程化编程 机器语言、汇编语言、BASIC、COBOL、C 、FORTRAN等 1
面向对象编程 Java C++等 2
事件驱动编程 Node.js等 3
并发范式 Erlang Go Haskell等 4
函数范式 Lisp等 5
  1. 过程化(命令式)语言特别适合解决线性(或者说按部就班)的算法问题。它强调“自上而下(自顶向下)”“精益求精”的设计方式。这种方式非常类似我们的工作和生活方式,因为我们的 日常活动都是按部就班的顺序进行的
  2. 面向对象的程序设计包括了三个基本概念:封装性、继承性、多态性。面向对象的程序语言通过类、方法、对象和消息传递,来支持面向对象的程序设计范式
  3. 主要包含事件、事件与轮询、事件处理器
  4. 并发式编程以进程为导向(Process-Oriented),以资源共享与竞争为主线
  5. 它将电脑运算视为数学上的函数计算,并且避免使用程序状态以及易变对象。函数编程语言最重要的基础是λ演算。而且λ演算的函数可以接受函数当作输入(引数)和输出(传出值)

其实编程语言的设计的时候不一定采用一种范式思想,很多语言都是多范式的,例如C可以过程化编程也可以面向对象编程(struct实现)。另外还有一些范式没有在表格列出,由于篇幅和水平限制这里不做详细说明,他们包括:

  • 逻辑式
  • 泛型式
  • 元编程
  • 切面式

由上面的表格表格可以看出作为第三代语言的C在设计之处就采用了过程化范式,“自顶向下”十分贴近我们日常解决问题的方式。

模块化思想

模块化它将事物分解为若干独立的、可替换的、具有预定功能的模块,每个模块实现一个功能,各模块通过接口(输入输出部分)组合在一起,形成最终整体。

赛车的轮胎有软胎、硬胎、雨胎等,每种轮胎花纹不同,材质不通,分别适用于在低速多弯赛道、在高速弯少赛道、在雨天潮湿赛道。赛车比赛时根据实际情况换上不通的车胎就可能更好的提高赛车的成绩。

对于简单问题,可以直接构建单一模块的程序。而对于复杂问题,则可以先创建若干个较小的模块,然后将它们组装、链接在一起,从而构成复杂的软件系统。模块化编程具有以 下优点:

  • 易设计:较大的复杂问题分解为若干较小的简单问题,使我们可以从抽象的模块功能角度而非具体的实现角度去理解软件系统,从而整个系统的结构非常清晰、容易理解,设计人员在设计之初可以更加关注系统的顶层逻辑而非底层细节。
  • 易实现:模块化设计适合团队开发,因为每个团队成员不需要了解系统全貌,只需关注所分配的小任务。另外团队可以灵活地增加人手,新人只需直接接手某个模块, 不会影响系统其他模块的开发。
  • 易测试:每个模块不但可以独立开发,也可以独立测试,最后组装时再进行联合测试。
  • 易维护:如果需要修改系统或者扩展系统功能,只需针对特定模块进行修改或者添加新模块。
  • 可重用:很多模块的代码都可以不加修改地用于其他程序的开发。

模块化编程实际上是分离关注点(Separation of Concerns,SoC)原则的具体体现。所谓关注点,是指设计者关心的某个系统特性或行为;而分离关注点是指将系统分解为互不重叠的若干单元,每个单元对应于一个关注点。在模块化编程中,以程序的各个功能作为关注点,模块划分就是分离关注点的结果。一个模块可以使用另一个模块来实现自己的功能,但除此之外模块之间最好没有交互,这是SoC原则的理想目标。

编程范式与模块化

为了满足Low coupling(低耦合), High cohesion(高内聚)的设计模式,面向过程式的编程范式(也是命令式编程范式一种)渐渐发展起来,其最大特色就是把所有程序分化为逐个子函数或者子程序(C里面几乎所有有执行意义的代码都需要在某个函数里,如main函数等)。而且它引入的变量、函数返回值等机制,使整个程序高度模块化,把状态的改变对应到不同的子函数之中去。因而也有很强的Side Effect。这种编程范式和图灵机的设想很贴合,也是编程方面比较主流的范式(软硬件条件共同决定的)。随之而来发展出了面向对象的编程范式,如果某种语言支持这种范式,意味着支持继承、多态、抽象、封装、重载等等功能。从面向过程式的编程范式到面向对象式的编程,编程范式有了一个新的提高,因为面向对象的编程范式提出了类、对象、实例等概念,一方面贴合我们的思维,另外一方面也为代码的重复利用做了很大贡献。相比于命令式编程,其模块化更为淋漓尽致,子函数的角色被模糊融合到各个类、对象中去,且各个类、对象又有独自的数据和方法,能够相互收发信息,这样写出来的程序不如传统的命令编程范式般冗长,简洁明了。但从实际上来说依然是图灵机的模型。
模块化是一种设计思想有很多应用不仅仅是在编程中,它最大的优点是降低被设计事物的复杂度。关于编程范式与模块化之间的关系,我的理解是模块化是编程范式一种具体实现方式,过程式范式可以实现模块化,面向对象也可以实现模块化等等。编程范式和模块化两者间有很微妙的联系,我想只有深入实践过才能够体会。(如何理清楚这两者之间的关系需要进一步的学习,后面会陆续补充)

模块化与编程框架

框架即framework,其实就是某种应用的半成品,就是一组组件,供你选用完成你自己的系统。简单说就是使用别人搭好的舞台,你来做表演。而且,框架一般是成熟的,不断升级的软件。框架的概念最早起源于Smalltalk环境,其中最著名的框架是Smalltalk 80的用户界面框架MVC(Model-View-Controller)。框架是一个可复用设计,它是由一组抽象类及其实例间协作关系来表达的。这个定义是从框架内涵的角度来定义框架的,当然也可以从框架用途的角度来给出框架的定义:框架是在一个给定的问题领域内,一个应用程序的一部分设计与实现。从以上两个定义可以看出,框架是对特定应用领域中的应用系统的部分设计和实现,它定义了一类应用系统(或子系统)的整体结构。框架将应用系统划分为类和对象,定义类和对象的责任,类和对象如何互相协作,以及对象之间的控制线程。这些共有的设计因素由框架预先定义,应用开发人员只须关注于特定的应用系统特有部分。框架刻画了其应用领域所共有的设计决策,所以说框架着重于设计复用,尽管框架中可能包含用某种程序设计语言实现的具体类。

理解编程框架

一、框架要解决的问题

框架要解决的最重要的一个问题是技术整合的问题,要理解这一点,我们来举一些例子:
一个做视频流应用的软件企业,他为电广行业提供整体的解决方案。他的优势在于将各种各样的视频硬件、服务器、和管理结合起来,因此他扮演的是一个集成商的角色。因此他的核心价值在于使用软件技术将不同的硬件整合起来,并在硬件的整合层面上提供一个统一的管理平台。所以他的精力应该放在解决两个问题:
如何找到一种方法,将不同的硬件整合起来,注意,这里的整合并不是技术整合,而是一种思路上的整合。首先要考虑的绝对不是要使用什么技术,而是这些硬件需要提供哪些服务,需要以什么样的方式进行管理。因此,这时候做的事情实际上是对领域进行建模。例如,我们定义任何一种硬件都需要提供两种能力,一种是统一的管理接口,用于对所有硬件统一管理;另一种是服务接口,系统平台可以查询硬件所能够提供的服务,并调用这些服务。所以,设计的规范将会针对两种能力进行。另一个问题是如何描述这个管理系统的规范。你需要描述各种管理活动,以及管理中所涉及的不同实体。因为管理系统是针对硬件的管理,所以它是构架在硬件整合平台之上的。

二、什么要用框架?

因为软件系统发展到今天已经很复杂了,特别是服务器端软件,设计到的知识,内容,问题太多。在某些方面使用别人成熟的框架,就相当于让别人帮你完成一些基础工作,你只需要集中精力完成系统的业务逻辑设计。而且框架一般是成熟,稳健的,他可以处理系统很多细节问题,比如,事物处理,安全性,数据流控制等问题。还有框架一般都经过很多人使用,所以结构很好,所以扩展性也很好,而且它是不断升级的,你可以直接享受别人升级代码带来的好处。
框架一般处在低层应用平台(如J2EE)和高层业务逻辑之间的中间层。软件为什么要分层?为了实现“高内聚、低耦合”。把问题划分开来各个解决,易于控制,易于延展,易于分配资源等。

三、为什么要进行框架开发?

框架的最大好处就是重用。面向对象系统获得的最大的复用方式就是框架,一个大的应用系统往往可能由多层互相协作的框架组成。
由于框架能重用代码,因此从一已有构件库中建立应用变得非常容易,因为构件都采用框架统一定义的接口,从而使构件间的通信简单。
框架能重用设计。它提供可重用的抽象算法及高层设计,并能将大系统分解成更小的构件,而且能描述构件间的内部接口。这些标准接口使在已有的构件基础上通过组装建立各种各样的系统成为可能。只要符合接口定义,新的构件就能插入框架中,构件设计者就能重用构架的设计。
框架还能重用分析。所有的人员若按照框架的思想来分析事务,那么就能将它划分为同样的构件,采用相似的解决方法,从而使采用同一框架的分析人员之间能进行沟通。

编程框架的模块化

总而言之编程框架是库、组件、结构件等的有效组合,而编程框架的内部的组合方法、设计模式、框架的管理逻辑等等都可以按照模块化的思想来分解和实现。
通过这两章的描述分析我们可以看到不同粒度的编程设计思想与模块化之间的关系,所有这一切的出发点是为了提高编程的效率,降低解决问题的复杂度和成本。

模块化的特点

  • 易于理解
  • 易于测试和调试
  • 易于代码重用
  • 易于修改和改进

模块支持单一数据结构上的操作

  • 接口声明的操作,而不是数据结构
  • 实现隐藏在客户机(封装)
  • 使用编程语言的特性,以确保封装

分配和回收处理模块的数据结构

  • 函数和变量的名字以modulename_开头
  • 提供尽可能多的通用性/灵活的接口
  • 使用空指针允许多态性

C语言模块化实践

./
├── func.h
├── main.c
├── math.c
└── web.c

// filename: main.c
#include <stdio.h>
#include <conio.h>
#include "include/func.h"
int main()
{
int n1 = 1, n2 = 10;
printf("从%d加到%d的和为%ld\n", n1, n2, sum(n1, n2));
printf("从%d乘到%d的积为%ld\n", n1, n2, mult(n1, n2));
printf("OS:%s\n",OS);
printf("Power By %s(%s)", getWebName(), getWebURL());
getch();
return 0;
}
// filename: math.c
// 没有使用到 func.h 中的函数声明或宏定义,也可以不包含进来
#include "../include/func.h"
// 从 fromNum 加到 endNum
long sum(int fromNum, int endNum)
{
int i;
long result = 0;
// 参数不符合规则,返回 -1
if(fromNum < 0 || endNum < 0 || endNum < fromNum)
{
return -1;
}
for(i=fromNum; i <= endNum; i++)
{
result += i;
}
// 返回大于等于0的值
return result;
}
// 从 fromNum 乘到 endNum
long mult(int fromNum, int endNum)
{
int i;
long result = 1;
// 参数不符合规则,返回 -1
if(fromNum < 0 || endNum < 0 || endNum < fromNum)
{
return -1;
}
for(i=fromNum; i <= endNum; i++)
{
result *= i;
}
// 返回大于等于0的值
return result;
}
// filename: web.c
// 使用到了 func.h 中的宏定义,必须包含进来,否则编译错误
#include "../include/func.h"
char* getWebName()
{
return WEB_NAME;
}
char* getWebURL()
{
return WEB_URL;
}

// filename: func.h
#ifndef _FUNC_H
#define _FUNC_H
// 用宏定义来代替全局变量
#define OS "Windows 7"
#define WEB_URL "http://www.baidu.com"
#define WEB_NAME "百度"
// 也可以省略 extern,不过为了程序可读性,建议都写上
extern long sum(int, int);
extern long mult(int, int);
extern char* getWebName();
extern char* getWebURL();
#endif

 

写在最后

TODO(edony): 模块化的总结工作和模块化实践需要再深入一点

(未完待续)

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s