伍.1.1 面向对象与面向过程有什么区别?

emmm……这个话题其实很复杂,我觉得应该从编程的发展史来谈。

01.面向过程编程

当开发软件这门科学还处于非常简单的早期时,我们这样编程:

定义函数
函数a
函数b
……
定义数据
数据a
数据b
……
然后
将数据传递给函数
按指定的步骤执行函数
输出结果

你基本上可以认为上面就是面向过程编程。

面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,然后一个一个依次调用函数就可以了。

当计算机软件应用越来越多后,软件的功能变得越来越强大,代码行数越来越多,复杂度远超Hello World的时候,我们如果按上面的形式编写程序/软件就有麻烦了。

函数和数据会定义得非常多,这会导致两个问题:

  • 首先是命名冲突。英文单词也就那么几个,可能写着写着取名时就没合适的短词用了,为了避免冲突,只能把函数名取得越来越长。

  • 然后是代码重复。比如你做一个计算器程序,你的函数就要确保处理的是合理的数据,这样最起码加减乘除四个函数里,你就都要写对参数进行检测的代码,写四遍或者复制粘贴四遍不会很烦,但多了你就痛苦了,而且因为这些检测代码是跟你的加减乘除函数的本意是无关的,却一定要写在那里,使代码变得臃肿不堪、意图模糊,不能直观地看出其用意。

就算一个网络小说的作者,他每次写新章节时也不大可能直接打开包含着前面几百章文字的文档接着写。更正常的做法是新建一个文档,写新的一章,为的是减小复杂性,减少干扰。更何况代码那么复杂那么容易出错的东西。

02.面向对象编程

1).初步解决办法

随着软件业的发展,解决办法就要出来了:

  • 代码重复,我们可以用函数里面调用函数的方法,比如把检测代码抽出来成一个独立函数,然后加减乘除四个函数运行时只要调用一下检测函数对参数进行检查就可以了。分布在四个函数里的重复代码变成了一个函数,是不是好维护多了。

  • 命名冲突,我们就把这一堆函数们进行分类吧。比如没有分类时候,我们取名只能取名:

    检测
    整数加
    整数减
    整数乘
    整数除
    复数加
    复数减
    复数乘
    复数除
    小数加
    ...

    进行归类后:

    整数{
    检测
    }
    复数 {
    检测
    }
    小数 {
    检测
    }
    分数 {
    检测
    }

是不是一种叫做类(class)的概念就呼之欲出了,这样我们打开一个整数类代码文件,里面就是简简单单的加减乘除四个函数(也叫方法,method),简单清晰,而不会跟外面的其他加减乘除函数产生命名冲突(因为有了命名空间,namespace)。

当然,这样进行归类后,又会有新的问题和解决办法出现。比如四个类中的检测也是应该提取出来的,所以简单的起因最终发展出比如封装继承多态之类等挺复杂的一套编程范式。然后编程界的一些学术大神起了各种高大上的名字,即所谓面向对象编程(OOP),然后去”毒害“你我这样当初还年轻的孩子们,从此上了贼船乐呵呵下不来 😭

面向对象是把构成问题的要素分解成各个对象,建立对象的目的不是为了完成一个个步骤,而是为了描叙某个要素在整个解决问题的步骤中的行为。

2).更进一步解决问题

上面进行归类后,代码其实还是不好维护的,然后我们就继续提取为:

数 {
检测
}
整数 {
沿用上面数的设计
}
小数 {
沿用上面数的设计
}

所谓继承,就是数这个类的整体设计,沿用给整数,分数小数这些类,作为他们的编写大纲去编写加减乘除这些函数的具体代码。根据整数,分数,小数各自的性质,做出各自的调整。

这时数这个类,如果你给它里面的加减乘除函数的写了一些很粗糙简单的代码,就叫做父类,基(础)类。子类们“继承”了父类(把代码进行了复杂化)。

如果类里没写具体的函数实现逻辑,那这个类其实就只是个空壳一样的设计图而已,叫做抽象类。子类们“实现”了抽象类(把空空的设计变成了具体代码)。

模版是什么?像C++语言是强类型的,就是给变量进行了类型区分的,比如整数类型,双整数类型。很明显这两种变量所能容纳的数据体积是不一样的,单个函数不能通吃多种类型的参数,我们就可能会面临下面两套代码并存的局面。

单整数类 {
单整数加
单整数减
单整数乘
单整数除
}
双整数类 {
双整数加
双整数减
双整数乘
双整数除
}

所以C++跟其他强类型语言比如JAVA等,为我们提供了一个所谓模版功能,也叫泛型

<变量类型>整数 {
<变量类型>加
<变量类型>减
<变量类型>乘
<变量类型>除
}

整数类等于把变量类型设置为整数,套上模版 双整数类等于把变量类型设置为双整数,套上模版

这样就写了一份代码,得到了两份类的代码。

当然,弱类型的编程语言比如我最爱的JavaScript,因为变量没有严格的类型分别,是没有这种烦恼的。但变量类型有时候还是很重要的,弱类型语言里就会出现类似数加字符串这种运算,可能并不是程序员的预期和本意,所以比起强类型性语言而言,初学者在使用弱类型语言时,经常会出现很多”枯燥无聊“的bug。

3).再深入啰嗦几句

上面发展出了父类之后,我们发现编程还是有问题的,小数类:

小数类 {
}

如果我们需要一个”能自动实现结果四舍五入功能”的小数计算类,同时又需要一个不带该功能的,怎么办呢,难道要写两个类吗?大可不必。

所以编程界的天才大神们捣鼓出了“实例”或者说是“对象”这个概念,首先把类改成:

小数类 {
标识变量:是否四舍五入
标识变量:是否限定小数点后位数
构造函数(设置上面的标识)
加(会根据上面两个标识变量输出不同结果)
减(会根据上面两个标识变量输出不同结果)
乘(会根据上面两个标识变量输出不同结果)
除(会根据上面两个标识变量输出不同结果)
}

这样,我们就写一个类,但是通过构造函数,把一份代码,通过关键字new构造出行为稍微有点不同的两个实例供我们使用,这个过程叫实例化

不能进行实例化的类,叫做静态类,函数们的行为是固定的。不能实例化的类,其实只是函数们的一个集合归纳,只是对函数进行了整理,功能的强大和编码的自由灵活度是不够的。

能够进行实例化,变化出各种行为各自不大一样的实例的类,我们一般就把它们叫做类了,因为最常见。程序员们也就能保持代码简单的同时而又可以很方便进行代码行为微调。

总结

文尾以五子棋来举例说明一下。

面向过程的设计思路就是首先分析问题的步骤:1、开始游戏,2、黑子先走,3、绘制画面,4、判断输赢,5、轮到白子,6、绘制画面,7、判断输赢,8、返回步骤2,9、输出最后结果。把上面每个步骤用分别的函数来实现,问题就解决了。

而面向对象的设计则是从另外的思路来解决问题。整个五子棋可以分为:1、黑白双方,这两方的行为是一模一样的,2、棋盘系统,负责绘制画面,3、规则系统,负责判定诸如犯规、输赢等。第一类对象(玩家对象)负责接受用户输入,并告知第二类对象(棋盘对象)棋子布局的变化,棋盘对象接收到了棋子的变化就要负责在屏幕上面显示出这种变化,同时利用第三类对象(规则系统)来对棋局进行判定。

啰啰嗦嗦说了一大堆,也不知道说明白没有。夜已深,我要洗澡然后休息了。最后一句:就算有一天新出来一个什么”面向XX编程“的概念,我们也不用困惑怎么又出新东西了,肯定是为了解决现存在的问题而出现的;多多思考其背后的原因,有助于我们更深入的理解当前所学所用的编程范式。