写点什么

梦回战国,领略两千多年前公孙龙如何将面向对象运用得炉火纯青

作者:迷彩
  • 2022 年 8 月 07 日
  • 本文字数:8503 字

    阅读完需:约 28 分钟

梦回战国,领略两千多年前公孙龙如何将面向对象运用得炉火纯青

前言

听说早在两千多年前的战国时期公孙龙就把面向对象玩得炉火纯青,而学过面对对象的你却还是一知半解

梦回战国


在春秋战国时期,各种思想学术流派的成就,与同期古希腊文明交相辉映;以孔子、老子、墨子为代表的三大哲学体系,形成诸子百家争鸣的繁荣局面;霎时,华夏大地百花齐放。诸子百家中流传甚广的是法家、道家、墨家、儒家、阴阳家、名家、杂家、农家、小说家、纵横家、兵家、医家。而我们今天所聊到的这位"大佬"也在其列。他就是名家著名代表人物公孙龙(主角登场自带光环和 BGM,哈哈~~).公孙龙能言善辩,曾经做过平原君的门客,其主要著作为《公孙龙子》,其中最著名的一篇当属《白马论》。

大家都说面向对象是一种对现实世界理解和抽象的方法,是计算机编程技术发展到一定阶段后的产物。既然是战国时期,那时候连计算机都没有,哪来的面向对象呢?

虽然面向对象最早是由编程语言提出的。但其实面向对象并非是计算机编程语言的专属,他只是一种思想.或者对于编程来说他是一种编程范式,一种思考问题的方式,或者是思考问题的角度。什么是思想:思想本义是客观存在反映在人的意识中经过思维活动而产生的结果或形成的观点及观念体系,那也就说明面向对象思想本就是客观存在的,只是发现早晚的问题,这让我想起了一句话:你来或者不来,他都在哪里...而且面向对象发展到今天,面向对象的概念和应用已超越了程序设计和软件开发,扩展到如数据库系统、交互式界面、应用结构、应用平台、分布式系统、网络管理结构、CAD 技术、人工智能等领域。


白马非马--领略两千多年前面向对象的应用

白马非马是众多哲学家特别是先秦哲学家争论和探讨的一个问题之一。语出战国时期平原君的食客公孙龙。

接下来到了听故事的环节:

2200 年前的战国时期,赵国平原君的食客公孙龙有一天骑着白马进城时,被守城的官兵以马不能进城而将其拦下.公孙龙当众即兴演讲,口述"白马非马"一论.守城的官兵被说的一愣一愣的,无法反驳。于是公孙龙就骑着他'不是马的白马'大摇大摆进城去了,这其实就是历史上最为经典的一次面向对象思维的阐述。让我们来看看原文:

"白马非马,可乎?"曰:"可。"

曰:"何哉?"曰:"马者,所以命形也。白者,所以命色也。命色者,非命形也,故曰白马非马。"

曰:"有白马,不可谓无马也。不可谓无马者,非马也?有白马为有马,白之非马,何也?"

曰:"求马,黄、黑马皆可致。求白马,黄、黑马不可致。使白马乃马也,是所求一也,所求一者,白者不异马也。所求不异,如黄、黑马有可有不可,何也?可与不可其相非明。故黄、黑马一也,而可以应有马,而不可以应有白马,是白马之非马审矣。"

曰:"以马之有色为非马,天下非有无色之马也。天下无马,可乎?"

曰:"马固有色,故有白马。使马无色,有马如已耳,安取白马?故白者非马也。白马者,马与白也;马与白马也,故曰:白马非马也。

脑补下当时的场景~



公孙龙的话大意是说:"马" 指的是马的形态,"白马"指的是马的颜色,而形态不等于颜色,所以白马不是马。

这在逻辑学上是一个典型的偷换概念的例子。他把"白马"和"马"这两个不同的概念,用在了一个问题里来进行论证,并作为同等意义上的概念来分析。在哲学上,这是把事物的共性和个性的关系混淆了。

白马非马与面向对象的联系

名家的中心论题是所谓的'名'与'实'的逻辑关系问题,也就是概念和实际存在的逻辑关系问题。名者,抽象也,类也。实者,具体也,对象也。从这个角度来看公孙龙是我国早期最著名的面向对象思维的思想家。



白马非马这一论断的关键就在于一个"非"字。公孙龙一再强调白马与马的特征,通过把白马和马视为两个不同的类,用"非"这一关系,成功地把'白马'与'马'的关系由从属关系转移到"白马"这个对象与"马"这个对象的相等关系上,显然,二者不相等,故"白马非马"。而我们正常的思维是:马是一个类,白马是马这个类的一个对象,二者属于从属关系,说:白马非马,就是割裂了马与白马之间的从属关系,是一种形而上学的思想体系,偷换概念,故而为诡辩也.

白马非马这个典故我们可以称之为诡辩,但是我们把这个问题抽象出来.实际上讨论的就是类与类之间的界定,类的定义等一系列问题.类应该抽象到什么程度.其中涉及到类与对象的本质问题,也涉及到类的设计过程中的一些原则.

让我们看看正常思维的面向对象中马这个类如何实现:(这里使用 java 语言实现)

//马类public class Horse{  	public String color;    	//成员变量初始化	public Horse(String color){ 		this.color = color;	}  	public void run(){		System.out.println("马能跑");	}}
public class Test{ public static void main(String[] args){ Horse h = new Horse("白色"); h.run();//马能跑 }}
复制代码

面向对象的提出


在面向对象出现之前,编程领域主要是以面向过程为主,或者说是流程式编程。就像流水线,一步步按步骤实现,最具代表的语言就是:C 语言。而且两者是也是我们在学习面向对象时常用来对比的两种编程思想,两种编程思想各有优缺点,面向对象不一定是最好的选择,具体问题具体分析,需要放在具体的问题上讨论,而不是为了面向对象而面向对象。其实在面向过程之前还有面向机器,它趋于从计算机的视角看问题,但是面向机器的语言编写困难,不利于人的阅读和理解,也很容易出错,后来才有了面向过程。从面向机器到面向对象,是经历一次次软件危机的产物。


面向对象技术最早是在编程语言 Simula 中提出的。1967 年 5 月 20 日,在挪威奥斯陆郊外的小镇莉沙布举行的 IFIP TC-2 工作会议上,挪威科学家 Ole-Johan Dahl 和 Kristen Nygaard 正式发布了 Simula 67 语言。Simula 67 被认为是最早的面向对象程序设计语言,是面向对象的开山祖师,它引入了所有后来面向对象程序设计语言所遵循的基础概念:对象、类、继承,但它的实现并不是很完整。

Simula 虽然最早提出面向对象的概念,但因为其本身复杂,比较难学,而并没有大规模流行。但 Simula 提出的面向对象的概念对编程语言后继的发展产生了巨大且深远的影响

何为面向对象

面向对象是相对于 C 语言中的面向过程的一种编程思想,更趋于站在人的视角思考问题。最具代表的语言是 java 和 C#(听说当年微软为了和 JAVA 对抗,挽回颜面...根据 java 而推出 C#),这两个都是纯粹的面向对象语言,不像 php 和 Python,javascript 等脚本语言可以支持多种编程范式,既能够支持使用面向对象还可以兼容面向过程.


我们用 java 语言去做一件事,只用去 new 一个对象,并且去调用这个对象的方法,就可以实现我们所要的业务,而这个对象的具体方法做了什么,具体的操作过程,我们就不用去关注了;这就是面向对象编程思想。说到面向对象,总有另一个绕不过的话题,那就是面向过程。那具体什么是面向过程呢?这里用简短的语言概括一下:面向过程就是分析出解决问题所需要的步骤或者流程,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用即可。举个例子:大家应该都看过宋丹丹的春晚小品《钟点工》,里面有个经典的问题:要把大象装冰箱总共分几步?第一步打开冰箱门,第二步把大象装进去,第三步带上冰箱门,一共三步,先干什么,后干什么,接着干什么,一步一步往下走,这就是典型的面向过程


这是不是很好理解,而且同样很符合我们现实生活中解决问题的基本逻辑。既然符合人类的现实执行逻辑,为什么还需要面向对象呢?那就得从面向对象的概念说起了,面向对象则是把构成问题事物分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描述某个事物在整个解决问题的步骤中的行为。可以明显地看出,面向对象是以功能来划分问题,而不是步骤,可以将复杂的问题简单化。当你遇到庞大且复杂问题的时候,面向过程按步骤一步步实现起来可能会比较吃力且困难,面向对象或许更合适。


比如同样是绘制棋局,这样的行为在面向过程的设计中会分散在多个步骤中,很可能出现不同的绘制版本,因为通常设计人员会考虑到实际情况进行各种各样的简化。而在面向对象中,比如在 java 或者 C++实际编程中数据和对数据的处理(算法)都被封装在了一个对象里,具有很好的复用性,甚至我们可以使用同一套模板,只需要修改相关属性就可以实现不同的功能或者产生不同的效果。

对象和类的关系

在面向对象的世界里,用类一个个地构造出对象来,在主程序里调用的是一个个对象的行为。使用过 Linux 的童鞋可能都知道,在 linux 中一切皆文件.而在面向对象中,一切皆对象,所有的行为、数据等都需要基于对象.


1、对象是人们要进行研究的任何事物,从最简单的整数到复杂的飞机,以及人等均可看作对象,它不仅能表示具体的事物,还能表示抽象的规则、计划或事件。


2、对象的状态和行为

  • 对象具有状态,一个对象用数据值来描述它的状态

  • 对象还有操作,用于改变对象的状态,对象及其操作就是对象的行为。

  • 对象实现了数据和操作的结合,使数据和操作封装于对象的统一体之中


3、类

  • 具有相同特性(数据元素)和行为(功能)的对象的抽象就是类。

  • 类具有属性,它是对象的状态的抽象,用数据结构来描述类的属性。

  • 类具有操作,它是对象的行为的抽象,用操作名和实现该操作的方法来描述。因此,对象的抽象是类,类的具体化就是对象,也可以说类的实例是对象,类实际上就是一种数据类型。


类是对象的抽象组织,而对象是类的具体存在.

比如 Java 是面向对象的编程语言,对象就是面向对象程序设计的核心。所谓对象就是真实世界中的实体,对象与实体是一一对应的,也就是说现实世界中每一个实体都是一个对象,它是一种具体的概念。


面向对象的三大特征:封装,继承、多态

1、封装:隐藏对象的属性和实现细节,仅对外提供公共访问方式。例如:夏天宿舍很热,我们(用户)只需要操作遥控器即可使用空调,并不需要了解空调内部是如何运行的

  • 好处:将变化隔离、便于使用、提高复用性、提高安全性

  • 原则:将不需要对外提供的内容隐藏起来;把属性隐藏,提供公共方法对其访问

  • 封装的意义:封装是实现面向对象程序设计的第一步,在于保护或者防止代码(数据)被我们无意中破坏。在面向对象程序设计中数据被看作是一个中心的元素并且和使用它的函数结合的很密切,从而保护它不被其它的函数意外的修改


2、继承:提高代码复用性;而且继承也是多态的前提

  • 子类中所有的构造函数都会默认访问父类中的空参的构造函数,比如:java 中默认第一行有 super()

  • 若无空参构造函数,子类中需指定;另外,子类构造函数中可用 this 指定自身的其他构造函数


3、多态:"一个接口,多种方法",同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果

  • 好处:提高了程序的扩展性

  • 弊端:当父类引用指向子类对象时,虽提高了扩展性,但只能访问父类中具备的方法,不可访问子类中的方法;即访问的局限性

  • 实现多态的前提:实现或继承关系(继承是多态的基础,没有继承就没有多态);覆写父类方法(多态下调用子类重写的方法); 父类引用变量指向子类或具体实现类的对象(子类到父类的类型转换)

  • 重载(overload)和重写(override)是实现多态的两种主要方式

    实现多态的方法:

  1. 接口多态性

    继承多态性

    通过抽象类实现的多态性

透过现象看本质


在现实世界中,任何一个操作或是业务逻辑的实现都需要一个实体来完成,即实体是动作的支配者,没有实体就没有动作.所谓的面向对象,就是用模拟现实世界思维去编程,按照现实世界的逻辑去分析问题思考问题中涉及哪些实体.这些实体具备哪些属性和方法.

所谓的模拟现实世界,就是使计算机的编程语言在解决相关的业务逻辑时的方式,与真实的业务逻辑的发生保持一致,即每个动作的背后都有一个完成这个动作的实体.这样任何功能都依赖于一个具体实体的动作,因此面向对象可以看作是一个又一个的实体在发挥各自的能力.并在内部进行协调有序地调用过程.当采用面向对象的思想解决问题时,可分为以下几步:

  1. 分析哪些动作是由哪些实体发出

  2. 定义这些实体,为其增加相应的属性和方法

  3. 实体去执行相应的功能或者动作

就以工厂流水线的机器人为例,位于不同生产环节的机器人有着不同的作用,它们有着很多属性,如:机器人的 ID 或标识信息.也有一些方法,比如焊接机器人有焊接的方法,喷漆机器人有喷漆的方法,传送机器人有传送的方法.这些完成不同工作的机器人其实就是我们所说的实体。而将他们抽象出来的便是类.类用于表示这些机器人有哪些属性和方法.我们可以想象机器人的制造商就是参照定义类好的类来生产机器人的。


再举个通俗点的例子:

比如今天我要打扫教室卫生,在面向过程中,我要自己亲自去做这件事,我要考虑从前往后,从左到右去打扫,一步一步来。而在面向对象中,我只需要想谁能做这件事,谁能把这件事完成得更好,我就安排谁来做,刚好我想到张三能干好这件事,那我就安排张三打扫教室卫生,我只要跟张三说今天让他打扫教室,其余的我压根不需要关注,他是怎么打扫的,是从左到右,从前往后,打扫的过程,我完全不关心,地不是我来扫,我只需要考虑安排谁去做这件事就行,我直面的是张三这个对象,他是执行者,他才是面向过程的


但是如果我跟张三说完,张三又安排李四去做,李四又安排王五去做。。简直没完没了了,那最后实际没有人去执行,完全没有意义,所以面向对象离不开面向过程的,也就是说面向对象依托于面向过程,因为这件事总要有人去完成,就像现实生活中,如果一个公司中每个人都在行使指挥权,而底下没有实际做事的人,很快就得解散。好比将军手下没有士兵,只是光杆司令一枚。


所以在面向对象中也并非完完全全只有面向对象,因为在现实中做一件事总是要有执行者的,所以面向对象的最后一环同样需要面向过程.只是我们不需要关注过程,而是我们只需要面对一个个对象实体,具体的操作由实体来实现.

现实生活中的面向对象

举例来说:比如说你要去饭店吃饭,你只需要到饭店,然后找到饭店的服务员,跟她说你要吃什么,然后他就会通知厨师给你做出来,你并不需要知道这个饭是怎么做的,菜是怎么炒出来的.你只需要面向这个服务员,告诉他你要吃什么,然后他也只需要等你吃完,面向你收到钱就好,不需要知道你怎么吃的这顿饭


面向对象的特点:

1:将复杂的事情简单化

2:面向对象更贴合管理者的角度(可以说是上帝视角),而面向过程更贴合于执行者的角度,面向对象将以前的过程中的执行者,变成了指挥者

3:面向对象这种思想是符合现在人们思考习惯的一种思想,更贴近于生活中人们的思维角度


再举个和我们生活息息相关的例子:

面向对象是管人的,比如老板只需要面向底下的管理层去分配工作即可,比如老板要求明天要上线新项目,老板把任务下达给我,我告诉小王明天要把新项目上线,我不管小王是如何去执行的,我不看过程,只想要结果:明天上线新项目,把事办成就可以.所以我只需要安排小王去做这件事, 不管他是加班也好,多找几个一起开发也好,都与我无关,甚至老板都不关心这个任务我是如何往下传达的,老板面向我这个对象,而我是直面小王这个对象,小王是最后一环,小王就是那个卑微的执行者(哈哈~)


对象就是执行者:在整个环节中老板是上帝视角,是指挥者,我也是对象,但我执行的任务是传达;而最后真正把事完成的是小王。但在实际的程序设计中,会简化或忽略掉一些没有意义的环节。上面的例子中,老板给我下达任务这个环节可以省略。老板分配的是给我们部门的任务,就可以简化成我们部门想做这件事,而我作为部门管理者,我无需亲自执行,只需要我来把具体任务分配给小王即可。所以在实际的开发中我是实际指挥者,小王还是那个卑微的执行者

面向对象的基本思想是使用面向对象中的三大特征继承、封装、多态进行程序设计

光说不练假把式,接下来我们通过具体的实例来看看面向对象的形与本


public class Person {
public String name; public String gender;
//成员变量的set和get方法(与外界联系的桥梁) public void setName(String name) { this.name = name; }
public String getName() { return name; }
public void setGender(String gender) { this.gender = gender; }
public String getGender() { return gender; }
public void say() { System.out.println(this.name + ":" + this.gender); }}
public class Test { public static void main(String[] args) { //创建学生对象 student Person student = new Person(); student.setName("张三"); student.setGender("男"); student.say();
//创建老师对象 teacher Person teacher = new Person(); teacher.setName("李四"); teacher.setGender("男"); teacher.say(); }}
复制代码


根据上面的例子,可以有下面的理解:

  1. 类定义了一系列的属性和方法,并提供了实际的操作细节,这些方法可以用来对属性进行加工

  2. 对象含有类属性的具体值,这就是类的实例化,正是由于属性的不同,才能区分不同的对象.在上面的例子中,由于 student 和 teacher 的性别和姓名不同,才得以区分二人

  3. 类与对象的关系是类似一种服务与被服务,加工与被加工的关系,具体而言,就如同原材料与流水线的关系,只需要在对象上调用所存在的方法,就可以对类的属性进行加工并展示其功能


面向对象的设计原则

面向对象设计原则仍然是面向对象思想的体现

  1. 单一职责原则:要求类只负责一件事情。接口分离原则,让用户只关心他们所需的接口, 单一职责原则与接口分离原都体现了内聚的思想

  2. 开放封闭原则:要求类不作修改而能够扩展功能,体现了类的封装与继承

  3. Liskov 替换原则:要求派生类要能够替换基类,是对类继承的规范

  4. 依赖倒置原则:要求类依赖于抽象,而不是实现,是抽象思想的体现

  5. 接口分离原则(Interface Segregation Principle,ISP):

  • 该原则的核心思想是:使用多个小的专门的接口,而不要使用一个大的总接口。具体而言,接口应该是内聚的,应该避免“胖”接口。一个类对另一个类的依赖应该建立在最小的接口上,而不要强迫依赖不同的方法,这是一种接口污染。

  • 其实简单点的讲与前面说的单一职责类似,这里的接口不是函数接口,而是一个类。C#中的有专门的接口 interface,和类区分开来,而且 C#中不像 C++支持类的多继承,只支持接口的多继承,所以这里可以把接口理解成功能更小更特殊的类,一个接口可能就只要那么几个很少的方法即可

  • 接口分离手段主要有以下两种方式:(a)利用委托分离接口(b)利用多重继承分离接口


总结

面向对象程序设计概括

是我们对一组对象的描述,一组相关的属性和行为的集合,是一个抽象的概念。而对象就是把属性进行具体化,然后交给类处理,通俗点理解:比如我想买一辆五菱神车自驾旅游,毕竟我们又不是哈利波特,无法骑着魔毯到处飞,首先是造车,画图纸,工厂根据图纸造车,图纸就是类


对象:该类事物的具体表现形式,具体存在的个体。对象就是数据,对象本身并不包含方法,但是对象有一个"指针"指向一个类,这个类里面可以有方法.在面向对象编程中,对象也是一种很普通的变量,不同的是其携带了对象的属性和类的入口.


成员变量:事物的属性


成员方法:事物的行为,描述不同属性所导致的不同表现


类就是对一些具有共性特征,并且行为相似的个体的描述.

比如小李和老王都有姓名、年龄、身高、体重等一些属性,并且两人都能够进行聊天、运动等相似的行为

类和对象是不可分割的,有对象就必定有一个类与之对应,否则这个对象就成了没有亲人的孩子,但是也有特殊情况,比如:"孤儿"对象(比如在 PHP 中,由标量进行强制转换的 object,没有一个类与他对应,此时 PHP 中一个称为"孤儿"的 stdClass 类就会收留这个对象).


面向对象相对于面向过程的优缺点

面向过程编程

优点:性能上它是优于面向对象的,因为类在调用的时候需要实例化,开销过大

缺点:主体是函数,一个函数就是一个已封装的模块.不易维护、复用、扩展

用途:单片机、嵌入式开发、Linux/Unix 等对性能要求较高的地方

面向对象编程

优点:易维护、易复用、易扩展,由于面向对象有封装继承多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护

缺点:一般来说性能比面向过程低(当时在 CPU 执行能力的极强的今天,很多场景下性能的差异是可以忽略不计的)


无论是面向过程还是面向对象,都是一种思想,准确地说都是编程思想。一种思考问题的角度或思考问题的方式.面向对象编程的基本思想是使用面向对象中的三大特征继承、封装、多态进行程序设计,我们将繁琐的步骤,通过行为、功能,模块化,这就是面向对象;其实作为面向过程的 C 语言,也是同样能够写出符合面向对象的代码的,只是这并不是 C 语言擅长的事,毕竟术业有专攻。

结合现实世界从实现和存储理解对象和类,这样就不会把二者看成一个抽象,神秘的东西。还需要注意的是在使用面向对象思想进行程序设计时,为防止类的过度设计,需要依据面向对象设计的相应原则灵活去设计符合现实世界的类。


如果需要设计一个类,要从客观的世界抽象出一套规律,就得总结这类事物的共性,并且让它可以与其他类进行区分.而这个区分的依据就是属性和方法。"雄兔脚扑朔,雌兔眼迷离",说明同个类不同对象之间属性可能不同,呈现的状态不同。区分的办法就是实例化出一个对象,并赋予相应的属性,毕竟是骡子是马,得拉出来溜溜。至此,你是否对"白马非马"这个典故有了新的认识呢。


面向对象涉及的知识极多,比如类和实例的具体定义,类的成员和方法具体设计和定义,类的抽象,接口......等等。这里只是科普下基本概念,抛转引玉,点到为止,如果感兴趣以后可以继续接着唠唠。


发布于: 2022 年 08 月 07 日阅读数: 218
用户头像

迷彩

关注

我的工作是常年写bug|公众号:编程架构之美 2020.06.18 加入

修bug的菜鸟~公众号:“互联网有啥事”已改名为“编程架构之美”

评论 (1 条评论)

发布
用户头像
写得不错,有趣,学到了👍 👍
2022 年 08 月 08 日 02:32
回复
没有更多了
梦回战国,领略两千多年前公孙龙如何将面向对象运用得炉火纯青_Java_迷彩_InfoQ写作社区