面向对象编程思想及入门知识

这几天在调程序,所以想写写自己对“面向对象编程”的一些理解,希望对打算入门计算机编程的同志们有所帮助。之前,好几个师弟问过我,C++与C有什么区别,学习面向对象语言需要掌握哪些基础。另外,我在阅读一些程序时,发现有许多程序虽然是使用的C++,但程序结构仍没有摆脱C的思维。所以,在此我想系统地论述面向对象编程的思想以及学习面向对象编程应当掌握的一些知识。

在写之前,我要强调,面向对象是一种方法论,而不是一种具体的编程语言,所以在学习时千万不要过份纠结某特定编程语言的语法,而要去深刻理解背后的编程思想

为了比较好地对比面向对象和面向过程,文章里面的举例采用C++和C,但C#和JAVA是比C++更彻底的面向对象语言,如果没有学过c++的,建议直接学习C#或JAVA。

图片来自网络

面向过程遇到的难题

面向过程,把问题世界看成步骤的逻辑组合。面向过程的编程往往会将一个问题分解成一个一个的步骤,这些步骤通过函数(function,C和C++)或方法(Method,JAVA)实现(后面统一采用“函数”的说法),然后再根据流程、逻辑将这些步骤进行组合(即按逻辑依次调用函数)。所以在编程之前,一般会画一些流程图,将步骤、流程、逻辑描述清楚,然后再开始写函数,最后写主程序调用这些函数。

我们先看一个问题:假设有一辆汽车,简化成一个质点,从A点直线运动到B点,AB的距离为500m,汽车要经历启动、加速、匀速、刹车等过程,汽车有最大速度,当加速到最大速度后匀速运动,刹车距离是20m,请用面向过程的方法来描述这个问题。

首先根据上面的问题描述设计一个流程图,
流程图

根据流程图我们知道需要实现start(),accelerate(),uniformMotion(),stop()等函数,同时我们还需要定义一些变量,如Vcurrent,Vmax,Lremain等等,再根据流程逻辑将这些函数进行组合

void main(){
  start();
  while(Vcurrent<Vmax) {
   accelerate();
   }
  while(Lremain<20){
   uniformMotion();
  }
  stop();
}

由此,我们会觉得面向过程的方法很简单,也很明了。但是如果把这个问题更复杂化一些:

汽车厂升级了该款汽车,加速度和最大速度都要都比改进的汽车大,厂家想通过一次比赛试验汽车的改进效果,于是改进前的汽车和改进后的汽车从A点同时出发,看哪一辆先到达B点。

此时要同时处理两辆汽车,要怎么办?是不是现在觉得用流程图去描述这个问题会变得费力多了,但也还勉强能实现,为B车重新设计一套类似的变量和函数即可。

但是问题如果更复杂呢?例如汽车增加新功能,可以转弯壁障,另外路上不只有这两辆车,还有公交车、货车、自行车、人,路面也不这么简单了,分成了私家车车道、公交车车道、非机动车车道和人行道,还有红绿灯、指示牌等交通管理设备,在这种情况下,如何用步骤去分解?简直要爆炸了!

由此可见,面向过程的抽象能力非常差,当问题复杂化后,面向过程的复杂度会成指数级上升,最后达到无法承受的程度。而且可以看到,面向过程的代码复用性也很差,只是函数级别的复用,这种强依赖于流程和逻辑的代码,耦合性极强,如果逻辑一变,就会有大量的代码需要修改,扩展性差、维护性差。

但是我们知道,计算机里面的虚拟世界越来越复杂,这是如何做到的呢?

既然面向过程遇到了难题,那么就放弃它,选择抽象能力更强、程度更高的编程思想——面向对象编程思想。

面向对象的哲学依据

既然面向对象是一种方法论,那么必然就有它的哲学依据。编程其实就是对现实世界的一种抽象,如何抽象现实世界,就必须要想清楚如何认知现实世界。

我们知道,本体哲学是讨论世界本源以及“存在”的哲学。关于“存在”的讨论,中国早在战国时期,名家公孙龙就提出了“白马非马”的论题,这个论题主要包含三层意思(参考冯友兰的《中国哲学简史》):
- “马”是一种动物,“白”是一种颜色,“白马”是一种动物加一种颜色。三者内涵不同,所以白马非马。
- “马”的外延包括一切马,不区别颜色,而“白马”的外延只有白马,有颜色区别,外延不同,所以白马非马。
- “马”这个共相是一切马的本质属性,而“白马”这个共相不同,所以白马非马。

总结起来,之所以白马非马,是因为“马”和“白马”是“名”(概念)而非“实”(实体)

公孙龙还有一篇《指物论》,指出了名与实的区别。公孙龙以“物”表示具体的个别的实体;以“指”表示抽象的共相(概念)。这里的共相就是指以某类事物共有的属性为内涵的名词。

到此,我们脑海里应该有了“概念”和“实体”的区别了。

希腊哲学家柏拉图也总结了类似的结论。柏拉图认为这个世界由两部分存在,一种是上帝创造的完美概念世界(形式世界,理型世界),这个概念就如同做饼干的模子,它是永恒不变的,另一个是实体世界,现实世界中的事物都是这些概念的映射,但这种映射并不是完美的,就如同用模子做饼干一样,饼干相似但并不完全一样。

但后来柏拉图的学生亚里士多德并不赞同他老师的观点,他并不认为现实世界来自于理型世界,也就是实体不是来自于概念,而恰好相反,概念来自于实体。他认为所谓形式只是某类事物共有的特征,所以用模子做饼干来比喻这个世界并不合适,他不赞同“概念”先于“实体”的说法。亚里士多德认为,我们所拥有的概念是透过我们感官感知到的事物而进入我们的意识形成的,而且我们拥有与生俱来的理性,可以组织所有的感官映像,并将它们加以整理和分类,所以才会产生诸如“植物”,“动物”,“人类”等概念

所以亚里士多德给了人类一个最伟大的发明——分类!他认为我们区别事物的方法就是将事物分门别类,于是他开始尝试对世界上的万事万物进行分类,并且是有层级的分类,他在最高层把世界分成两大类:生物和非生物。然后把生物又分成植物和动物,动物又分成禽兽和人类,就这么一步一步地分下去。亚里士多德的分类作品涉及到各种科学,其中很多名词成了现代科学中常见的词汇,因此他的分类学说对今天的科学仍然有极大的影响。

说到这,我们明白了概念和实体的区别,并且了明白了我们认识事物、区别事物的方法——分类,这便是面向对象编程的哲学依据。

面向对象里面的“类”即“概念”(也叫形式、理型、名、指等),对象即实体(也即具体的、个别的事物),那么程序组织的方法就是分类。

面向对象编程的核心思想

编程就是对现实世界的抽象,借助于上述哲学思想,面向对象编程定义了两个最核心的名词——类(class)和对象(object)。类实际上就是我们用来定义某个概念的,例如我们定义“Car”这么一个类,那么就在我们的程序世界里建立起了“Car”的概念。对象就是实体,例如我们用”Car”定义“myCar”这么一个对象,那么这个“myCar”就不再是一个概念了,而是一个独一无二的、具体的实体,“myCar”就是指我的那辆车,不再是别的车了。这个过程和柏拉图的理型世界有些相似,先有概念后有实体。

程序的组织方式就是分类,例如“汽车”可以分为“自动档汽车”和“手动档汽车”,那么我们继承”Car”,定义 “AutoCar” 和 “ManuCar” 两个类。这里提到了继承这个概念,待会还要详说。

那么,通过什么来描述一个概念(类)呢?换而言之,如何定义一个类呢?

首先,任何事物都会有它的属性。例如一个人,他(她)有区别与动物的社会属性、劳动属性等等,另外还会有身高、体重、年龄、性别、户籍、民族等等属性。属性描述了事物的静态特性,除此之外还需要描述事物的行为特性,这就是函数。函数(function)有功能的意思,它意味着类能完成一些功能。例如人能走路、说话、跳舞,这些就是人的行为特性,我们用函数来实现。从外界来看,人能完成走路、说话、跳舞等功能,假设这个人是机器人,那么外界就可以请求该机器人完成这些功能。简而言之,我们通过属性(C++中叫变量)和函数定义类

回到之前的汽车问题:假设有一辆汽车,简化成一个质点,从A点直线运动到B点,AB的距离为500m,汽车要经历启动、加速、匀速、刹车等过程,汽车有最大速度,当加速到最大速度后匀速运动,刹车距离是20m,请用面向对象的方法来描述这个问题。

这个问题中仅涉及到一个概念,即“汽车”,所以定义一个”Car”的类,描述“Car“的属性主要有m_currVilocity(当前速度),m_maxVilocity(最大速度),m_currDistance(当前行驶距离);描述“Car“的功能行为主要有start(),accelerate(),uniformMotion(),stop()等。根据这些,我们定义”Car”这个类:

class Car{
    private:
    float m_currVilocity, m_maxVilocity, m_currDistance;

    public:
    void start();
    void accelerate();
    void uniformMotion();
    void stop(); 
    float getCurrVilocity();
    float getMaxVilocity();
    float getCurrDistance();
}

再通过”Car”这个类定义一个对象”myCar”(实体),就可以对这个对象进行操作了。

void main(){
  Car myCar;
  myCar.start();
  while(myCar.getCurrVilocity()<myCar.getMaxVilocity()) {
       myCar.accelerate();
   }
  while(500- myCar.getCurrDistance()<20){
       myCar.uniformMotion();
  }
  myCar.stop();
}

到此,我们已经完成了简单的面向对象的编程,这时候初学者一定会纳闷,这样搞比面向过程的方法麻烦多了,有什么好的。这个问题暂时搁置一下,先看看面向对象的三个基本特性。

面向对象的三个基本特性

面向对象的三个基本特性分别是封装性、继承性、多态性。

(1)封装性。比较细心的同志会发现我在定义属性的时候使用了”private”这个关键词,这表示这些属性不能被外界直接访问和修改,这就是封装性。之所以这么做,是为了隐藏复杂性,包括两方面的原因:一是对客户隐藏他们不需要知道的细节,让客户专注到如何使用接口上来,这也可以防止他们窥探类的内部设计思想;二是允许库设计人员修改内部结构,不用担心它会对客户程序员造成什么影响,只要对外接口不变,类的内部变化不会对外界造成任何影响。所以封装让类保持了一定的独立性,有利于设计高内聚、低耦合的程序。

(2)继承性。上面提到了 “汽车”可以分为“自动档汽车”和“手动档汽车”,所以能继承”Car”,定义 “AutoCar” 和 “ManuCar” 两个类,这就是继承性。 这里,”Car”被称为父类,而 “AutoCar” 和 “ManuCar” 称为子类,子类会继承父类的所有属性和函数,所以 “AutoCar” 和 “ManuCar” 就复用了”Car”的所有属性和功能。但继承不止有类的继承的这种方式,在JAVA里面还有接口的继承,这个留给“JAVA与C++的区别”这篇文章。另外,继承性也不仅仅只体现在继承上,还可以通过“组合”来实现,而且有时候使用组合会带来更好的程序结构,这个也留到以后再写。

(3)多态性。“AutoCar” 和 “ManuCar” 两个类纯粹继承”Car”还不够,否则就和”Car”一模一样了。我们知道,自动档汽车和手动档汽车主要区别在于加速的方式不一样,所以就要重写accelerate()这个函数,通过重写,“AutoCar”定义的对象和 “ManuCar”定义的对象再加速时就会呈现不同的结果,这就是多态性。程序的多态性还包括“函数重载”,在C++里面,允许存在多个同名函数,但是参数表不同,或者参数类型不同,或者两者都不同,这就是“函数重载”。

了解了三个基本特性后,再回到之前那个问题,很多新学面向对象语言的人都会觉得面向对象要比面向过程麻烦多了,我当初也是这种感觉。但是当问题复杂一些时:汽车厂升级了该款汽车,加速度和最大速度都要都比改进的汽车大,厂家想通过一次比赛试验汽车的改进效果,于是改进前的汽车和改进后的汽车从A点同时出发,看哪一辆先到达B点。

用面向对象的方法轻而易举就完成了,我通过Car 定义两个对象就行了,一个是改进前的car1,一个是改进后的car2,这两个Car只是属性有所不同而已,然后就可以对这两个Car进行操作了。

更复杂的情况:汽车增加新功能,可以转弯壁障,另外路上不只有这两辆车,还有公交车、货车、自行车,路面也不这么简单了,分成了私家车车道、公交车车道、非机动车车道和人行道,还有红绿灯、指示牌等交通管理设备。

公交车、货车、自行车等都是交通工具,定义一个交通工具的类,找到他们的共有属性和行为,在交通工具这个类中定义,然后再继承交通工具的类,定义公交车、货车、自行车,找到他们的不同点,添加各自独特的属性和函数,改写与父类有区别的。然后再定义公路、交通管理设备等类,完成这些类的定以后,再根据实际情况定义不同的对象,对象之间进行交互,就能把这个复杂的问题给解决了,这就是封装性、继承性、多态性带来的神奇力量!

前面主要介绍了面向对象编程的哲学依据、核心思想以及基本特性。下面介绍入门面向对象编程要学习掌握的一些基础知识,并且推荐几本书。

统一建模语言(UML)

写程序离不开开设计,就如同建房子,谁都不会一上来就开始垒砖头。在建房子之前,设计师得先根据客户的需求设计房子的结构、功能、布局等等,并且要绘制标准的图纸,这样建筑工人才能拿着图纸开始建房子。编程也一样,首先得分析客户的需求,然后再根据需求设计软件的构架,这个构架需要使用标准的语言进行描述,这就像标准的图纸,如此程序员才能根据图纸往上垒代码。

这个标准语言就叫做统一建模语言(Unified Modeling Language,UML),它是用来对软件密集系统进行描述、构造、视化和文档编制的一种语言。简单一点理解,它就是软件的图纸。

UML包括的内容非常多,这里仅简单地介绍一下。前面说了第一步要对客户需求进行分析,UML提供了用例图,用于软件的功能需求分析。用例图主要包含两个元素——参与者和用例。参与者用小人表示,用例用椭圆表示,参与者和执行者之间的关系用箭头表示。用例图主要反映了从参与者的角度看,系统应当向外暴露哪些功能,并不反映系统内部的结构。进行用例分析时,最重要地就是区分参与者和用例。

除了要进行功能需求分析外,还要对系统的动态行为需求进行分析,UML提供了“活动图”,活动图中最重要的两个元素是甬道和活动,甬道往往用于表示实际执行活动的对象,活动的执行有一定的流程和逻辑,这类似于面向过程编程中的流程图。

完成需求分析后就开始设计软件的构架,这包括静态模型和动态行为,静态模型包括组建图、类图等等,动态行为包括状态图、时序图等等。程序员要和这些图直接打交道,掌握它们很重要。但这些图比较复杂,不是三言两语就能说的清的,请阅读相关的书籍。

设计模式

软件设计最核心的一个理念是区分“不变”与“变化”,这样会有利于软件后期的维护和升级。“变化”就是意味着将来可能需要修改或者升级,在设计时一定要将这部分程序小心地隔离出来。将来要修改这部分程序时不用担心牵一发而动全身,这样的软件才叫做构架良好的软件。

区分并隔离“不变”与“变化”是需要大量的编程经验的,对于新手而言,在没有经验积累的基础上,如何才能设计架构良好的软件?这并不是做不到的,因为前人已经为我们总结了大量的经验,并归纳出许多设计原则,这就是设计模式。

这些设计模式就是代码模板,我们可以直接拿来用,我们要做的只是将我们的具体问题套进去就行了。设计模式也许多种,建议阅读相关书籍进行学习。

书籍推荐

这里仅根据我自己的学习过程推荐书籍,但不一定是最好的书籍,也不一定适合所有人,还请根据实际情况选择。

  • (1)谭浩强,清华大学出版社,《c++程序设计》

  • (2)侯俊杰,华中科技大学出版社,《深入浅出MFC》

  • (3)Bruce Eckel,机械工业出版社,《java编程思想》

  • (4)Eric Freeman,东南大学出版社,《深入浅出设计模式》

  • (5)谭云杰,水利水电出版社,《大象-Thinking in UML(第二版)》

在学习编程时,建议精读,不要坐这山望那山,所以就推荐这几本了。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值