理解模块、组件和对象

发表于2016-03-02
评论3 1.7k浏览

  模块、组件和对象这三个名词,是软件开发中非常常见的说法。在很多软件平台、库、框架中,都使用这三个名词作为描述其复杂结构的单元结构。模块、组件、对象三者虽然有相似的含义,但是,也有非常大的差别,本文探究这三个概念的异同目的,并非仅仅是规范这些用语,而是希望能对复杂软件的解构单位,作一次较完整的思考。

  孙子兵法云:“制众如治寡,分而治之”。意思是:处理数量庞大而复杂的事物,其实和处理简单、单一的事物一样简单,关键是要把处理的目标分解开。软件系统的发展过程里,也对分解有很多不同的尝试。

早期结构化编程流行的阶段,软件是一个被层层分解的函数,每个函数都被划分成更多,具备平行含义的子函数。整个程序的结构就好像一本分类学书籍,由一个主题目main(),划分成非常多的子标题,子标题下面又分为更小的标题,形成一个树状的结构。


[青铜器的分类树状图]


  如果我们需要对这样一个程序的某部分代码重用,也就是说抽取其中一部分代码,放到其他程序里面,那么,一般来说都要以“函数”为包装形式,否则,拷贝整段代码的缺点是显而易见的。函数用作来重用代码,天然的有其接口形式:参数与返回值(或者是输入参数与输出参数)。因此,一个函数,就能成为一个封闭的代码块,具备固定的输入和输出接口,我们称之为模块。


[模块的形式]


  然而,当我们在编写“函数模块”的时候,往往会发现,针对同一块数据,往往需要多个函数来一起操作。比如我们操作文件,针对特定的一个文件,我们需要打开、添加、删除、关闭这个文件。如果每个操作都是一个函数,那么我们的输出参数往往都需要一个代表文件的指针;而这个指针,又往往是其他函数的输入参数。——这种做法,实际上是破坏了模块的重用性,因为针对“模块”这个概念,我们设想的是,能简单使用一些输入参数,就能获得操作结果,而不希望还要关心输入参数的状态,以及维护输出参数的状态。由于我们编写的大多数商业系统中,都是针对一些数据进行操作,而不仅仅是进行某些数据的运算,所以数据状态反而成了程序的核心,各种所谓的“模块”,都是为了修改、操作某些特定数据的工具而已。


[函数模块变成了工具]


  也许有人会说,这样的工具模块不也是挺好的吗?当然,从代码重用的角度来说,模块和工具都可以很方便的被重用在需要的地方。但是,我们希望被重用的代码,是能够比较简单的去使用的,如果使用“工具型函数”,需要用户去小心的维护一个复杂的数据模型,那么就达不到“简单”的重用代码的目的了。举个例子,如果我们有10个工具函数,都是操作同一个数据指针的,而这些函数的调用还是有一定的调用顺序或其他逻辑约束的,那么我们在使用的时候,就要认字仔细的查看例程,彻底搞明白这10个函数的关系,然后按设计者的思路小心翼翼的去使用。否则,如果只是看了下函数手册就用起来,很可能就会碰到诸如“未定义”的结果这类奇怪的错误。


[使用工具函数有时候就像在玩拼图]


  显然,我们对于“模块”的追求,无法简单的用函数这个工具来实现,因此人们想到,我们能不能把数据和函数组合起来,这样使用的时候,就不需要在使用的时候去维护复杂的状态。——因此诞生了面向对象。这里的“对象”,指的就是把数据,和操作这些数据函数,根据内在逻辑封装起来的单位。由于有了这种封装,一个对象除了代表业务数据状态以外,还附带了一系列的“方法”,这些“方法函数”是可以随时随意的调用,而用来操作对象的。虽然这些“方法函数”同样还是可以设置“输入参数”和“输出参数”,但是我们完全可以把方法函数所依附的对象(this对象),作为自由操作的输入或输出参数。我们在编写方法函数的时候,就可以有意识的去管理所依赖的数据状态——this对象,这样任何一个方法函数,都可以明确而唯一的操作this的内容,而无需用户去操心。举个例子,如果我们用面向对象的类库去操作文件,我们只需要创建一个File对象,就可以随意的删除、更新、修改这个对象的内容。如果这个对象对应的文件没有打开,或者不存在,那么File对象本身会有一个状态值做记录,那些删除、更新的方法就可以先判断一下这个状态值,从而返回“错误”提示。这样就可以避免用户去按照某些逻辑约束使用各个方法,极大的降低了对象作为“模块”的学习难度。


[对象就是一个自动运作的机器]


对象这个概念,对于那些希望把数据+操作构建成模块的情况,是非常合适的。但是,他也有一些缺点。其中有一个非常典型,就是对象模型是一个编程语言的概念,本质上一个对象只是内存中的一堆数据。然而我们要使用的模块,往往不止是内存中的一堆数字就能解决问题的。比如我们需要管理数据库中的一个表,或者操作一个GUI的按钮,又或者控制一个游戏里面的动画角色……这些都不止用内存中的“属性”能满足。这个时候,人们依托IDE工具,把许多需要复用的复杂数据,和对象模型关联起来,封装成一个个可以根据预先约束的用法去使用的模块, 这就是组件了。组件一般会比对象的约束要多,因为每一类组件,都有明确的使用接口,以便能“组合”到某个框架里面。比如JavaBean规范规定,所有这类的组件,必须要以getter/setter的形式对外提供属性的读写。又比如Flex规范中,组件甚至可以仅仅用XML来描述,而不必要是某种程序代码。

width="80%" height="80%">

top="10" bottom="70" left="10" right="30"/>

    right="30" bottom="40"/>

[两个以xml描述的GUI组件]



[delphi中的数据库、表空间可以用图形化修改属性]


  总体来说,所谓组件,是在某套使用规范下,特别构建的软件模块。这种模块很多依托对象模型,有所谓“属性”和“方法”。但是这些属性和方法,为了能提供更直观方便的使用接口,一般会有所约束。一旦满足这些约束,开发者重用这种模块的时候,甚至是不需要用编程语言来调用这些“属性”、“方法”。所以组件和对象的差别,往往是在于其约束方面。很多组件都要求对象从某个基类派生,或者要有一个主动注册和校验的程序,才能从对象编程组件。但是反过来一般组件都提供某种编程语言下的对象模型,让用户可以对其编写代码。


[组件和框架构成复杂的模块关系]


因此,我们可以看出,模块、对象、组件之间是有一定的关系,但并不完全等同。一般来说,我们喜欢把任何可重用的代码都成为模块,我们希望模块是简单的、仅仅通过输入输出就能控制的重用代码,所以其含义是最广泛而通用的。可惜现实中能很简单使用的模块并不多,为了能处理复杂的问题,人们开发了很多编程的工具,其中一些为了解决特定问题,比如数据库、GUI、游戏,都强化了一些模块的使用方法,让可重用的代码,能更简单的投入使用,而这些易用性上的好处,都需要付出遵守一定的规范的代价。这些能在特定问题解决框架下运行的模块,就成为了组件。虽然模块和组件本身都不需要采用面向对象的模型,但是面向对象作为编程上的一个重要概念,能帮助使用者理解和操作模块或者组件,并且因为其封装管理数据状态的特征,能降低编程上的复杂程度,更容易对业务领域建模,所以很多模块和组件,背后都采用了面向对象的概念来打造其编程接口和表现形式。


[模块、对象、组件的关系]


  当我们自己希望开发一个框架的时候,我们往往会思考,如何让用户更方便的使用这个框架啊,如何提高框架的扩展能力。如果我们的框架是比较专门的解决某个方面的问题,那么我们把扩展接口设计成组件,可能是一个好主意,因为这样能提供更友好的用户使用界面,包括图形化或配置化的用法;但是这种组件接口也带来了一定的复杂性,在用户需要扩展自己的功能时,组件和框架的接口直接限制了框架、组件的功能外延。所以,一般通用性比较高的框架或平台,其模块形式往往是类库,比如JDK或者C#的标准库,这种仅仅以对象为接口的模块,其编程灵活性是更加高的。然而,如果我们要处理的问题,更多的是数据计算,而不是数据状态的管理;或者我们要管理的数据模型高度抽象,比如Linux下以文件这个模型来管理所有的内存、外设,这样我们甚至可以不用对象模型,直接用函数就能解决问题。不过一般来说,作为业务逻辑开发领域的程序员,要处理的数据模型往往不会太简单,有大量的显示、修改、逻辑判断的需求,这方面还是构建对象模型来的更容易建模些。最后,可能会有疑问的是,函数能不能用来构建组件呢?实际上是可以的,比如我们在一些GUI编程中,如按钮的响应事件,就直接传入一个函数(VB);或者在多线程编程中,一个线程任务的接口往往就是一个函数。但是,由于函数这个模型比较简单,而且难以在空间上复制,必须和代码统一到一起,所以我们更倾向用对象来做组件,否则函数的约束也没有太多手段。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引