隔壁小孩也能看懂的面向对象(概念篇)

1,451 阅读8分钟

很多初学者在听到【面向对象】这个名词的时候挺懵逼,别害怕,它只是听起来很厉害的样子~

什么是面向对象

下面我举个栗子,大家来思考下该如何设计程序~

一个神秘组织的boss想玩五子棋了,任命你来开发。这个时候你就开始头脑风暴,脑海里复盘自己曾经晚自习上和同桌小哥哥下过的五子棋。我们脑海里有这样一个过程:

    1. 开始游戏
    1. 黑子先走
    1. 绘制画面
    1. 判断输赢
    1. 轮到白子
    1. 绘制画面
    1. 判断输赢
    1. 返回步骤2
    1. 输出最后结果

然后再用函数将上面每个步骤实现,这个游戏就设计好了~~~~

嗯嗯,看上去好像没什么问题,但这个时候boss说他想玩的不得了,给你加了个小弟一起开发,你说我来搞黑子,你来搞白子,最后我们再来和一下,小弟说o**k。

正在你和小弟背靠背埋头开发的时候,boss来转了一圈,大喊一句:“你俩咋在写同样的代码?”

这时你发现,

    1. 黑子和白子的行为是一模一样的
    1. 负责绘制画面的可交由棋盘系统
    1. 负责判定犯规、输赢的可交由规则系统

这个时候你和小弟说,我们搞三个对象:

“第一类玩家对象 负责接收用户输入,并告知第二类棋盘对象 起子布局的变化,棋盘对象接收到了棋子的变化就要负责在屏幕上面显示出这种变化,同时利用第三类规则对象来对棋局进行判定。”

小弟一听说要搞对象马上重振旗鼓开始开发~~

好了,故事讲到这里,我们其实就已经了解两种设计思想:

最开始你分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用的思想,就是【面向过程】

后来你把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描述某个事物在整个解决问题的步骤中的行为,我们用个高大上的名词包装下,称之为【面向对象】

二、面向对象的三大特征

虽然我们完成了boss的五子棋设计,但更多的时候,我们涉及的对象却远远不止三个。这个时候我们就要对对象、对象之间的关系进行设计~

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

听起来好高大上有木有,害怕你就输了~~~

封装

封装就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信类或者对象操作,对不可信的进行信息隐藏。

概念听起来比抽象本身还抽象,还是再举个栗子吧。

在我们日常聊天的时候,经常会说“这类人如何如何”、“那类怎样怎样”,如果你说过,那恭喜你,你已经知道什么是抽象了!

我们将不同的客观事物的共同点提取出来,它们的共同点可以是一些特征,也可以是都能完成的事情。

想想你的boss和小弟boy的共同点:

    1. 他们都是人类
    1. 他们都会下五子棋

那么我们可以抽象出一个 player 类,player类的属性是person,具有的方法是canPlay。如果有一天你对着神秘组织的人员名单找player,保证你名单翻来覆去找几遍都找不到,因为没有人叫player啊!player只是一个类,而boss和boy才是实现了这个类的实例对象。

继承

简单来说,我们现在已经有个player类了,player类的属性是person,方法是canPlay。后来你仔细一想,发现事情并不简单,别的组织会下五子棋的人居然也在这个类里面!这可不行,要知道你所在的神秘组织除了person属性、canPlay方法以外,还有beauty属性、canPlayGood方法。于是你打算搞个特别点的类,叫greatPlayer类。这个时候难道要重新写之前的player类的方法吗?nooooooo,我们所做的一切努力都是为了堵住那一句:“咋在写同样的代码?”

继承,就是使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行拓展。

而继承有两种实现方式:“继承”(Inheritance)和“组合”(Composition)。

首先我们要明白,组合和继承都是提高代码可重用性的手段。区别是在设计对象模型时,如果去划分类和类之间的关系。说白了就是如何对类和类进行拓展和复用。

继承:就是 is a的关系,比如说student继承person,则说明student is a person。

组合:设计类的时候把要组合的类的对象加入到该类中作为自己的成员变量。

比如说,如果发现两个类具有很多相同的方法,很相似需要抽象:

比如上图A,B两个类中method1,method2和method3三个方法都相同,

继承的抽象方式是:

组合的抽象方式是:

下面我们来比较下组合和继承的优缺点:

继承

优点:

  • 子类可以重写父类的方法来方便地实现对父类的扩展。

缺点:

  • 父类的内部细节对子类是可见的。这太不符合“你办事我放心”的想法了。

  • 如果对父类方法做了修改,子类的方法必须做出相应的修改。所以子类和父类是一种高耦合。

组合

优点:

  • 被包含的对象的内部细节对当前对象不可见,“你办事我放心”~

  • 当前对象与被包含的对象是一个低耦合关系,修改包含对象的类代码不需要修改当前对象类的代码~

缺点:

  • 容易产生过多对象

  • 为了能组合多个对象,必须对【接口】进行定义

所以,组合比继承更具有灵活性和稳定性,在设计的时候优先使用组合~

多态

多态的定义是同一个操作,作用在不同的对象上,可以产生不同的解释和不同的执行结果。

对于强类型语言,通常采用抽象类或者接口,进行更高一层的抽象,从而直接使用更高层的抽象。就好比强类型语言中里,有两个方法:int和int相加,float和float相加,既然都要相加,那就再抽象一下。实际上就是为了弱化具体的类型。

而对于像js这种弱类型语言来说,多态就是与生俱来的。

再来看多态的两种实现方式:覆盖重载

覆盖:子类重新定义父类的方法。我们的js原型链继承那套,不就是覆盖了吗~

重载:允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)

重载的话,像js确实没有,毕竟是弱类型语言。顺便一提,重载的概念并不属于“面向对象编程”,重载的实现是:编译器根据函数不同的参数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函数(对编译器)

有两个同名函数:function func(p:integer):integer;和function func(p:string):integer;。那么编译器做过修饰后的函数名称可能是这样的:int_func、str_func

对于这两个函数的调用,在编译器的时候就已经确定了,是静态的,其地址在编译器就绑定了。所以重载和多态无关。

而js本身是解释型语言而非编译型语言,所以肯定和重载没关系。

接口

刚刚我们说了,为了能组合多个对象,必须对【接口】进行定义。

那么接口到底是什么呢?

接口提供了一种用来说明一个对象应该具有哪些方法的手段。它可以表明这些方法的含义,但是却不包含具体的实现。我规定了你要做什么,但是你怎么做我可不管~

现在我们知道接口是什么了,但是为毛要用接口呢?

有了接口,我们就可以按对象提供的特性对它们进行分组。再再再举个栗子,现在有对象A、对象B以及接口I,即便对象A和对象B的差异巨大,但只要它们都实现了接口I,那么在A.I(B)方法中,就可以互换使用A和B,比如B.I(A)。那不是很爽吗!我可以互换使用A、B的方法了!

另外,还可以使用接口开发不同的类的共同性。如果把原本要求以一个特定的类为参数的函数改为要求以一个特定的接口为参数的函数,那么所有实现了该接口的对象都可以作为参数传递给它,这样做的话,明明彼此不相关的对象,也可以被相同地对待使用该方法了!

结语

面向对象是一门非常实用的设计思想,本文旨在让隔壁小孩也能看懂,如果内容有误欢迎在评论区吐槽~