为什么使用面向对象的设计方式?有什么优点?

参考:https://www.zhihu.com/question/19729316

https://www.zhihu.com/question/30082151

首先要理解,面向对象是一种设计方式/设计思想/设计规范,帮助我们更好更科学地设计程序。

一、我们讲个故事

上帝要创造世界,因为这个过程太过复杂,无从入手,所以先从一件简单的事情做起。

现在上帝要设计一个方法,用于描述狼吃羊这个事情。某只狼吃了某只羊,你可以面向过程地吃,eat(狼A, 羊A),也可以面向对象地吃,狼A.eat(羊A)。差别在哪里?只是写法有点变化。

狼吃羊的小问题解决了,上帝决定要模拟整个生物界。这里面很多东西可以吃,大鱼吃小鱼,小鱼吃虾米,吃不吃皮,吐不吐骨头,这时就要修改这个eat函数。

eat里面要判断很多东西,假如上帝很勤劳,所有代码都自己设计,那没关系,没太大区别,判断就判断呗(如果系统不复杂,可以进行管理,确实可以使用面向过程设计)。

比如说,上帝设计狼只吃羊,就要加上这样的判断:

随着设计的动物越来越多,上帝逐渐感觉没有足够的精力来管理整个东西了(总不可能每种动物吃的行为都在eat函数里加条件判断啊)。于是上帝雇了一群天使来协助设计。

因为不可能每个天使都来修改这个eat函数(否则eat函数将无比臃肿),所以要把eat函数进行拆分,比如拆成wolfEatSheep()、tigerEatWolf()。

那么问题来了,狼不止吃羊,有时也吃牛,所以上帝就把“wolfEatXX”这一系列的函数统一交给一个天使去做,就能解决“狼吃什么”的问题。

同理,上帝可以把“tigerEatXX”这一系列的函数统一交给另一个天使去做,就能解决“老虎吃什么”的问题。

现在问题变得更复杂了:动物不光要能吃,还能跑能跳,会说会叫。于是每个天使就要实现wolfRun,wolfRoar,wolfJump…..多了一大堆函数,相当相当的烦。怎么办?

于是上帝放弃从“做什么”的角度解决问题(放弃了面向过程设计模式),开始按照每种动物进行拆分(使用面向对象的设计模式),100个天使,每个天使创造一种动物。创造哪种动物,就站在哪种动物的角度考虑问题,我吃的时候怎么吃,跑的时候怎么跑,都跟别人无关。这么一来,天使的工作就简单专注多了。每个动物只关注我要做一些什么事情,不必站在上帝的角度考虑问题。这个过程,是类的划分过程,也就是封装的过程(面向对象的封装性)。

这时候,上帝觉得为每种生物安排衣食住行实在是太复杂了,干脆偷懒,设计一种继承结构吧。既然动物都会呼吸、进食、休息,那么就可以设计一个动物类animal,然后上帝对wolf说:“wolf是动物”(wolf继承animal类),wolf马上就学会了呼吸、进食、休息(子类继承父类的方法)。

从动物这个种类下,又可以分出各种纲,各种目,各种属。狮子是哺乳动物,猴子也是,但是狮子是猫科动物,猴子是灵长动物,这就构成了一个倒着的树状体系,一层一层形成继承关系。哺乳动物会喂奶,那么所有继承自哺乳动物的动物,都会自动拥有这个特征(面向对象的继承性)。

假设有一天变异出现了新种动物,上帝只要鉴别一下它属于什么类型,就知道这种动物大概能做什么事了、这种新动物的一举一动,必然拥有它所继承的种类的特征。

这样就能描述生物界了吗?不,还有那么一些怪胎的存在。你认为哺乳动物都不会飞,那就错了,因为蝙蝠会飞。那么,我们怎么描述蝙蝠这种动物?因为蝙蝠会飞就让蝙蝠继承鸟类吗?这当然不适合。

面对蝙蝠这种怪胎,上帝需要设置一个新的种类“飞翔的哺乳动物类”吗?因为会飞的哺乳类动物很少,所以这样做不划算。为了灵活处理这种问题,上帝更倾向于让蝙蝠“拥有飞翔的能力”,而不是另外建立一个种类。

蝙蝠会飞是它自身的特性,但是“飞”的能力并非蝙蝠独有。所以,可以定义一个“飞”的接口,如果上帝想要赐予一种动物飞的能力,就让这种生物实现接口。蝙蝠会飞,那么就让蝙蝠实现“飞”的接口。人类有了飞机之后也会“飞”,所以人类也可以实现“飞”的接口,但是至于怎么飞,就在接口方法中自己定义。

总之,是否面向对象只是思维方式的不同。做一个软件,面向对象也能做,不面向对象也能做。

我的观点是:如果关注可维护性和协作性,从目前的角度,面向对象是很好的选择。它很自然,很优雅,优雅得只要打一个“.”,你就能想起来什么事能做,什么事不能做。

通过这个故事,大概就能知道为什么我们使用面向对象。

二、我对面向对象三大特性的理解

(1)封装

1.封装的概念

封装是指将对象的状态信息隐藏在对象内部,不允许外部的程序直接访问对象内部的信息,而是通过该类所提供的方法来实现对内部信息的操作和访问。

假如我们是一个对象,体重是我们的私有属性,不是想改就能改的。如果要改变体重,就需要通过吃东西、运动等方法进行改变。

2.我认为封装有两个优点:

1.隐藏了实现细节

如果我最近变胖了,我妈就会叫我去减肥。过了十天,我妈就会得到一个苗条的儿子。我妈叫我减肥(调用方法),只是想让我变瘦而已(获得方法执行的结果),至于这十天我做了什么瘦下来(方法的实现细节),她完全可以不用知道。

就像我们调用HashMap的put方法,只是想把一个元素插入到HashMap中,仅此而已。我们不需要知道put方法的具体实现过程,就能得到想要的结果。

2.限制了对数据的操作方式

世间流传着一种快速减肥法,那就是一刀把身体的某个部位剁下来,从而起到减轻体重的效果。如果谁用这种方法减肥,那就是失了智。

我们是正常人,当然不会选择这么恐怖的减肥方法(根本不提供这个方法,想都别想),而是会选择运动、控制饮食等健康的方法去控制体重(通过提供的方法来实现对内部数据的操作和访问),从而保障了对象的完整性和安全(提供的方法在可控范围内,不会出现问题)。

(2)继承

大家都很清楚继承的概念和优点,这里就不叙述了。

我认为继承的作用之一在于对象的复用(作用之二是进行类之间的联系)。对象的复用有两种方式,那就是组合和继承。那么问题来了,选择哪一种方式比较好?

这里的“组合”好像又是一个新概念,其实我们平时已经用得很多了。如果我们在一个类中调用一个别的类,这种行为就是组合(把两个类组合起来)。

在选择组合或者继承的时候,我们要清楚:

组合是has a的关系,继承是is a的关系。

在选择之前问问自己是has a 还是 is a 的关系,就能做出选择了。如果不好选择的话,那就是说两种都可以,那就选组合。我个人认为组合用得比较多。

(3)多态

1.多态存在的前提

1.要有继承关系(无继承不成多态)。
2.子类要重写父类的方法(方法需要体现出不同的特征)。
3.父类引用指向子类(才能调用子类的方法)。

2.多态、方法重载、方法覆盖

一个方法名,参数不同,这叫方法重载。(Overload)

父类与子类有同样的方法名和参数,这叫方法覆盖。(Override)


父类引用指向子类对象,调用方法时会调用子类的实现(子承父业)。这不是父类的实现,而是多态。


我们光看“多态”这个词,确实不太好理解。但是,如果解释为:“事物的多种状态”,或者说“事物在运行过程中存在不同的状态”,就没有那么抽象了。

在上面的例子中,instance继承了Parent的名号,但是其本身是一个Child对象,所以instance.foo()实际调用的也是Child的foo()方法。

我理解的多态与继承体系有关,与方法覆盖有关,与方法重载无关。

3.多态的意义何在?

简单举个例子:

Mother.java

运行结果为:

在这段程序里,同一个Father对象(面向对象中的对象,而不是java概念中的对象,可以理解成java类)可以表现出两种状态(Father对象和Son对象),个人认为这就是多态的体现。

同时,根据传入实际对象的不同,Mother类的dinner()方法不仅能够照单全收,而且能够表现出不同的效果,这就提升了程序的灵活性

看到这里,你可能觉得:这根本没什么嘛,我们平时写代码不都是这样的吗?这就说明,即使我们未能深刻体会到多态的优点,却已经能够熟练地使用这一特性(甚至觉得不这样写是有问题的)。个人很难想象,没有多态的面向对象将会是怎样一番景象。

其实多态相对于前面两个概念还是没那么好懂,而且很容易和继承弄混。如果不经常回来看一看这篇笔记,我经常记不住多态的概念…

为了帮助大家理解,可以参考Stackoverflow上的文章:

https://stackoverflow.com/questions/154577/polymorphism-vs-overriding-vs-overloading

虽然不够雅观,但是足够生动形象。

三、总结

要对“为什么使用面向对象”有所了解,才能领悟到编程的科学之处。

发表评论

电子邮件地址不会被公开。 必填项已用*标注