3.2 面向对象与面向过程
20世纪60年代开始,面向过程的程序设计语言获得广泛应用,那时的主要问题是困扰软件开发人员的软件危机。引用工程化的管理方法和采用结构化程序设计语言是解决软件危机的良方。所以,以Pascal语言、C语言为代表的结构化语言深受欢迎。这种状况一直持续到面向对象程序设计语言如C++和Java等语言的兴起。
3.2.1 问题与解决问题的思维方式
Kristen Nygaard说过:编程即理解。用程序解决实际问题,首先要分析问题,理解问题,得出解题思路和解题步骤,然后选用具体的语言实现解题步骤。
事实上,语言的作用不仅体现在程序的实现方式,而且关系到分析问题的方式方法。语言给思维定型,也给思维定界。
1.面向过程方式
分析出解决问题的步骤,然后用函数(Function)或者程序过程(Procedure)把这些步骤一步一步地实现,程序执行的过程就是按一定顺序调用函数并且执行函数的过程。解决问题的过程就是函数执行的过程。所以面向过程(Procedure-Oriented)的核心是,分析事物过程,用函数来实现。
有些计算问题,适合于采用面向过程的方式来处理。
【例3.2】编程完成如下计算。
【分析】这个计算过程就是循环地得到1~21这11个奇数,调用已定义的计算阶乘值的函数,并对函数返回的阶乘值累加求和。
s= 1!+3!+5!+…+21!
这个计算过程以及所有诸如此类的问题的特点是:计算过程的每一步,包括循环地给出操作数n、调用计算阶乘的函数f、累加求和s等动作在时间先后顺序都是确定的。这个过程参见图3-5。
计算阶乘是一个循环(或者递归)过程,计算阶乘的和的过程也是一个循环过程。在方法中实现计算某数阶乘,在主方法main中给出1~21这11个操作数的循环,调用求阶乘方法。
求阶乘的方法包括递归和非递归实现。
图3-5 程序执行过程
【代码】
public class Example3_02{
//非递归方法计算阶乘
static long non Recur Factorial(int n)
{
long facto = 1;
for(int i = 1;i < n; i++)
{
facto *= i;
}
return facto;
}
//递归方法计算阶乘
static long recur Factorial(int n)
{
if(n == 1||n ==0) return 1;
else
return n* recur Factorial(n-1);
}
public static void main(String[] args){
long sum = 0;
int i = 1;
for(; i<=21;i=i+2){
sum += non Recur Factorial(i));
//或者sum +=recur Factorial(i);
}
System.out.println(“sum= ”+ sum);
}
}
这类问题我们在第2章中接触了许多。
在面向过程语言解题过程中,编程人员是总指挥,他们控制程序的执行过程。
在面向过程解题思路中,函数本来是整个过程的一部分。程序员把它们从其原来的位置提取出来,作为一个相对独立的单元,是为了让它们可以被重复调用,为了让程序变得短小一些,仅此而已。在有的语言中,为了提高执行速度,还可以让函数再“站回”到它本来的位置上,就是所谓内联(inline,联想stand in line便知内联的含义了)。因为在函数的执行过程之外,函数的调用/返回动作本身也耗费时间。就像看资料花时间,查资料、还资料也要花时间一样。
函数是否提取出来,都不影响面向过程程序执行过程的本质:程序中的语句按照顺序、分支或者循环结构被执行,程序的执行逻辑是固定的。
如果解题过程是确定的,面向过程是合适的。但是,有些问题,涉及很多不同类对象。它们相互作用,共同影响,事物过程是不可预知的。面向过程思维不适用于解决这类问题。
分析下面几个实际场景,观察对象行为的不可预知性和系统运行的不确定性。之后,请认真思考一下,你愿意做控制一切的“总指挥”吗?即使愿意,你能不能指挥各个对象的行为?在编程阶段,预先设定在执行阶段出场的各种对象的行为合理吗?
2.面向对象方式
程序员交出控制权,把权利还给对象自己。事实上,这权利本来就属于它们自己。程序员专注于分析系统,找出其中的对象和类,分析一个事物类和对象与其他事物类和对象之间的关系。在程序中再现它们在现实世界中原始的状态和模样、不同类和对象之间的关系、对象的行为方式以及整个系统的运行方式。
观察下面的几个场景,体会其中蕴含的面向对象因素。
计算机系统中,中央处理单元(Central Processing Unit,CPU)并不独断专行,相反,它以民主的方式工作。它在大部分时间执行一个主程序,当某外部设备需要它时就以中断方式(类似于有人来敲门)打断CPU,CPU在紧急处理完一些必须处理的工作(入栈保护,类似你读书被别人打断时需要一个书签标识当前位置!),转而为这个外设处理中断服务。例如,打印机检测到没有打印纸了,希望主机(其实就是CPU)及时通知计算机用户!或者打印机已经打印完上一批数据,希望CPU再发给它下一批打印数据!如图3-6所示。可见,系统中各种组件和设备既各行其道,又协同工作。程序员无法按照面向过程思路提前安排好什么时间打印机无纸,什么时间打印机发生故障。
图3-6 CPU响应打印机中断请求示意图
为了理解图3-6所解释的机制,下面简要地解释一下中断的相关概念。
首先,类比说明什么是中断。中断就是打断(Interrupt)、暂停。假设你的名字叫CPU,你的主程序(与那个中断服务子程序相对而言,成为主子关系)是在认真读一本很有趣的书。你读书之前叫了个外卖,外卖送到时按你的门铃,门铃声打断了你读书,这就是“中断”。你在书的当前页放一个书签,记录了“断点”,然后去开门,付款,然后坐下来品尝美味。这就是“响应中断执行中断服务子程序”。最后,你“返回主程序”,找到“断点”,继续读书。后面,一定还有其他人来打断你。类似地,在计算机系统中,向CPU发中断请求的也不只打印机一种设备。
图3-7 内存数据送显示器的过程示意图
然后,我们再用对比法理解计算机的操作机制,参见图3-7。我们知道CPU用于计算和控制,显示器用于显示信息。因为显示器和CPU一样是电子设备,工作速度相当。因此CPU向显示器输出数据进行显示采用了与中断方式不同的控制方式:编写一个从内存读数据,输出到显示器的子程序(类似函数),在CPU执行主程序过程中,需要输出数据时,即调用子程序,子程序执行完返回到主程序继续执行,这个过程由CPU主导。由于显示器不像打印机存在这样那样的不确定性,它的显示过程可以预先在程序中设定,计算机的这种工作方式是面向过程的。而采用中断方式工作则是面向对象的。在中断方式下, CPU和打印机是两类对象,它们协调动作,共同决定计算机如何工作。
对比一下以上所说的两种情况,有助于深入理解面向过程和面向对象的思想。
在电商系统中,客户、商家、商品是3个不同事物类。客户和商家操作总体描述如图3-8所示。具体到某个客户和某个商家,他们进行某种操作的时间、顺序和内容是他们自己的事情,比如商家决定商品打折的方式是商家自己根据市场总体供求关系和商品销售情况决定的。顾客买或不买某个商品也是顾客根据自己的需求和财力决定。交易双方无法预知交易过程,编程人员当然也不能够用面向过程方法提前对买卖这件事情的执行过程进行安排。
图3-8 电商系统中两种角色对象的行为
面向对象语言的解题思路是:在程序中定义与现实世界中相一致的事物类和对象,定义其属性和行为,对象按系统状态(包括和其他对象的交互)决定自身的行为。
在交通系统中,有十字路口的红绿灯和在路上不同方向行驶的汽车。路上有没有车,车多或少,行驶速度,启动与停车,红绿灯颜色的切换等都不是编程人员可以事先设定的,应该由对象自己根据系统状态,根据对象间消息通信来决定它们自己的行为和操作,如图3-9所示。车要看红绿灯颜色决定行车还是停车,红绿灯要根据时间切换颜色。而程序员需要研究这些不同的类对象的属性和行为,以及它们之间的关系,是这些因素而不是程序员决定系统的运行。
图3-9 车辆行驶示意图
通过以上3个场景的分析,体会一下在一个应用程序中存在什么样的对象,存在哪些不同的类。对象之间传递什么样的消息,消息和程序运行的状态是怎样决定对象行为的。
拓展知识
克瑞斯坦·内加德(Kristen Nygaard,1926—2002年),出生于挪威奥斯陆,是著名的计算机科学家,社会活动家。1948年大学毕业后,他进入挪威国防研究院NDRE,从事有关计算、程序设计和运筹学方面的工作。经过不断的努力,他成为SIMULA-67语言的创始人、面向对象技术的先驱,曾获得冯•诺依曼奖和第36届图灵奖。Simula 67于1967年5月20日发布,之后,在1968年2月形成了正式文本。Simula 67被认为是最早的面向对象程序设计语言,它引入了后来面向对象程序设计语言所遵循的所有基础概念:对象、类、继承。
3.2.2 面向对象的内涵
究竟什么是面向对象?我们需要一点咬文嚼字的精神,需要一点穷理尽微的劲头。
早晨起来面向太阳,不是只要求站立的姿势,而是要求怎么看世界的。看到了早晨的太阳在哪里,就知道了它红彤彤、朝气蓬勃的样子,还知道了前面是东,后面是西的方位,太阳为方向提供了参照。
面向对象(Object-Oriented,OO)也是同样的道理。面向它们,而不是背离它们,不是对它们的存在视而不见。
面向对象,把对象当作整体,而不是撕裂的条块分割的东西,因为对象以整体存在于世。这有点像以人为本的理念。
基于这个基本理念,面向对象语言(OOPL)中把对象的属性和操作封装起来,作为一个整体。认为对象的操作是对象自己的事情,这在思维方式和解决问题方式上是回归自然。
基于这个理念,易于发现了对象之间的继承关系,例如学生干部类从学生类继承了许多,又新增了某些属性和操作,例如职务。这使得代码可以重用(Reusable),编程效率更高了。
基于这个理念,让同类操作拥有同一个名字,它们在不同语境下有不同的含义。例如:三角形、矩形、圆形、梯形计算面积,可以共用一个名字area,而不必取4个不同的名字,这使得软件的维护变得容易了。其实,我们还是要定义4个方法:
double area(Triangle tri){ //计算三角形面积代码
double area;
double s = (tri.a + tri.b + tri.c)/2;
area = Math.sqrt(s*(s-tri.a)*(s-tri.b)*(s-tri.c));
return area;
}
double area(Rectangle rect){ //计算矩形面积代码
}
double area(Circle circ){ //计算圆形面积代码
}
double area(Trapezoid trap){ //计算梯形面积代码
}
面向对象语言有许多优点,如代码重用、易于扩充、易于维护等。这些优点需要在程序设计训练中尤其是软件开发中逐渐深入地理解。
我们用一个对比的例子结束对面向对象的赞美。有一句话说得好:此时无声胜有声。你认为呢?
【例3.3】假设一个班有26名学生,本学期大家一致选修了两门课程。要求按总分从高到低排序,打印输出一个表,表中各列分别是学号、姓名、各科成绩、总成绩、名次。
【分析】可以用数组来保存学号、姓名、各科成绩数据。由于数组中元素必须是同类型的,因此需要用多个数组,分别存储这些数据。相应地,在排序时分别存储不同数组中的对应数据需要按照总成绩比较的结果同时进行交换。
【代码】
package ch3;
import java.util.Scanner;
public class Example3_3 {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int student Number = scan.next Int();
String number[]=new String[student Number],temp1;
String name[]=new String[student Number],temp2;
int course1[]=new int[student Number],temp3;
int course2[]=new int[student Number],temp4;
int sum[]= new int[student Number],temp5;
//输入学生的学号、姓名、科目1分数、科目2分数,计算各人两科总分
for(int i=0;i<student Number;i++) {
number[i] = scan.next();
name[i] = scan.next();
course1[i] = Integer.parse Int(scan.next());
course2[i] = Integer.parse Int(scan.next());
sum[i] = course1[i] + course2[i];
}
//用选择排序法,按总分从高到低排序
for(int i=0;i<student Number-1;i++)
for(int j=i+1;j<student Number;j++) {
if(sum[i] < sum[j]) {
temp1=number[i];number[i]=number[j];number[j]=temp1;
temp2=name[i];name[i]=name[j];name[j]=temp2;
temp3=course1[i];course1[i]=course1[j];course1[j]=temp3;
temp4=course2[i];course2[i]=course2[j];course2[j]=temp4;
temp5=sum[i];sum[i]=sum[j];sum[j]=temp5;
}
}
//输出学生成绩单:学号、姓名、科目1、科目2、总分、名次
System.out.println(" 学号----姓名----科目1----科目2----总分----名次");
for(int i=0;i<student Number;i++) {
System.out.println(" "+number[i]+"----"+name[i]+"----
"+course1[i]+"----"
+course2[i]+"----"+sum[i]+"----"+(i+1));
}
}
}
【例3.4】题目同例3.3,要求用面向对象方法实现。
【分析】在例3.3中,学生对象数据被拆分成若干条,存储在不同数组中,给后续排序和交换带来一些麻烦。本例中以对象整体存储,后续处理简单许多。
其实,面向对象的好处在大型系统中,尤其在多线程情况下,最能显现出来。在简单问题中,往往看不出它有什么明显的优势。
【代码】
package ch3;
import java.util.Scanner;
class Student{
String number;
String name;
int course1;
int course2;
int sum;
int rank;
Student(String number,String name,int course1,int course2){
this.number = number;
this.name = name;
this.course1 = course1;
this.course2 = course2;
}
}
public class Example3_4 {
public static void main(String[] args) {
String number;
String name;
int course1;
int course2;
Scanner scan = new Scanner(System.in);
int student Number = scan.next Int();
Student []s = new Student[student Number];
Student temp;
//输入学生的学号、姓名、两门课成绩,算出个人总分
for(int i=0;i<student Number;i++) {
number = scan.next();
name = scan.next();
course1 = Integer.parse Int(scan.next());
course2 = Integer.parse Int(scan.next());
s[i] = new Student(number,name,course1,course2);
s[i].sum = course1 + course2;
}
//按个人总分排序
for(int i=0;i<student Number-1;i++) {
for(int j=i+1;j<student Number;j++) {
if(s[i].sum < s[j].sum) {
temp = s[i];s[i] = s[j];s[j] = temp;
//与Example3_3对比一下,对象作为一个整体的好处显
而易见吧?
}
s[i].rank = i+1;
}
}
s[student Number-1].rank = student Number;
//输出学生成绩单:学号、姓名、科目1、科目2、总分、名次
System.out.println(" 学 号----姓 名----科 目1----科 目2----
总分----名次");
for(int i=0;i<student Number;i++) {
System.out.printf("%6s%7s%9s%9s%8s%8s\n",s[i].number,s[i].
name,s[i].course1,
s[i].course2,s[i].sum,s[i].rank);
}
}
}
3.2.3 面向对象和面向过程思想的关系
面向对象和面向过程的语言并存于世界已经多年,可能还会继续并存下去。面向对象和面向过程的编程思想也是一样。
面向对象并不取代面向过程,二者是相辅相成的。面向对象关注于从宏观上把握事物类的结构和事物类之间的关系,在具体实现类方法的时候,仍然会用到面向过程的思维方式。
面向对象如果离开了面向过程,就无法从抽象的思维层面落实到实现,成为无源之水。