论坛首页 综合技术论坛

思考软件开发(1)——面向对象的前前后后

浏览 7668 次
精华帖 (3) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2004-03-29  
这是我一直想写的一系列文章的第一篇,我始终没有想好,到底什么时候算是一个合适的时机,因为不断的思考,似乎总也没有尽头,而且要说彻底想通,真不知道是不是妄想,所以我打算先把已经有一定结论的思考写下来,请大家多多批评。

思考软件开发(1)——面向对象的前前后后

面向对象,这个概念真是了得,每一个程序员,都肯定听过它,学过它,思考过它,为它着迷,为它困惑,为它...
但是,为什么是OO呢?为什么要OO呢?面向对象已经像真理一样的放在我们面前,为什么很多时候,我们还是觉得不对劲呢?
我一直对历史很感兴趣,因此也很想分析一下关于面向对象的历史。在这个思想出现之前,编程是怎么样的?在这之后又是怎么样的?

当然,这篇文章不是一篇严格意义上的“OO传”,而是一种概念演进上的探讨,也许并不是很符合时间的序列。

第一章:对于面向过程思想的分析

第一部分:执行过程

第一节:消除冗余

最早的程序是什么样的呢?让我们回忆一下吧。在那个遥远的年代里,每一个程序员都是天才,因为只有天才,才有可能理解那似乎毫无意义的0和1的千变万化的组合。是的,所有的程序,只有两种符号,0和1。

理解这样的程序很困难吗?其实不困难,其实非常的简单。不要认为我在说大话,真的很简单。因为,只要你的思考,能够和电脑一样,你就会认为“它”是非常简单的。

那时候的电脑,体积虽然庞大,其实功能并不强大,指令的字节数,也不过就是8位,甚至只有4位。记住那少得可怜的命令,真是易如反掌。

困难的不是0和1,而是你如果要想一个程序正确运行,你就必须了解一切。了解电脑的每一个零件,每一个存储空间,以及每一个指令执行之后的后果。

如果你只是想要在某一个寄存器里放一个数字,那非常简单,放就是了。但是如果你想输出一个Hello World。那么你的脑子里,必须能够将整个过程,像放电影一样,放一遍。每一个过程状态,你都必须了然于胸。如果你想做更加复杂的程序,那么这个在脑子里预演的过程,一定会超出很多人的智力极限,只有天才,才能始终清晰的,准确地像电脑一样在脑子里预演。

然后出现了汇编语言,这种方式解决了一个大问题,哪怕是最伟大的天才,看多了0和1,也会眼花。如果天才能够不眼花,那么他的工作效率,就会更高。

然后出现了高级语言,比如说C。如果说机器语言和汇编语言是给天才用的话,那么C语言只不过是给高手用的。你不需要是天才,就可以理解C。C并不简单,但是相对于汇编,那是简单太多了。

为什么会简单那么多呢?因为你不需要了解一切,就可以令电脑为你工作。使用print函数的人,完全不需要了解为什么那些字就能够出现在屏幕上。打个比方,孩子还小的时候,我必须把他从卧室抱到书房,如果他长大了,能走路了,我只需要对他说:“到书房来”。他就会过来,而不需要我去关心他先出左脚,还是先出右脚,会不会被门撞伤。

且慢,刚才这个比喻其实是错误的,电脑远没有一个会走路的小孩那么聪明,如果门的情况没有输入到行走函数里去,它就一定会撞伤自己。所以我在调用这个函数之前,一定得很清楚他的功能,需要的参数,可能犯的错误等等。

高级语言的使用者,可以完成同样的功能,但是完成的水平天差地远,区别就在于这个使用者,是不是清楚,他所调用的函数的细节。

我读《人月神化》这本书,作者在20年后,还写下了一些他的思想的变化,其中一个引人注目的变化就是,原来作者认为,对于系统了解得越多越好。在20年OO思想大行其道之后,他终于承认,封装是有必要的。

面向过程的思维,最高的境界,就是能够了解全部,只有能够了解全部的人,才能开发出最好的面向过程的程序。我们举个例子。

最初的程序是这样的:

void main();{
	if(用户输入==1);{
		做某事1;
		做某事1;
		做某事1;
		做某事1;
		做某事1;
		做某事2;
		做某事3;
		做某事4;
		做某事5;
		做某事2;
		做某事3;
		做某事4;
		做某事5;
		做某事6;
		做某事7;
		做某事9;
		做某事10;
	}
	if(用户输入==2);{
		做某事7;
		做某事9;
		做某事10;
		做某事7;
		做某事9;
		做某事10;
		做某事7;
		做某事9;
		做某事10;
		做某事1;
		做某事1;
		做某事1;
		做某事1;
	}
}


然后我们就可以优化这个程序

void main();{
	if(用户输入==1);{
		做某事1五次();;
		做某事2~6各一次();;
	}
	if(用户输入==2);{
		做某事7.9.10各一次();;
		做某事7.9.10各一次();;
		做某事7.9.10各一次();;
		做某事1四次();;
	}
}
void 做某事1五次();{
	做某事1;
	做某事1;
	做某事1;
	做某事1;
	做某事1;
}
void 做某事1四次();{
	做某事1;
	做某事1;
	做某事1;
	做某事1;
}
void 做某事2~6各一次();{
	做某事2;
	做某事3;
	做某事4;
	做某事5;
	做某事6;
}
void 做某事7.9.10各一次();{
	做某事7;
	做某事9;
	做某事10;
}


然后我们还可以再优化
void main();{
	if(用户输入==1);{
		做某事1五次();;
		做某事2~6各一次();;
	}
	if(用户输入==2);{
		for(int i=0;i<3;i++);{
			做某事7.9.10各一次();;
		}
		做某事1四次();;
	}
}
void 做某事1五次();{
	for(int i=0;i<5;i++);
		做某事1;
	}
}
void 做某事1四次();{
	for(int i=0;i<4;i++);
		做某事1;
	}
}
void 做某事2~6各一次();{
	做某事2;
	做某事3;
	做某事4;
	做某事5;
	做某事6;
}
void 做某事7.9.10各一次();{
	做某事7;
	做某事9;
	做某事10;
}


再优化
void main();{
	if(用户输入==1);{
		做某事1N次(5);;
		做某事2~6各一次();;
	}
	if(用户输入==2);{
		for(int i=0;i<3;i++);{
			做某事7.9.10各一次();;
		}
		做某事1N次(4);;
	}
}
void 做某事1N次(int N);{
	for(int i=0;i<N;i++);
		做某事1;
	}
}
void 做某事2~6各一次();{
	做某事2;
	做某事3;
	做某事4;
	做某事5;
	做某事6;
}
void 做某事7.9.10各一次();{
	做某事7;
	做某事9;
	做某事10;
}


这样的优化,有一个非常明确的目的,同样的代码,不要重复出现,类似的代码,最好也能够归并。但是要做到这一点,其实非常困难,因为最好的优化,是需要对整个系统有一个全面的了解,才可能的。

第二节:有含义的命名

接下来如何优化呢?

不对,我们应该先不想优化的事情。

如果我们可以把软件开发的目标分为两类的话,那么一个目标是为了别人,另一个目标就是为了自己。从机器码转变到汇编指令,就是为了自己。而不断的尽一切可能的消除代码冗余,就是为了别人(当时使用软件的用户,机器都不如现在的好,硬盘不大,内存极小)。当然同样也对自己有好处,因为做同样事情的代码,只在一个地方存在。

如果软件开发人员没什么追求,那么前面的两种努力也就够了,但是如果想要更方便自己呢?给函数命名是一个关键的办法。

每一个机器指令,都可以有一个“稍微有点意义的命名”,那么一个完成了更多任务的函数,应该有一个更加有意义的命名。这不只是为了满足程序员的虚荣心(啊,我开发了一个ScreenPrint函数),也是其他程序员(包括他自己)能够更加清晰、快速、方便的理解这个函数。

比如说:
void main();{ 
   if(用户输入==1);{ 
      拉警报(5);; 
      通知各单位();; 
   } 
   if(用户输入==2);{ 
      for(int i=0;i<3;i++);{ 
         对下级单位进行检查();; 
      } 
      拉警报(4);; 
   } 
} 
void 拉警报(int N);{ 
   for(int i=0;i<N;i++); 
      做某事1; 
   } 
} 
void 通知各单位();{ 
   做某事2; 
   做某事3; 
   做某事4; 
   做某事5; 
   做某事6; 
} 
void 对下级单位进行检查();{ 
   做某事7; 
   做某事9; 
   做某事10; 
} 


这样的代码,就开始有点含义了,一个初次阅读这个代码的人,可以仅仅因为自己的基本的语言能力,初步的猜测出这个程序的含义来。甚至进而猜出这整个程序的目的——“大概是关于一个单位的安全管理方面的事情吧”。

第三节:有意义的分层与分类

但是这样的命名只能在最后才能做,也就是说,我们只能用从底向上的思维方法。不断的提炼出更高级的函数。在提炼出函数之后,还要仔细的考虑这个函数的工作内容,然后给它一个恰当的命名。因为一段从“逻辑含义上没有冗余的程序”,可能非常难以命名。

这样的思维逻辑,也无法想像:先定出函数名,然后再写函数这样的工作方式。

一个稍微大一点的面向过程的程序,就一定会用到函数,我们可以把不调用任何函数的函数,称为底层函数,然后把不被任何函数调用的函数,成为顶层函数,那么其他的函数就可以称之为中层函数。如果我们完全采用自底向上的优化,会得到什么样的层次结构呢?——一片混乱。

因为这样的优化,只对计算机有意义,只有执行这个程序的计算机才能理解。而人基本上就无法理解了。

因此我们必须将函数分层,把那些基本上没有相互调用的函数,放在一起,然后给它一个有意义的命名。比如说:I/O层。

同样的事情还发生在函数的分类上,我们把做类似事情的函数,放在一起,也给它一个有意义的命名。比如说:打印模块。

有意义的分类,与有意义的分层,很有可能会丧失执行效率,造成代码冗余,但是这样的牺牲是值得的。我从《重构》这本书上看到过一句话,觉得非常经典:“写出机器能懂的代码很容易,写出人能懂的代码很难。”(大意如此)。

自底向上的思维模式,和自顶向下的思维模式,从本质上是矛盾。自底向上时,意义是不存在的,外面的世界是不存在的,各种函数和变量的命名,也是“权宜之计”,完全可以变成另外的名字,这对于电脑来说,没有任何区别。而自顶向下时,意义是必须的,整个系统的设计,就是按照真实世界的意义,再层层分解,逐步细化的。一般来说,自顶向下开发的程序,都会比自底向上开发出来的程序要“烂”一点,当然,这是在电脑的眼光看来。

但是,当系统复杂到一定程度之后,自底向上就是不可能的任务,没有人能够完成,我们只能选择折中的方案,首先大致的进行自顶向下的划分,然后分工到具体的个人后,再进行自底向上的优化开发。

特别提请注意:这里存在一个很大的矛盾,存在着两种不同的思维模式之间的碰撞,这个矛盾,即使到今天,也没有得到妥善的解决。
论坛首页 综合技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics