前言:
在软件架构中,一个重要的任务就是切分系统,而切分系统进程涉及到一个基本的概念,如模块(Module)、组件(Component)、包(Package)、对象,本文澄清他们的区别。
组件(Component)和模块(Module)是一对容易混淆的名词,也常常被用来相互替换。
两者是否有差异往往取决专业背景、所在领域、以及视角。
从设计上来看,组件强调复用,模块强调职责(内聚、分离),或者说组件是达到可复用要求的模块。
模块、组件和对象这三个名词,是软件开发中非常常见的说法。在很多软件平台、库、框架中,都使用这三个名词作为描述其复杂结构的单元结构。模块、组件、对象三者虽然有相似的含义,但是,也有非常大的差别,本文探究这三个概念的异同目的,并非仅仅是规范这些用语,而是希望能对复杂软件的解构单位,作一次较完整的思考。
备注:
在有些场合和书籍中,上述概念的关系是:函数 --》模块--》组件--》包
1. 追溯历史
孙子兵法云:“制众如治寡,分而治之”。意思是:处理数量庞大而复杂的事物,其实和处理简单、单一的事物一样简单,关键是要把处理的目标分解开。
软件系统的发展过程里,也对分解有很多不同的尝试。
1.1 函数的出现
早期结构化编程流行的阶段,软件是一个被层层分解的函数,每个函数都被划分成更多,具备平行含义的子函数。整个程序的结构就好像一本分类学书籍,由一个主题目main(),划分成非常多的子标题,子标题下面又分为更小的标题,形成一个树状的结构,如下图所示:
1.2 单函数模块的出现
如果我们需要对这样一个程序的某部分代码重用,也就是说抽取其中一部分代码,放到其他程序里面,那么,一般来说都要以“函数”为包装形式,否则,拷贝整段代码的缺点是显而易见的。
函数用作来重用代码,天然的有其接口形式:参数与返回值(或者是输入参数与输出参数)。因此,一个函数,就能成为一个封闭的代码块,具备固定的输入和输出接口,多个功能相关的函数,组合在一起,我们称之为模块。
1.3 工具模块的出现
然而,当我们在编写“函数模块”的时候,往往会发现,针对同一块数据,往往需要多个函数来一起操作。比如我们操作文件,针对特定的一个文件,我们需要打开、添加、删除、关闭这个文件。如果每个操作都是一个函数,那么我们的输出参数往往都需要一个代表文件的指针;而这个指针,又往往是其他函数的输入参数。——这种做法,实际上是破坏了模块的重用性,因为针对“模块”这个概念,我们设想的是,能简单使用一些输入参数,就能获得操作结果,而不希望还要关心输入参数的状态,以及维护输出参数的状态。由于我们编写的大多数商业系统中,都是针对一些数据进行操作,而不仅仅是进行某些数据的运算,所以数据状态反而成了程序的核心,各种所谓的“模块”,都是为了修改、操作某些特定数据的工具而已。是一组函数的组合。
也许有人会说,这样的工具模块不也是挺好的吗?当然,从代码重用的角度来说,模块和工具都可以很方便的被重用在需要的地方。但是,我们希望被重用的代码,是能够比较简单的去使用的,如果使用“工具型函数”,需要用户去小心的维护一个复杂的数据模型,那么就达不到“简单”的重用代码的目的了。举个例子,如果我们有10个工具函数,都是操作同一个数据指针的,而这些函数的调用还是有一定的调用顺序或其他逻辑约束的,那么我们在使用的时候,就要认字仔细的查看例程,彻底搞明白这10个函数的关系,然后按设计者的思路小心翼翼的去使用。否则,如果只是看了下函数手册就用起来,很可能就会碰到诸如“未定义”的结果这类奇怪的错误。
1.4 对象的出现
显然,我们对于“模块”的追求,无法简单的用函数这个工具来实现,因此人们想到,我们能不能把数据和函数组合起来,这样使用的时候,就不需要在使用的时候去维护复杂的状态。——因此诞生了面向对象。这里的“对象”,指的就是把数据,和操作这些数据函数,根据内在逻辑封装起来的单位。由于有了这种封装,一个对象除了代表业务数据状态以外,还附带了一系列的“方法”,这些“方法函数”是可以随时随意的调用,而用来操作对象的。
虽然这些“方法函数”同样还是可以设置“输入参数”和“输出参数”,但是我们完全可以把方法函数所依附的对象(this对象),作为自由操作的输入或输出参数。我们在编写方法函数的时候,就可以有意识的去管理所依赖的数据状态——this对象,这样任何一个方法函数,都可以明确而唯一的操作this的内容,而无需用户去操心。
举个例子,如果我们用面向对象的类库去操作文件,我们只需要创建一个File对象,就可以随意的删除、更新、修改这个对象的内容。如果这个对象对应的文件没有打开,或者不存在,那么File对象本身会有一个状态值做记录,那些删除、更新的方法就可以先判断一下这个状态值,从而返回“错误”提示。这样就可以避免用户去按照某些逻辑约束使用各个方法,极大的降低了对象作为“模块”的学习难度。
1.5 组件的出现
对象这个概念,对于那些希望把数据+操作构建成模块的情况,是非常合适的。但是,他也有一些缺点。其中有一个非常典型,就是对象模型是一个编程语言的概念,本质上一个对象只是内存中的一堆数据。然而我们要使用的模块,往往不止是内存中的一堆数字就能解决问题的。比如我们需要管理数据库中的一个表,或者操作一个GUI的按钮,又或者控制一个游戏里面的动画角色……这些都不止用内存中的“属性”能满足。
这个时候,人们依托IDE工具,把许多需要复用的复杂数据,和对象模型关联起来,封装成一个个可以根据预先约束的用法去使用的模块,这就是组件了。组件一般会比对象的约束要多,因为每一类组件,都有明确的使用接口,以便能“组合”到某个框架里面。比如JavaBean规范规定,所有这类的组件,必须要以getter/setter的形式对外提供属性的读写。又比如Flex规范中,组件甚至可以仅仅用XML来描述,而不必要是某种程序代码。
总体来说,所谓组件,是在某套使用规范下,特别构建的软件模块。这种模块很多依托对象模型,有所谓“属性”和“方法”。但是这些属性和方法,为了能提供更直观方便的使用接口,一般会有所约束。一旦满足这些约束,开发者重用这种模块的时候,甚至是不需要用编程语言来调用这些“属性”、“方法”。所以组件和对象的差别,往往是在于其约束方面。很多组件都要求对象从某个基类派生,或者要有一个主动注册和校验的程序,才能从对象编程组件。但是反过来一般组件都提供某种编程语言下的对象模型,让用户可以对其编写代码。
因此,我们可以看出,模块、对象、组件之间是有一定的关系,但并不完全等同。一般来说,我们喜欢把任何可重用的代码都成为模块,我们希望模块是简单的、仅仅通过输入输出就能控制的重用代码,所以其含义是最广泛而通用的。可惜现实中能很简单使用的模块并不多,为了能处理复杂的问题,人们开发了很多编程的工具,其中一些为了解决特定问题,比如数据库、GUI、游戏,都强化了一些模块的使用方法,让可重用的代码,能更简单的投入使用,而这些易用性上的好处,都需要付出遵守一定的规范的代价。
这些能在特定问题解决框架下运行的模块,就成为了组件。虽然模块和组件本身都不需要采用面向对象的模型,但是面向对象作为编程上的一个重要概念,能帮助使用者理解和操作模块或者组件,并且因为其封装管理数据状态的特征,能降低编程上的复杂程度,更容易对业务领域建模,所以很多模块和组件,背后都采用了面向对象的概念来打造其编程接口和表现形式。
应用(Application):
应用系统,由多个Module组成,用方框表示。
模块(Module):
中文为模块。它的核心意义是分离职责,属于代码级模块化的产出。
模块是从代码逻辑的角度进行划分的;方便代码分层开发,保证每个功能模块的职能单一。
本身是一组具有一定内聚性代码的组合,职责明确。对外的接口可以是松散的,也可以是集中的。
SEI的定义如下:
An implementation unit of software that provides a coherent set of responsibilities.
它以问题分解的形式,来解决软件设计问题。它更强调一个内聚的概念,形式上可以是Java中的包,也可以是一个源代码目录。
一个Module是由一组Component构成,用正方体表示。
组件(Component):
中文称为组件,或者构件。
组件是程序设计角度抽象出的可复用的单元,而且大多数时候是站在技术视角做的抽象。
一个模块可能有多个组件,一个组件也可能被多个模块使用。
组件可以是一个SDK、一个工具包、一个方法。
组件使用非常比较广泛,它的核心意义在于复用,相对模块,对于依赖性有更高的要求。
表示一个可以独立提供某方面功能的物件,用UML的组件图表示。
包(Package):
应用程序、模块、组织,都是功能的组织形式,
Package是一种目标系统文件的组织形式,和粒度不是一个维度的,也就是说,一个Component可以包含多个Package,一个Package也可以包含多个Component