由“结构化编程”与“面象对象编程”争论引发的思考

gules 2010-09-09 08:25:42
前言

自我学习程序设计以来,接触过了四种编程典范(或称编程方法):结构化编程(面象过程编程)、基于对象编程(ADT)、面象对象编程、泛型编程(之所以按这个顺序排列,并不是指它们递进关系,而是我接触并开始学习的顺序)。以前,我从来没有考虑过哪个方法好、哪个方法不好……等问题,实际上当时我对每个方法都有极大兴趣。我开始粗略的浏览了一些软件工程的书籍,由于兴趣原因,我还是喜欢语言本身(具体就是C++语言)的技术多得多,我是那样痴迷于她以致于我对网络上讨论语言之间的好坏及编程方法的好坏问题不屑一顾,所以浏览软件工程的书籍也实际上还是具体到C++的设计方法与技术,对其本身研究不深。后来,由于工作原因我中断了程序设计三年之久,至到前不久我又有时间开始学习和编程,第一个想到的就是csdn。很偶然,我看到的第一篇帖子是“甚是惊诧: OO消除了switch..case了吗???”,该篇帖子的内容没有引起我的兴趣(回复内容太多),让我觉得惊讶的是那么多人参与争论且历时一年!正是这一点,引发了我对上述四种程序设计方法之间关系的思考,这是我从前从没有考虑过的。
那么我该从哪里入手呢?我最先想到的是“分而治之”,然后再考虑它们之间的关系。

结构化程序设计(基于过程程序设计)

我最先接触到的程序设计方法就是结构化程序设计,实际上在我完全没有程序设计知识的时候,我就在《计算机导论》这门课程中学到了这个“术语”。然后我开始学习Pascal语言,初次感受到结构化编程的方法,再然后我开始学习数据结构与算法(这是计算机程序设计最基础最重要的学科之一),这时我才感觉到程序设计的内涵——数据结构+算法设计。

现在回过头来看,说编程(coding)也好,说程序设计也好,说软件工程也好,都是为了生产软件。软件就是数据与指令的集合,是人们为解决问题而将现实世界概念、物体、结构与逻辑映射为计算机中的数据与指令的集合,姑且先将上述三种说法看成是同一件事——生产软件。

结构化程序设计在生产软件中是怎么做的呢?我个人的理解,就是它注重的是算法设计(过程设计)。人们在解决问题时总会用到一个工具——抽象,四种方法都利用了抽象这个工具。结构化程序设计中的抽象就是函数(有多种说法:子例程、过程、方法等),也就是算法的抽象与设计,然后实现之。那么数据结构在结构化编程中扮演什么角色呢?我认为在结构化编程中,为了实现某个算法,就离不开与之实现相依的数据结构,如下代码:

char* string = "hello, world";

char* find (char* string, char c) {
while (*string != '\0' && !(*str == c)) {
++string;
}
if (*string == '\0') return 0;
return string;
}

char* pos = find (string, 'w');

这段代码实现了“在一个字符串string中查找特定的字符c并返其第一个出现的位置”的算法,这个算法施行于“以空字符结尾的字符数组”这个数据结构上,在语言中我们可以表示为 char[] 或 char*这样的数据类型(或称为型别)。这个算法只能作用于以'\0'结尾的字符串上,因此对于其他的型别(如整数数组或链表)就不能使用。看起来算法是依赖于数据结构的,我们必须先有数据结构才能设计算法。而实际上结构化编程是先抽象出算法(过程、流程),再根据具体的实现来设计数据结构(型别)。

因此,定义抽象的算法(过程),基于此抽象算法来设计数据结构并实现算法是结构化程序设计的本质。也就是说,结构化程序设计描述的是软件中算法与型别的关系,结构化程序设计把算法与数据结构是分开来设计的,但同时它们又是相依赖的。

基于对象程序设计(ADT——抽象数据类型)

当我在学习数据结构与算法这门课程时,就已知道ADT的定义,但对比较理性的认识它是在我学习了C++语言后,C++中用class来描述它。很多人一见到class就会说面象对象,把这两者混为一谈,这应该是个误区,后面谈及。首先,从感性上讲,ADT就是产生了一个新的数据类型(型别),即自定义的型别,在C语言中我们用struct、union,在C++中我们用class或struct。其次,ADT没有把算法撇开而单纯的设计数据结构,它把算法看作是本身的一系列属性,把数据结构与算法封装在一起。

基于对象程序设计在生产软件中是怎么做的呢?个人理解就是它注重数据结构与算法的整体设计(ADT设计),这种方法考虑的是事物本身所应该具备的数据结构与行为(算法),它把事物的结构表示隐藏于后,将事物的行为表示显示于外,外界通过ADT所提供的行为接口(函数、方法)与之交互。因此,该方法把现实的事物或概念在计算机中表示为ADT,如下代码:

class String {
public:
……
char* find (char c) {
while (*string != '\0' && !(*str == c) {
++string;
}
if (*string == '\0') return 0;
return string;
}
……
private:
char* string;
};

String s;
char* pos = s.find('w');

这段代码定义了一个ADT,即自定义型别string,这个ADT定义了“一个可以查找特定字符的字符串”概念。从外界看,我们无法得知其内在的数据结构是什么,但这并不妨碍我们与之交互,一旦通过该自定义的型别生成一个对象后,我们就可以通过这个对象的find方法(函数或成员函数)来完成查找任务。实际上,C++中class最适合的用法就是用之表示概念,通过定义的概念,我们可以生成许多的对象并与这些对象进行交互来解决问题。

因此,定义抽象的概念(ADT),针对该概念(ADT)封装其特有数据结构与算法是基于对象程序设计的本质。也就是说,基于对象程序设计描述的也是软件中算法与型别的关系,它把算法与数据结构合并起来设计与实现用以表示事物概念。

面象对象程序设计

在我第一次接触到继承与多态(动态绑定)的时候,我非常茫然,我不知道它们到底要描述什么。甚至我一度认为它们只是基于对象在技巧上的扩展,这也是前面提到的许多人一见到class就说是面象对象的误区。不错,面象对象的基础的确是基于对象,但这两者之间有着本质上的差别,重要的是这两者描述了程序设计中事物的不同关系。从前面的叙述,我们知道基于对象是研究事物(一个ADT)的内在关系:它应该有哪些属性,它应该具有什么样的数据结构及作用其上的算法,它应该具备怎样的行为等等,也就是我们常说的高聚合。既然我们说基于对象与面象对象有着本质的差别,为什么前者又是后者的基础呢?要回答这个问题,先看看基于对象程序设计产生了什么?是的,它产生了ADT——在语言中就是一种自定义的型别,这种型别和语言内建的型别不同的是它还封装了作用于其内部数据结构的算法。但事情总有复杂的时候,当产生了一堆的自定义型别后,有时我们没有办法只通过单一的型别来解决问题,也就是说我们不仅需要与型别分别进行交互,我们还需要它们之间能够互相交互!而面象对象正是研究型别之间关系及如何相互交互的方法,这就是前面问题的答案。因为如果我们在软件设计的时候,还没有准确的定义出这些ADT,那又如何能准确的描述它们之间的关系呢?

面象对象程序设计在生产软件中是怎么做的呢?个人理解就是对一堆已存在的型别进行抽象,产生又一些新的型别,这些新型别一般是抽象基类或接口,原有型别通过继承于抽象基类或接口并依赖于多态机制,实现外界只需与这些抽象基类或接口进行交互而不用与一大堆型别进行关联式交互,也就是我们常说的低耦合。型别之间的关系抽象模型一般有以下几种:

1、型别A与型别B在本身概念(我们用具体类来表示概念)上有共同之处,它们都可以用另一个抽象概念来表示,那么我们就定义这一抽象概念C,用抽象基类C来表示它,同时令型别A与型别B(具体类)公有继承于型别C,这时我们一般说:A或B is-a C(“是一个”关系,但这种说法其实是含混不清的,我们这里所说的“是一个”是指程序设计中所定义出的概念A是一个概述C,而不是指现实世界中物体的分类),即x属于C则必有x属于A(返之不成立),这里x是指型别的内部数据或内部函数。可能马上会有人反对:如果x在private区段呢?注意:语言中private等控制是数据或函数的可访问性,而不是可见性,也就是说A继承于C后,C中私有的x也在A中,只不过即使在A内也不可访问,但如果x是虚函数,你可重写之(可见性)。

2、型别A与型别B在行为(算法)上有共同之处,那么把这些共同的行为抽象出来,形成接口C,我们用接口类C表示(C不具有数据,只有方法),同时令A和B继承于C。

3、型别A has-a (“有一个”)型别B,如果型别B与型别A是部分与整体的关系(C++中表现为具有相同的生命周期),那么就用组合(型别A内包含一个型别B的对象);如果型别B与型别A的生命周期不同,那么就用聚合(型别A内包含一个型别B的引用)。

4、型别A以型别B实现(“以……实现之”),那么我们令型别A私有或保护继承于型别B,当然这种抽象模型可以用组合或聚合实现之,这种模型只在“极端”的情况下用之——在型别A以型别B实现且必须使用型别B中的虚函数时。那么私有继承和保护继承有何区别呢?当然是关乎到访问控制及后续继承的问题了。

以下以接口类示例代码:

class myInterface {
public:
virtual void* find (int c) = 0;
virtual ~myInterface () {}
};

class String : public myInterface {
public:
……
char* find (int c) {
while (*string != '\0' && !(*str == c) {
++string;
}
if (*string == '\0') return 0;
return string;
}
……
private:
char* string;
};

class intArr : public myInterface {
public:
……
int* find (int c) {
int* intptr = array;
while (intptr != array + size && !(*intptr == c) {
++intptr;
}
if (intptr == array + size) return 0;
return intptr;
}
……
private:
int* const array;
size_t size;
};

// client
void* find(const myInterface& mi)
{
return mi.find (97);
}

这段代码通过抽象出String和intArr共同的行为find形成了接口类myInterface,现在客户端代码只需与myInterface交互,这即是所谓的针对接口编程。

因此,定义抽象的接口(抽象基类与接口类),针对接口封装或扩展变化的具体概念(具体类或ADT)的数据结构与算法是面象对象程序设计的本质。也就是说,面象对象程序设计描述的是软件中型别(ADT型别)之间的关系与交互方法。


泛型程序设计

初次学习泛型程序设计时,我认为它只是使型别参数化了,并没有注意到泛型编程中一些极为重要的概念:Requirement、Concept、modeling、Refinement,当然也就没法理解traits技术。在考虑这些东西之前,我们先看看泛型程序设计是如何组织数据结构与算法的。在前三种方法中我们知道,只有结构化程序设计是将数据结构与算法分开设计的(但在实现时它们时无法分离),泛型程序设计是一种试图“彻底”分开算法与数据结构设计与实现的方法。

泛型程序设计在生产软件中是怎么做的呢?我认为它以泛型算法为主导思想,使算法与数据结构成为独立的可复用组件,通过迭代器(iterator)进行胶合,或通过适配器(adaptor)进行配接,利用仿函数(functor,或称函数对象)改变运算需求。我们使用下面的泛型算法导出泛型编程的主要概念:

template <typename InputIterator, typename T>
InputIterator
find (InputIterator first, InputIterator last, const T& value)
{
while (first != last && !(*first == value))
++first;
return first;
}

这段代码实现了在[first, last)区间(半开区间)内查找value的算法。这段代码中我们看不到任何数据结构,我们只知道要查找的是一维线性空间,这个空间的数据结构是连续的还是分散我们都不需要知道。泛型find算法试图能对任何满足Requirement(需求条件)的型别起作用,在这里InputIterator的需求条件(Requirements)是:支持!=运算、支持*first(提领)运算、支持++运算。从这些需求条件我们可以看出指针能满足,所以泛型find算法的参数可以是指针;那可以是某个自定义的class吗?是的,只要这个class满足这些需求条件(C++可以对操作符进行重载)。

一系列的Requirements(需求条件)导出了一个Concept(概念)。你可以把Concept看作是一整组型别(如:char*,int*,T*,……),即型别的集合,它定义了一系列的Requirements,是一组合法的程序。泛型算法正是基于某(几)个Concept(s)的假设来实现的,实际调用时,泛型算法会根据实参的型别推导出模板(template)的输入型别,这个输入的具体型别就是Concept的一个modeling,也就是说modeling是型别集合中的一个具体型别。Rifinement(精炼、强化)定义了Concept之间的关系,如果Concept A是Concept B的Rifinement,那么符合Concept B的Requirements的所有型别均符合Concept A的Requirements,反之不然。由此,我们可看出代码中的InputIterator就是一个Concept,而C++ STL中的iterator描述的是一组Concepts:input_iterator、output_iterator、forward_iterator、bidirectional_iterator、random_access_iterator。其中forward_iterator是input_iterator、output_iterator的Rifinement(精炼、强化),bidirectional_iterator是forward_iterator的Rifinement(精炼、强化),
random_access_iterator是bidirectional_iterator的Rifinement(精炼、强化)。

如果说面象对象中继承是描述type(型别)之间的关系,那么Rifinement(精炼、强化)描述就是一整组型别与另一整组型别之间的关系,modeling则是描述一个型别和一整组型别之间的关系。

因此,定义抽象的Concepts(概念),并根据抽象的Concepts来编写数据结构与算法是泛型程序设计的本质。也就是说泛型程序设计描述的是软件中Concepts(型别集合)之间的关系,这些Concepts提供了泛型算法一个自然的分类法则。

结论

这篇文章并不是为了阐述这四种程序设计方法,只所以给出它们简单的描述是为了考查其之间的关系与区别。我从数据结构与算法的设计和实现的角度思考它们的异同,应该说它们都是运用了“抽象”,每种方法给出的抽象都是不同的性质,因此我很难回答在软件工程中应该运用某种方法的抽象,因为四种方法描述的是软件设计中不同的关系。作为解决某一特定的问题,可能某种方法比另一种方法更直接、更方便或更有效率,这时你可以说解决某某问题时谁比谁好、谁比谁差,但任何一种方法论(如软件工程)如果只尝试运用其中某种关系(方法),都是不完整的。
...全文
251 6 打赏 收藏 转发到动态 举报
写回复
用AI写文章
6 条回复
切换为时间正序
请发表友善的回复…
发表回复
gules 2010-09-10
  • 打赏
  • 举报
回复
正如楼上所说,提高生产力才是王道。
那么思考啥呢?本论坛精华区的帖子:“甚是惊诧: OO消除了switch..case了吗???”,内容我不感兴趣,让我觉得惊讶的是那么多人参与争论且历时一年!正是这一点,引发了我的思考:为什么大家这么有精力去争论这些方法的好坏,而不去想如何提高生产力呢?为什么大家一定要否定某种方法(正如楼上的“泛型编程也称作一种与OO对立的思想的话,这种思想实在是无法走远”的观点,更何况方法之间何来“对立”),或极力推崇只用一种方法呢?
不同的思想与方法描述的是软件设计中不同的关系。作为解决某一特定领域(可能是绝大部分)的问题,也许我们只用一种方法便可以解决问题,但任何一种方法论(如软件工程)如果只尝试运用其中某种关系(方法),都是不完整的。”正因为如此,才会不但出现所谓“面象方面”、“面象体系结构”、“面象……”等不同的思想与方法,所以说软件工程所尝试的是尽可能全面的考虑问题的不同关系。其实,无论程序设计思想与方法如何推陈出新,它们都是为了更好的解决问题(或说为了提高生产力),我们面对这些方法应思考其本质、掌握其含义,才能在解决问题中真正用好它。
“埋头干事,抬头看路”。
iambic 2010-09-09
  • 打赏
  • 举报
回复
思考啥。直接学Lisp,你就知道你在这上面的思考完全是在浪费时间了。提高生产力才是王道,写代码就是把你想的写出来。
另外你说的这四种,基本上是在C++里才分离出现的东西,把泛型编程也称作一种与OO对立的思想的话,这种思想实在是无法走远。
dingshaofengbinbin 2010-09-09
  • 打赏
  • 举报
回复
顶顶顶!!
maoxing63570 2010-09-09
  • 打赏
  • 举报
回复
感觉思想很高深,有的还不是很理解,尤其是说到后面的时候就不怎么理解了,只恨自己的水平还差,还有待提高,个人感觉楼主的数据结构是学的很好的,学到了本质。当然本人菜鸟,怎么也轮不到我来这里胡乱说一通,呵呵
yutaooo 2010-09-09
  • 打赏
  • 举报
回复

挺好的。顶一下。

再推荐 代码大全2。 这是我看到过的,对抽象这个概念解释的最清楚,深入的书了。
maoxing63570 2010-09-09
  • 打赏
  • 举报
回复
很好,很强大

64,648

社区成员

发帖
与我相关
我的任务
社区描述
C++ 语言相关问题讨论,技术干货分享,前沿动态等
c++ 技术论坛(原bbs)
社区管理员
  • C++ 语言社区
  • encoderlee
  • paschen
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
  1. 请不要发布与C++技术无关的贴子
  2. 请不要发布与技术无关的招聘、广告的帖子
  3. 请尽可能的描述清楚你的问题,如果涉及到代码请尽可能的格式化一下

试试用AI创作助手写篇文章吧