在实际开发中,都是用面向对象的开发过程是怎样的来写的吗

什么是面向对象的开发过程是怎样的编程和面向对象的开发过程是怎樣的编程语言?

面向对象的开发过程是怎样的编程中有两个非常重要、非常基础的概念那就是类(class)和对象(object)。

面向对象的开发过程昰怎样的编程是一种编程范式或编程风格它以类或对象作为组织代码的基本单元,并将封装、抽象、继承、多态四个特性作为代码设計和实现的基石 。

面向对象的开发过程是怎样的编程语言是支持类或对象的语法机制并有现成的语法机制,能方便地实现面向对象的开發过程是怎样的编程四大特性(封装、抽象、继承、多态)的编程语言

一般来讲, 面向对象的开发过程是怎样的编程都是通过使用面向對象的开发过程是怎样的编程语言来进行的但是,不用面向对象的开发过程是怎样的编程语言我们照样可以进行面向对象的开发过程昰怎样的编程。反过来讲即便我们使用面向对象的开发过程是怎样的编程语言,写出来的代码也不一定是面向对象的开发过程是怎样的編程风格的也有可能是面向过程编程风格的。

在技术圈里封装、抽象、继承、多态也并不是固定地被叫作“四大特性”(features),也有人稱它们为面向对象的开发过程是怎样的编程的四大概念(concepts)、四大基石(cornerstones)、四大基础(fundamentals)、四大支柱(pillars)等等

如何判定一个编程语言是否是面向对象的开发过程是怎样的编程语言

如果按照严格的的萣义,需要有现成的语法支持类、对象、四大特性才能叫作面向对象的开发过程是怎样的编程语言如果放宽要求的话,只要某种编程语訁支持类、对象语法机制那基本上就可以说这种编程语言是面向对象的开发过程是怎样的编程语言了,不一定非得要求具有所有的四大特性

面向对象的开发过程是怎样的编程和面姠对象的开发过程是怎样的编程语言之间有何关系

面向对象的开发过程是怎样的编程一般使用面向对象的开发过程是怎样的编程语言来進行,但是不用面向对象的开发过程是怎样的编程语言,我们照样可以进行面向对象的开发过程是怎样的编程反过来讲,即便我们使鼡面向对象的开发过程是怎样的编程语言写出来的代码也不一定是面向对象的开发过程是怎样的编程风格的,也有可能是面向过程编程風格的

什么是面向对象的开发过程是怎样的分析和面向對象的开发过程是怎样的设计

OOA、OOD、OOP 三个连在一起就是面向对象的开发过程是怎样的分析、设计、编程(实现),正好是面向对象的开发過程是怎样的软件开发要经历的三个阶段

面向对象的开发过程是怎样的分析和设计两个概念相对来说要简单一些。面向对象的开发过程昰怎样的分析与设计中的“分析”和“设计”这两个词我们完全可以从字面上去理解,不需要过度解读简单类比软件开发中的需求分析、系统设计即可。不过你可能会说,那为啥前面还加了个修饰词“面向对象的开发过程是怎样的”呢有什么特殊的意义吗?

之所以茬前面加“面向对象的开发过程是怎样的”这几个字是因为我们是围绕着对象或类来做需求分析和设计的。分析和设计两个阶段最终的產出是类的设计包括程序被拆解为哪些类,每个类有哪些属性方法类与类之间如何交互等等。它们比其他的分析和设计更加具体、更加落地、更加贴近编码更能够顺利地过渡到面向对象的开发过程是怎样的编程环节。这也是面向对象的开发过程是怎样的分析和设计與其他分析和设计最大的不同点。

封装、抽象、继承、多态

首先我们来看封装特性。封装也叫作信息隱藏或者数据访问保护类通过暴露有限的访问接口,授权外部仅能通过类提供的方式(或者叫函数)来访问内部信息或者数据这句话怎么理解呢?我们通过一个简单的例子来解释一下
下面这段代码是金融系统中一个简化版的虚拟钱包的代码实现。在金融系统中我们會给每个用户创建一个虚拟钱包,用来记录用户在我们的系统中的虚拟货币量对于虚拟钱包的业务背景,这里你只需要简单了解一下即鈳

从代码中,Wallet 类主要有四个属性(也可以叫作成员变量)也就是我们前面定义中提到的信息或者数据。

我们参照封装特性对钱包的這四个属性的访问方式进行了限制。调用者只允许通过下面这六个方法来访问或者修改钱包里的数据

之所以这样设计,是因为从业务的角度来说id、createTime 在创建钱包的时候就确定好了,之后不应该再被改动所以,我们并没有在 Wallet 类中暴露 id、createTime 这两个属性的任何修改方法,比如 set 方法而且,这两个属性的初始化设置对于 Wallet 类的调用者来说,也应该是透明的所以,我们在 Wallet 类的构造函数内部将其初始化设置好而鈈是通过构造函数的参数来外部赋值。

对于封装这个特性我们需要编程语言本身提供一定的语法机制来支持。这个语法机制就是访问权限控制
例子中的 private、public 等关键字就是 Java 语言中的访问权限控制语法。private 关键字修饰的属性只能类本身访问可以保护其不被类之外的代码直接访問。如果 Java 语言没有提供访问权限控制语法所有的属性默认都是 public 的,那任意外部代码都可以通过类似 wallet.id=123; 这样的方式直接访问、修改属性也僦没办法达到隐藏信息和保护数据的目的了,也就无法支持封装特性了

封装的意义是什么?它能解决什么编程问题

如果我们对类中属性的访问不做限制,那任何代码都可以访问、修改类中的属性虽然这样看起来更加灵活,泹从另一方面来说过度灵活也意味着不可控,属性可以随意被以各种奇葩的方式修改而且修改逻辑可能散落在代码中的各个角落,势必影响代码的可读性、可维护性比如某个同事在不了解业务逻辑的情况下,在某段代码中“偷偷地”重设了 wallet 中的 balanceLastModifiedTime

除此之外类仅仅通过囿限的方法暴露必要的操作,也能提高类的易用性如果我们把类属性都暴露给类的调用者,调用者想要正确地操作这些属性就势必要對业务细节有足够的了解。而这对于调用者来说也是一种负担相反,如果我们将属性封装起来暴露少许的几个必要的方法给调用者使鼡,调用者就不需要了解太多背后的业务细节用错的概率就减少很多。这就好比如果一个冰箱有很多按钮,你就要研究很长时间还鈈一定能操作正确。相反如果只有几个必要的按钮,比如开、停、调节温度你一眼就能知道该如何来操作,而且操作出错的概率也会降低很多

封装也叫作信息隐藏或者数据访问保护。类通过暴露有限的访问接口授权外部仅能通过类提供的方式来访问内蔀信息或者数据。它需要编程语言提供权限访问控制语法来支持例如 Java 中的 private、protected、public 关键字。封装特性存在的意义一方面是保护数据不被随意修改,提高代码的可维护性;另一方面是仅暴露有限的必要接口提高类的易用性。

封装主要讲的是如何隐藏信息、保护数据而抽象讲的是如何隐藏方法的具体实现,让调用者只需要关心方法提供了哪些功能并不需要知道这些功能是如何实现的。

在面向对象嘚开发过程是怎样的编程中我们常借助编程语言提供的接口类(比如 Java 中的 interface 关键字语法)或者抽象类(比如 Java 中的 abstract 关键字语法)这两种语法機制,来实现抽象这一特性

这里我稍微说明一下,我们把编程语言提供的接口语法叫作“接口类”而不是“接口”之所以这么做,是洇为“接口”这个词太泛化可以指好多概念,比如 API 接口等所以,我们用“接口类”特指编程语言提供的接口语法

对于抽象这个特性,我举一个例子来进一步解释一下

在上面的这段代码中,我们利用 Java 中的 interface 接口语法来实现抽象特性调用者在使用图片存储功能的时候,呮需要了解 IPictureStorage 这个接口类暴露了哪些方法就可以了不需要去查看 PictureStorage 类里的具体实现逻辑。

实际上抽象这个特性是非常容易实现的,并不需偠非得依靠接口类或者抽象类这些特殊语法机制来支持换句话说,并不是说一定要为实现类(PictureStorage)抽象出接口类(IPictureStorage)才叫作抽象。即便鈈编写 IPictureStorage 接口类单纯的 PictureStorage 类本身就满足抽象特性。

之所以这么说那是因为,类的方法是通过编程语言中的“函数”这一语法机制来实现的通过函数包裹具体的实现逻辑,这本身就是一种抽象调用者在使用函数的时候,并不需要去研究函数内部的实现逻辑只需要通过函數的命名、注释或者文档,了解其提供了什么功能就可以直接使用了。比如我们在使用 C 语言的 malloc() 函数的时候,并不需要了解它的底层代碼是怎么实现的

抽象有时候会被排除在面向对象的开发过程是怎样的的四大特性之外,抽象这个概念是一个非常通用的设计思想并不單单用在面向对象的开发过程是怎样的编程中,也可以用来指导架构设计等而且这个特性也并不需要编程语言提供特殊的语法机制来支歭,只需要提供“函数”这一非常基础的语法机制就可以实现抽象特性、所以,它没有很强的“特异性”有时候并不被看作面向对象嘚开发过程是怎样的编程的特性之一。

抽象的意义是什么它能解决什么编程问题?

实际上如果上升一个思考层面的话,抽象及其前面讲到的封装都是人类处理复杂性的有效手段在面对复杂系统的时候,人脑能承受的信息复雜程度是有限的所以我们必须忽略掉一些非关键性的实现细节。而抽象作为一种只关注功能点不关注实现的设计思路正好帮我们的大腦过滤掉许多非必要的信息。

除此之外抽象作为一个非常宽泛的设计思想,在代码设计中起到非常重要的指导作用。很多设计原则都體现了抽象这种设计思想比如基于接口而非实现编程、开闭原则(对扩展开放、对修改关闭)、代码解耦(降低代码的耦合性)等。我們在讲到后面的内容的时候会具体来解释。

换一个角度来考虑我们在定义(或者叫命名)类的方法的时候,也要有抽象思维不要在方法定义中,暴露太多的实现细节以保证在某个时间点需要改变方法的实现逻辑的时候,不用去修改其定义举个简单例子,比如 getAliyunPictureUrl() 就不昰一个具有抽象思维的命名因为某一天如果我们不再把图片存储在阿里云上,而是存储在私有云上那这个命名也要随之被修改。相反如果我们定义一个比较抽象的函数,比如叫作 getPictureUrl()那即便内部存储方式修改了,我们也不需要修改命名

封装主要讲如何隐藏信息、保护数据,那抽象就是讲如何隐藏方法的具体实现让使用者只需要关心方法提供了哪些功能,不需要知道这些功能是如何实现嘚抽象可以通过接口类或者抽象类来实现,但也并不需要特殊的语法机制来支持抽象存在的意义,一方面是提高代码的可扩展性、维護性修改实现不需要改变定义,减少代码的改动范围;另一方面它也是处理复杂系统的有效手段,能有效地过滤掉不必要关注的信息

继承是用来表示类之间的 is-a 关系,比如猫是一种哺乳动物从继承关系上来讲,继承可以分为两种模式单继承和多继承。单继承表示一个子类只继承一个父类多继承表示一个子类可以继承多个父类,比如猫既是哺乳动物又是爬行动物。

为了实现继承这个特性编程语言需要提供特殊的语法机制来支持,比如 Java 使用 extends 关键字来实现继承C++ 使用冒号(class B : public A),Python 使用 paraentheses()Ruby 使用 <。不过有些编程语言只支持单继承,不支持多重继承比如 Java、PHP、C#、Ruby 等,而有些编程语言既支持单重继承也支持多重继承,比如 C++、Python、Perl 等

继承存在的意义是什么?它能解决什么编程问题

继承最大的一个好处就是代码复用。假如两个类有一些相同的属性和方法我们就可以将这些相同的部分,抽取到父类中让两个子类继承父类。这样两个子类就可以重用父类中的代码,避免代码重复写哆遍不过,这一点也并不是继承所独有的我们也可以通过其他方式来解决这个代码复用的问题,比如利用组合关系而不是继承关系
洳果我们再上升一个思维层面,去思考继承这一特性可以这么理解:我们代码中有一个猫类,有一个哺乳动物类猫属于哺乳动物,从囚类认知的角度上来说是一种 is-a 关系。我们通过继承来关联两个类反应真实世界中的这种关系,非常符合人类的认知而且,从设计的角度来说也有一种结构美感。

继承的概念很好理解也很容易使用。不过过度使用继承,继承层次过深过复杂就会导致代码可读性、可维护性变差。为了了解一个类的功能我们不仅需要查看这个类的代码,还需要按照继承关系一层一层地往上查看“父类、父类的父類……”的代码还有,子类和父类高度耦合修改父类的代码,会直接影响到子类

所以,继承这个特性也是一个非常有争议的特性佷多人觉得继承是一种反模式。我们应该尽量少用甚至不用。

继承是用来表示类之间的 is-a 关系分为两种模式:单继承和多繼承。单继承表示一个子类只继承一个父类多继承表示一个子类可以继承多个父类。为了实现继承这个特性编程语言需要提供特殊的語法机制来支持。继承主要是用来解决代码复用的问题

多态是指,子类可以替换父类在实际的代码运行过程中,调用子类的方法实现

对于多态这种特性,纯文字解释不好理解我们还是看一个具体的例子。

多态这种特性也需要编程语言提供特殊的语法机制来實现在上面的例子中,我们用到了三个语法机制来实现多态

第一个语法机制是编程语言要支持父类对象可以引用子类对象,也就是可鉯将 SortedDynamicArray 传递给 DynamicArray

对于多态特性的实现方式,除了利用“继承加方法重写”这种实现方式之外我们还有其他两种比较常见的的实现方式,一個是利用接口类语法另一个是利用 duck-typing 语法。不过并不是每种编程语言都支持接口类或者 duck-typing 这两种语法机制,比如 C++ 就不支持接口类语法而 duck-typing 呮有一些动态语言才支持,比如 Python、JavaScript 等

如何利用接口类来实现多态特性

我们还是先来看一段代码。

我们还是先来看一段代码。这是一段 Python 代码

从这段代码中,我们发现duck-typing 实现多态的方式非常灵活。Logger 和 DB 两个类没有任哬关系既不是继承关系,也不是接口和实现的关系但是只要它们都有定义了 record() 方法,就可以被传递到 test() 方法中在实际运行的时候,执行對应的 record() 方法

也就是说,只要两个类具有相同的方法就可以实现多态,并不要求两个类之间有任何关系这就是所谓的 duck-typing,是一些动态语訁所特有的语法机制而像 Java 这样的静态语言,通过继承实现多态特性必须要求两个类之间有继承关系,通过接口实现多态特性类必须實现对应的接口。

多态特性存在的意义是什么它能解决什么编程问题?

多态特性能提高代码的可扩展性和复用性为什么这么说呢?我们回过头去看讲解多态特性的时候举的第二个代码实例(Iterator 的例子)。

在那个例子Φ我们利用多态的特性,仅用一个 print() 函数就可以实现遍历打印不同类型(Array、LinkedList)集合的数据当再增加一种要遍历打印的类型的时候,比如 HashMap我们只需让 HashMap 实现 Iterator 接口,重新实现自己的 hasNext()、next() 等方法就可以了完全不需要改动 print() 函数的代码。所以说多态提高了代码的可扩展性。

linkedList) 函数洏利用多态特性,我们只需要实现一个 print() 函数的打印逻辑就能应对各种集合数据的打印操作,这显然提高了代码的复用性

哆态是指子类可以替换父类,在实际的代码运行过程中调用子类的方法实现。多态这种特性也需要编程语言提供特殊的语法机制来实现比如继承、接口类、duck-typing。多态可以提高代码的扩展性和复用性是很多设计模式、设计原则、编程技巧的代码实现基础。

除了面向对象的开发过程是怎样的之外被大家熟知的编程范式还有另外两种,面向过程编程和函数式编程面向过程这种编程范式隨着面向对象的开发过程是怎样的的出现,已经慢慢退出了舞台而函数式编程目前还没有被广泛接受。

在过往的工作中很多人搞不清楚面向对象的开发过程是怎样的和面向过程的区别,总以为使用面向对象的开发过程是怎样的编程语言来做开发就是在进行面向对象的開发过程是怎样的编程了。而实际上他们只是在用面向对象的开发过程是怎样的编程语言,编写面向过程风格的代码而已并没有发挥媔向对象的开发过程是怎样的编程的优势。这就相当于手握一把屠龙刀却只是把它当作一把普通的刀剑来用,相当可惜

什么是面向过程编程与面向过程编程语言

对比着面向对象的开发过程是怎样的编程和面向对象的开发过程昰怎样的编程语言这两个概念,来理解面向过程编程和面向过程编程语言
面向对象的开发过程是怎样的编程是一种编程范式或编程风格。它以类或对象作为组织代码的基本单元并将封装、抽象、继承、多态四个特性,作为代码设计和实现的基石

面向对象的开发过程是怎样的编程语言是支持类或对象的语法机制,并有现成的语法机制能方便地实现面向对象的开发过程是怎样的编程四大特性(封装、抽潒、继承、多态)的编程语言。

类比面向对象的开发过程是怎样的编程与面向对象的开发过程是怎样的编程语言的定义对于面向过程编程和面向过程编程语言这两个概念,我给出下面这样的定义

面向过程编程也是一种编程范式或编程风格。它以过程(可以为理解方法、函数、操作)作为组织代码的基本单元以数据(可以理解为成员变量、属性)与方法相分离为最主要的特点。面向过程风格是一种流程囮的编程风格通过拼接一组顺序执行的方法来操作数据完成一项功能。

面向过程编程语言首先是一种编程语言它最大的特点是不支持類和对象两个语法概念,不支持丰富的面向对象的开发过程是怎样的编程特性(比如继承、多态、封装)仅支持面向过程编程。

这里出嘚面向过程编程和面向过程编程语言的定义也并、不是严格的官方定义。之所以要给出这样的定义只是为了跟面向对象的开发过程是怎样的编程及面向对象的开发过程是怎样的编程语言做个对比,以方便你理解它们的区别

定义不是很严格,也比较抽象所以,我再用┅个例子进一步解释一下假设我们有一个记录了用户信息的文本文件 users.txt,每行文本的格式是 name&age&gender(比如小王 &28& 男)。我们希望写一个程序从 users.txt 攵件中逐行读取用户信息,然后格式化成 name age gender(其中 是分隔符)这种文本格式,并且按照 age 从小到达排序之后重新写入到另一个文本文件 formatted_users.txt 中。针对这样一个小程序的开发我们一块来看看,用面向过程和面向对象的开发过程是怎样的两种编程风格编写出来的代码有什么不同。

首先我们先来看,用面向过程这种编程风格写出来的代码是什么样子的注意,下面的代码是用 C 语言这种面向过程的编程语言来编写嘚

// 按照年龄从小到大排序 users

然后,我们再来看用面向对象的开发过程是怎样的这种编程风格写出来的代码是什么样子的。注意下面的玳码是用 Java 这种面向对象的开发过程是怎样的的编程语言来编写的。

从上面的代码中我们可以看出,面向过程和面向对象的开发过程是怎樣的最基本的区别就是代码的组织方式不同。面向过程风格的代码被组织成了一组方法集合及其数据结构(struct User)方法和数据结构的定义昰分开的。面向对象的开发过程是怎样的风格的代码被组织成一组类方法和数据结构被绑定一起,定义在类中

面向对象的开发过程是怎样的编程相比面向过程编程有哪些优势

OOP 更加能够应对大规模复杂程序的开发

看了刚刚举的那个格式化文本文件的例子,你可能会有这样的疑问两种编程风格实现的代碼貌似差不多啊,顶多就是代码的组织方式有点区别没有感觉到面向对象的开发过程是怎样的编程有什么明显的优势呀!你的感觉没错。之所以有这种感觉主要原因是这个例子程序比较简单、不够复杂。

对于简单程序的开发来说不管是用面向过程编程风格,还是用面姠对象的开发过程是怎样的编程风格差别确实不会很大,甚至有的时候面向过程的编程风格反倒更有优势。因为需求足够简单整个程序的处理流程只有一条主线,很容易被划分成顺序执行的几个步骤然后逐句翻译成代码,这就非常适合采用面向过程这种面条式的编程风格来实现
但对于大规模复杂程序的开发来说,整个程序的处理流程错综复杂并非只有一条主线。如果把整个程序的处理流程画出來的话会是一个网状结构。如果我们再用面向过程编程这种流程化、线性的思维方式去翻译这个网状结构,去思考如何把程序拆解为┅组顺序执行的方法就会比较吃力。这个时候面向对象的开发过程是怎样的的编程风格的优势就比较明显了。

面向对象的开发过程是怎样的编程是以类为思考对象在进行面向对象的开发过程是怎样的编程的时候,我们并不是一上来就去思考如何将复杂的流程拆解为┅个一个方法,而是采用曲线救国的策略先去思考如何给业务建模,如何将需求翻译为类如何给类之间建立交互关系,而完成这些工莋完全不需要考虑错综复杂的处理流程当我们有了类的设计之后,然后再像搭积木一样按照处理流程,将类组装起来形成整个程序這种开发模式、思考问题的方式,能让我们在应对复杂程序开发的时候思路更加清晰。

除此之外面向对象的开发过程是怎样的编程还提供了一种更加清晰的、更加模块化的代码组织方式。比如我们开发一个电商交易系统,业务逻辑复杂代码量很大,可能要定义数百個函数、数百个数据结构那如何分门别类地组织这些函数和数据结构,才能不至于看起来比较凌乱呢类就是一种非常好的组织这些函數和数据结构的方式,是一种将代码模块化的有效手段

你可能会说,像 C 语言这种面向过程的编程语言我们也可以按照功能的不同,把函数和数据结构放到不同的文件里以达到给函数和数据结构分类的目的,照样可以实现代码的模块化你说得没错。只不过面向对象的開发过程是怎样的编程本身提供了类的概念强制你做这件事情,而面向过程编程并不强求这也算是面向对象的开发过程是怎样的编程楿对于面向过程编程的一个微创新吧。
实际上利用面向过程的编程语言照样可以写出面向对象的开发过程是怎样的风格的代码,只不过鈳能会比用面向对象的开发过程是怎样的编程语言来写面向对象的开发过程是怎样的风格的代码付出的代价要高一些。而且面向过程編程和面向对象的开发过程是怎样的编程并非完全对立的。很多软件开发中尽管利用的是面向过程的编程语言,也都有借鉴面向对象的開发过程是怎样的编程的一些优点

OOP 风格的代码更易复用、易扩展、易维护

在刚刚的那个例子中,洇为代码比较简单所以只用到到了类、对象这两个最基本的面向对象的开发过程是怎样的概念,并没有用到更加高级的四大特性封装、抽象、继承、多态。因此面向对象的开发过程是怎样的编程的优势其实并没有发挥出来。

面向过程编程是一种非常简单的编程风格並没有像面向对象的开发过程是怎样的编程那样提供丰富的特性。而面向对象的开发过程是怎样的编程提供的封装、抽象、继承、多态这些特性能极大地满足复杂的编程需求,能方便我们写出更易复用、易扩展、易维护的代码

首先,我们先来看下封装特性封装特性是媔向对象的开发过程是怎样的编程相比于面向过程编程的一个最基本的区别,因为它基于的是面向对象的开发过程是怎样的编程中最基本嘚类的概念面向对象的开发过程是怎样的编程通过类这种组织代码的方式,将数据和方法绑定在一起通过访问权限控制,只允许外部調用者通过类暴露的有限方法访问数据而不会像面向过程编程那样,数据可以被任意方法随意修改因此,面向对象的开发过程是怎样嘚编程提供的封装特性更有利于提高代码的易维护性

其次,我们再来看下抽象特性我们知道,函数本身就是一种抽象它隐藏了具体嘚实现。我们在使用函数的时候只需要了解函数具有什么功能,而不需要了解它是怎么实现的从这一点上,不管面向过程编程还是是媔向对象的开发过程是怎样的编程都支持抽象特性。不过面向对象的开发过程是怎样的编程还提供了其他抽象特性的实现方式。这些實现方式是面向过程编程所不具备的比如基于接口实现的抽象。基于接口的抽象可以让我们在不改变原有实现的情况下,轻松替换新嘚实现逻辑提高了代码的可扩展性。

再次我们来看下继承特性。继承特性是面向对象的开发过程是怎样的编程相比于面向过程编程所特有的两个特性之一(另一个是多态)如果两个类有一些相同的属性和方法,我们就可以将这些相同的代码抽取到父类中,让两个子類继承父类这样两个子类也就可以重用父类中的代码,避免了代码重复写多遍提高了代码的复用性。

最后我们来看下多态特性。基於这个特性我们在需要修改一个功能实现的时候,可以通过实现一个新的子类的方式在子类中重写原来的功能逻辑,用子类替换父类在实际的代码运行过程中,调用子类新的功能逻辑而不是在原有代码上做修改。这就遵从了“对修改关闭、对扩展开放”的设计原则提高代码的扩展性。除此之外利用多态特性,不同的类对象可以传递给相同的方法执行不同的代码逻辑,提高了代码的复用性

所鉯说,基于这四大特性利用面向对象的开发过程是怎样的编程,我们可以更轻松地写出易复用、易扩展、易维护的代码当然,我们不能说利用面向过程风格就不可以写出易复用、易扩展、易维护的代码,但没有四大特性的帮助付出的代价可能就要高一些。

OOP 语言更加人性化、更加高级、更加智能

人类最开始跟机器打交道是通过 0、1 这样的二进制指令然后是汇编语訁,再之后才出现了高级编程语言在高级编程语言中,面向过程编程语言又早于面向对象的开发过程是怎样的编程语言出现之所以先絀现面向过程编程语言,那是因为跟机器交互的方式从二进制指令、汇编语言到面向过程编程语言,是一个非常自然的过渡都是一种鋶程化的、面条式的编程风格,用一组指令顺序操作数据来完成一项任务。

从指令到汇编再到面向过程编程语言跟机器打交道的方式茬不停地演进,从中我们很容易发现这样一条规律那就是编程语言越来越人性化,让人跟机器打交道越来越容易笼统点讲,就是编程語言越来越高级实际上,在面向过程编程语言之后面向对象的开发过程是怎样的编程语言的出现,也顺应了这样的发展规律也就是說,面向对象的开发过程是怎样的编程语言比面向过程编程语言更加高级!

跟二进制指令、汇编语言、面向过程编程语言相比面向对象嘚开发过程是怎样的编程语言的编程套路、思考问题的方式,是完全不一样的前三者是一种计算机思维方式,而面向对象的开发过程是怎样的是一种人类的思维方式我们在用前面三种语言编程的时候,我们是在思考如何设计一组指令,告诉机器去执行这组指令操作某些数据,帮我们完成某个任务而在进行面向对象的开发过程是怎样的编程时候,我们是在思考如何给业务建模,如何将真实的世界映射为类或者对象这让我们更加能聚焦到业务本身,而不是思考如何跟机器打交道可以这么说,越高级的编程语言离机器越“远”離我们人类越“近”,越“智能”

在面向对象的开发过程是怎样的编程中,抽象类和接口是两个经常被用到的语法概念昰面向对象的开发过程是怎样的四大特性,以及很多设计模式、设计思想、设计原则编程实现的基础比如,我们可以使用接口来实现面姠对象的开发过程是怎样的的抽象特性、多态特性和基于接口而非实现的设计原则使用抽象类来实现面向对象的开发过程是怎样的的继承特性和模板设计模式等等。

不过并不是所有的面向对象的开发过程是怎样的编程语言都支持这两个语法概念,比如C++ 这种编程语言只支持抽象类,不支持接口;而像 Python 这样的动态编程语言既不支持抽象类,也不支持接口尽管有些编程语言没有提供现成的语法来支持接ロ和抽象类,我们仍然可以通过一些手段来模拟实现这两个语法概念

这两个语法概念不仅在工作中经常会被用到,在面试中也经常被提忣比如,“接口和抽象类的区别是什么什么时候用接口?什么时候用抽象类抽象类和接口存在的意义是什么?能解决哪些编程问题”等等。

什么是抽象类和接口区别在哪里?

不同的编程语言对接口和抽象类的定义方式可能有些差别但差别并不会很大。Java 这种编程语言既支持抽象类,也支持接口所以,为了让你对这两个语法概念有比较直观的认识我们拿 Java 这種编程语言来举例讲解。

首先我们来看一下,在 Java 这种编程语言中我们是如何定义抽象类的。

下面这段代码是一个比较典型的抽象类的使用场景(模板设计模式)

Logger 是一个记录日志的抽象类,FileLogger 和 MessageQueueLogger 继承 Logger分别实现两种不同的日志记录方式:记录日志到文件中和记录日志到消息队列中。

// 抽象类的子类:输出日志到文件 // 抽象类的子类: 输出日志到消息中间件 (比如 kafka)

通过上面的这个例子我们来看一下,抽象类具有哪些特性我总结了下面三点。

  1. 抽象类不允许被实例化只能被继承。也就是说你不能 new 一个抽象类的对象出来(Logger logger = new Logger(…); 会报编译错误)。

  2. 抽象類可以包含属性和方法方法既可以包含代码实现(比如 Logger 中的 log() 方法),也可以不包含代码实现(比如 Logger 中的 doLog() 方法)不包含代码实现的方法叫作抽象方法。

  3. 子类继承抽象类必须实现抽象类中的所有抽象方法。对应到例子代码中就是所有继承 Logger 抽象类的子类,都必须重写 doLog() 方法

刚刚我们讲了如何定义抽象类,现在我们再来看一下在 Java 这种编程语言中,我们如何定义接口

// 接口实现类:鉴权过滤器 // 接口实现类:限流过滤器

上面这段代码是一个比较典型的接口的使用场景。我们通过 Java 中的 interface 关键字定义了一个 Filter 接口AuthencationFilter 和 RateLimitFilter 是接口的两个实现类,分别实现了對 RPC 请求鉴权和限流的过滤功能

代码非常简洁。结合代码我们再来看一下,接口都有哪些特性总结了三点。

  1. 接口不能包含属性(也就昰成员变量)
  2. 接口只能声明方法,方法不能包含代码实现
  3. 类实现接口的时候,必须实现接口中声明的所有方法

前面我们讲了抽象类囷接口的定义,以及各自的语法特性从语法特性上对比,这两者有比较大的区别比如抽象类中可以定义属性、方法的实现,而接口中鈈能定义属性方法也不能包含代码实现等等。除了语法特性从设计的角度,两者也有比较大的区别

抽象类实际上就是类,只不过是┅种特殊的类这种类不能被实例化为对象,只能被子类继承我们知道,继承关系是一种 is-a 的关系那抽象类既然属于类,也表示一种 is-a 的關系相对于抽象类的 is-a 关系来说,接口表示一种 has-a 关系表示具有某些功能。对于接口有一个更加形象的叫法,那就是协议(contract)

抽象类和接口能解决什么编程问题

刚刚我们学习了抽象类和接口的定义和区别,现在来学习一下抽象类囷接口存在的意义,让你知其然知其所以然

为什么需要抽象类?它能够解决什么编程问題

刚刚我们讲到,抽象类不能实例化只能被继承。而前面的章节中我们还讲到,继承能解决代码复用的问题所以,抽象类也是为玳码复用而生的多个子类可以继承抽象类中定义的属性和方法,避免在子类中重复编写相同的代码。

不过既然继承本身就能达到代碼复用的目的,而继承也并不要求父类一定是抽象类那我们不使用抽象类,照样也可以实现继承和复用从这个角度上来讲,我们貌似並不需要抽象类这种语法呀那抽象类除了解决代码复用的问题,还有什么其他存在的意义吗

我们还是拿之前那个打印日志的例子来讲解。我们先对上面的代码做下改造在改造之后的代码中,Logger 不再是抽象类只是一个普通的父类,删除了 Logger 中 log()、doLog() 方法新增了 isLoggable() 方法。FileLogger 和 MessageQueueLogger 还是繼承 Logger 父类以达到代码复用的目的。具体的代码如下:

//...构造函数不变代码省略... // 子类:输出日志到文件 //...构造函数不变,代码省略... // 抽象类的孓类: 输出日志到消息中间件 (比如 kafka) //...构造函数不变代码省略...

这个设计思路虽然达到了代码复用的目的,但是无法使用多态特性了像下面这樣编写代码,就会出现编译错误因为 Logger 中并没有定义 log() 方法。

你可能会说这个问题解决起来很简单啊。我们在 Logger 父类中定义一个空的 log() 方法,让子类重写父类的 log() 方法实现自己的记录日志的逻辑,不就可以了吗

这个设计思路能用,但是它显然没有之前通过抽象类的实现思蕗优雅。为什么这么说呢主要有以下几点原因。

这个设计思路能用但是,它显然没有之前通过抽象类的实现思路优雅我为什么这么說呢?主要有以下几点原因

  1. 在 Logger 中定义一个空的方法,会影响代码的可读性如果我们不熟悉 Logger 背后的设计思想,代码注释又不怎么给力峩们在阅读 Logger 代码的时候,就可能对为什么定义一个空的 log() 方法而感到疑惑需要查看 Logger、FileLogger、MessageQueueLogger 之间的继承关系,才能弄明白其设计意图

  2. 当创建┅个新的子类继承 Logger 父类的时候,我们有可能会忘记重新实现 log() 方法之前基于抽象类的设计思路,编译器会强制要求子类重写 log() 方法否则会報编译错误。你可能会说我既然要定义一个新的 Logger 子类,怎么会忘记重新实现 log() 方法呢我们举的例子比较简单,Logger 中的方法不多代码行数吔很少。但是如果 Logger 有几百行,有 n 多方法除非你对 Logger 的设计非常熟悉,否则忘记重新实现 log() 方法也不是不可能的。

  3. Logger 可以被实例化换句话說,我们可以 new 一个 Logger 出来并且调用空的 log() 方法。这也增加了类被误用的风险当然,这个问题可以通过设置私有的构造函数的方式来解决鈈过,显然没有通过抽象类来的优雅

为什么需要接口?它能够解决什么编程问题

抽象类哽多的是为了代码复用,而接口就更侧重于解耦接口是对行为的一种抽象,相当于一组协议或者契约你可以联想类比一下 API 接口。调用鍺只需要关注抽象的接口不需要了解具体的实现,具体的实现代码对调用者透明接口实现了约定和实现相分离,可以降低代码间的耦匼性提高代码的可扩展性。

实际上接口是一个比抽象类应用更加广泛、更加重要的知识点。比如我们经常提到的“基于接口而非实現编程”,就是一条几乎天天会用到并且能极大地提高代码的灵活性、扩展性的设计思想。

如何模拟抽象类和接口两个语法概念?

在前面举的例子中我们使用 Java 的接口语法实现了一个 Filter 过滤器。不过C++ 只有抽象类,并没有接口那从代碼实现的角度上来说,是不是就无法实现 Filter 的设计思路了呢

实际上,我们可以通过抽象类来模拟接口怎么来模拟呢?这是一个不错的面試题

我们先来回忆一下接口的定义:接口中没有成员变量,只有方法声明没有方法实现,实现接口的类必须实现接口中的所有方法呮要满足这样几点,从设计的角度上来说我们就可以把它叫作接口。实际上要满足接口的这些语法特性并不难。在下面这段 C++ 代码中峩们就用抽象类模拟了一个接口(下面这段代码实际上是策略模式中的一段代码)。

抽象类 Strategy 没有定义任何属性并且所有的方法都声明为 virtual 類型(等同于 Java 中的 abstract 关键字),这样所有的方法都不能有代码实现,并且所有继承这个抽象类的子类都要实现这些方法。从语法特性上來看这个抽象类就相当于一个接口。

不过如果你熟悉的既不是 Java,也不是 C++而是现在比较流行的动态编程语言,比如 Python、Ruby 等你可能还会囿疑问:在这些动态语言中,不仅没有接口的概念也没有类似 abstract、virtual 这样的关键字来定义抽象类,那该如何实现上面的讲到的 Filter、Logger 的设计思路呢实际上,除了用抽象类来模拟接口之外我们还可以用普通类来模拟接口。具体的 Java 代码实现如下所示

我们知道类中的方法必须包含實现,这个不符合接口的定义但是,我们可以让类中的方法抛出 MethodUnSupportedException 异常来模拟不包含实现的接口,并且能强迫子类在继承这个父类的时候都去主动实现父类的方法,否则就会在运行时抛出异常那又如何避免这个类被实例化呢?实际上很简单我们只需要将这个类的构慥函数声明为 protected 访问权限就可以了。

刚刚我们讲了如何用抽象类来模拟接口以及如何用普通类来模拟接口,那如何用普通类来模拟抽象类呢这个问题留给你自己思考,你可以留言说说你的实现方法

实际上,对于动态编程语言来说还有一种对接口支持的策略,那就是 duck-typing

如何决定该用抽象类还是接口

刚刚的讲解可能有些偏理论,现在我们就从真实项目开发的角度来看一丅,在代码设计、编程开发的时候什么时候该用抽象类?什么时候该用接口

实际上,判断的标准很简单如果我们要表示一种 is-a 的关系,并且是为了解决代码复用的问题我们就用抽象类;如果我们要表示一种 has-a 关系,并且是为了解决抽象而非代码复用的问题那我们就可鉯使用接口。

从类的继承层次上来看抽象类是一种自下而上的设计思路,先有子类的代码重复然后再抽象成上层的父类(也就是抽象類)。而接口正好相反它是一种自上而下的设计思路。我们在编程的时候一般都是先设计接口,再去考虑具体的实现

  1. 抽象类和接口的语法特性
    抽象类不允许被实例化,只能被继承它可以包含属性和方法。方法既可以包含代码实现也可以不包含代码实现。不包含代码实现的方法叫作抽象方法子类继承抽象类,必须实现抽象类中的所有抽象方法接口不能包含属性,只能声明方法方法不能包含代码实现。类实现接口的时候必须实现接口中声明的所有方法。
  2. 抽象类和接口存在的意义
    抽象类是对成员变量和方法的抽象是一种 is-a 關系,是为了解决代码复用问题接口仅仅是对方法的抽象,是一种 has-a 关系表示具有某一组行为特性,是为了解决解耦问题隔离接口和具体的实现,提高代码的扩展性
  3. 抽象类和接口的应用场景区别
    什么时候该用抽象类?什么时候该用接口实际上,判断的标准很简单洳果要表示一种 is-a 的关系,并且是为了解决代码复用问题我们就用抽象类;如果要表示一种 has-a 关系,并且是为了解决抽象而非代码复用问题那我们就用接口。

基于接口而非实现编程这个原则非常重要是一种非常有效的提高代码质量的手段,在平时的開发中特别经常被用到

为了让你理解透彻,并真正掌握这条原则如何应用将结合一个有关图片存储的实战案例来讲解。除此之外这條原则还很容易被过度应用,比如为每一个实现类都定义对应的接口针对这类问题,怎样恰到好处地应用这条原则

如何解读原则中的“接口”二字

“基于接口而非实现编程”这条原则的英文描述是:“Program to an interface, not an implementation”。我们理解这条原则的时候千万鈈要一开始就与具体的编程语言挂钩,局限在编程语言的“接口”语法中(比如 Java 中的 interface 接口语法)这条原则最早出现于 1994 年 GoF 的《设计模式》這本书,它先于很多编程语言而诞生(比如 Java 语言)是一条比较抽象、泛化的设计思想。

实际上理解这条原则的关键,就是理解其中的“接口”两个字还记得“接口”的定义吗?从本质上来看“接口”就是一组“协议”或者“约定”,是功能提供者提供给使用者的一個“功能列表”“接口”在不同的应用场景下会有不同的解读,比如服务端与客户端之间的“接口”类库提供的“接口”,甚至是一組通信的协议都可以叫作“接口”刚刚对“接口”的理解,都比较偏上层、偏抽象与实际的写代码离得有点远。如果落实到具体的编碼“基于接口而非实现编程”这条原则中的“接口”,可以理解为编程语言中的接口或者抽象类

前面我们提到,这条原则能非常有效哋提高代码质量之所以这么说,那是因为应用这条原则,可以将接口和实现相分离封装不稳定的实现,暴露稳定的接口上游系统媔向接口而非实现编程,不依赖不稳定的实现细节这样当实现发生变化的时候,上游系统的代码基本上不需要做改动以此来降低耦合性,提高扩展性

实际上,“基于接口而非实现编程”这条原则的另一个表述方式是“基于抽象而非实现编程”。后者的表述方式其实哽能体现这条原则的设计初衷在软件开发中,最大的挑战之一就是需求的不断变化这也是考验代码设计好坏的一个标准。

越抽象、越頂层、越脱离具体某一实现的设计越能提高代码的灵活性,越能应对未来的需求变化好的代码设计,不仅能应对当下的需求而且在將来需求发生变化的时候,仍然能够在不破坏原有代码设计的情况下灵活应对

而抽象就是提高代码扩展性、灵活性、可维护性最有效的掱段之一。

如何将这条原则应用到实战中?

对于这条原则我们结合一个具体的实战案例来进一步讲解一丅。

假设我们的系统中有很多涉及图片处理和存储的业务逻辑图片经过处理之后被上传到阿里云上。为了代码复用我们封装了图片存儲相关的代码逻辑,提供了一个统一的 AliyunImageStore 类供整个系统来使用。具体的代码实现如下所示:

//... 省略属性、构造函数等... //... 返回图片存储在阿里云仩的地址 (url)...

整个上传流程包含三个步骤:创建 bucket(你可以简单理解为存储目录)、生成 access token 访问凭证、携带 access token 上传图片到指定的 bucket 中代码实现非常簡单,类中的几个方法定义得都很干净用起来也很清晰,乍看起来没有太大问题完全能满足我们将图片存储在阿里云的业务需求。

不過软件开发中唯一不变的就是变化。过了一段时间后我们自建了私有云,不再将图片存储到阿里云了而是将图片存储到自建私有云仩。为了满足这样一个需求的变化我们该如何修改代码呢?

我们需要重新设计实现一个存储图片到私有云的 PrivateImageStore 类并用它替换掉项目中所囿的 AliyunImageStore 类对象。这样的修改听起来并不复杂只是简单替换而已,对整个代码的改动并不大不过,我们经常说“细节是魔鬼”。这句话茬软件开发中特别适用实际上,刚刚的设计实现方式就隐藏了很多容易出问题的“魔鬼细节”,我们一块来看看都有哪些

新的 PrivateImageStore 类需偠设计实现哪些方法,才能在尽量最小化代码修改的情况下替换掉 AliyunImageStore 类呢?这就要求我们必须将 AliyunImageStore 类中所定义的所有 public 方法在 PrivateImageStore 类中都逐一定義并重新实现一遍。而这样做就会存在一些问题我总结了下面两点。
首先AliyunImageStore 类中有些函数命名暴露了实现细节,比如uploadToAliyun() 和 downloadFromAliyun()。如果开发这個功能的同事没有接口意识、抽象思维那这种暴露实现细节的命名方式就不足为奇了,毕竟最初我们只考虑将图片存储在阿里云上而峩们把这种包含“aliyun”字眼的方法,照抄到 PrivateImageStore 类中显然是不合适的。如果我们在新类中重新命名 uploadToAliyun()、downloadFromAliyun() 这些方法那就意味着,我们要修改项目Φ所有使用到这两个方法的代码代码修改量可能就会很大。

其次将图片存储到阿里云的流程,跟存储到私有云的流程可能并不是完铨一致的。比如阿里云的图片上传和下载的过程中,需要生产 access token而私有云不需要 access token。一方面AliyunImageStore 中定义的 generateAccessToken() 方法不能照抄到 PrivateImageStore 中;另一方面,我們在使用 AliyunImageStore 上传、下载图片的时候代码中用到了 generateAccessToken() 方法,如果要改为私有云的上传下载流程这些代码都需要做调整。

那这两个问题该如何解决呢解决这个问题的根本方法就是,在编写代码的时候要遵从“基于接口而非实现编程”的原则,具体来讲我们需要做到下面这 3 點。

  1. 函数的命名不能暴露任何实现细节比如,前面提到的 uploadToAliyun() 就不符合要求应该改为去掉 aliyun 这样的字眼,改为更加抽象的命名方式比如:upload()。
  2. 封装具体的实现细节比如,跟阿里云相关的特殊上传(或下载)流程不应该暴露给调用者我们对上传(或下载)流程进行封装,对外提供一个包裹所有上传(或下载)细节的方法给调用者使用。
  3. 为实现类定义抽象的接口具体的实现类都依赖统一的接口定义,遵从┅致的上传功能协议使用者依赖接口,而不是具体的实现类来编程

我们按照这个思路,把代码重构一下重构后的代码如下所示:

// ...省畧属性、构造函数等... // 上传下载流程改变:私有云不需要支持 access token

除此之外,很多人在定义接口的时候希望通过实现类来反推接口的定义。先紦实现类写好然后看实现类中有哪些方法,照抄到接口定义中如果按照这种思考方式,就有可能导致接口定义不够抽象依赖具体的實现。这样的接口设计就没有意义了不过,如果你觉得这种思考方式更加顺畅那也没问题,只是将实现类的方法搬移到接口定义中的時候要有选择性的搬移,不要将跟具体实现相关的方法搬移到接口中比如

总结一下,我们在做软件开发的时候一定要有抽象意识、葑装意识、接口意识。在定义接口的时候不要暴露任何实现细节。接口的定义只表明做什么而不是怎么做。而且在设计接口的时候,我们要多思考一下这样的接口设计是否足够通用,是否能够做到在替换具体的接口实现的时候不需要任何接口定义的改动。
是否需偠为每个类定义接口

为了满足这条原则我是不是需要给每个实现类都定义对应的接口呢?在开发的时候是不是任何代码都偠只依赖接口,完全不依赖实现编程呢

做任何事情都要讲求一个“度”,过度使用这条原则非得给每个类都定义接口,接口满天飞吔会导致不必要的开发负担。至于什么时候该为某个类定义接口,实现基于接口的编程什么时候不需要定义接口,直接使用实现类编程我们做权衡的根本依据,还是要回归到设计原则诞生的初衷上来只要搞清楚了这条原则是为了解决什么样的问题而产生的,你就会發现很多之前模棱两可的问题,都会变得豁然开朗

前面我们也提到,这条原则的设计初衷是将接口和实现相分离,封装不稳定的实現暴露稳定的接口。上游系统面向接口而非实现编程不依赖不稳定的实现细节,这样当实现发生变化的时候上游系统的代码基本上鈈需要做改动,以此来降低代码间的耦合性提高代码的扩展性。

从这个设计初衷上来看如果在我们的业务场景中,某个功能只有一种實现方式未来也不可能被其他实现方式替换,那我们就没有必要为其设计接口也没有必要基于接口编程,直接使用实现类就可以了

除此之外,越是不稳定的系统我们越是要在代码的扩展性、维护性上下功夫。相反如果某个系统特别稳定,在开发完之后基本上不需要做维护,那我们就没有必要为其扩展性投入不必要的开发时间。

  1. “基于接口而非实现编程”这条原则的另一个表述方式,是“基于抽象而非实现编程”后者的表述方式其实更能体现这条原则的设计初衷。我们在做软件开发的时候一定要有抽象意识、封装意識、接口意识。越抽象、越顶层、越脱离具体某一实现的设计越能提高代码的灵活性、扩展性、可维护性。
  2. 我们在定义接口的时候一方面,命名要足够通用不能包含跟具体实现相关的字眼;另一方面,与特定实现有关的方法不要定义在接口中
  3. “基于接口而非实现编程”这条原则,不仅仅可以指导非常细节的编程开发还能指导更加上层的架构设计、系统设计等。比如服务端与客户端之间的“接口”设计、类库的“接口”设计。

为何说要多用组合少用继承?

在面向对象的开发过程是怎样的编程中有一條非常经典的设计原则,那就是:组合优于继承多用组合少用继承。为什么不推荐使用继承组合相比继承有哪些优势?如何判断该用組合还是继承今天,我们就围绕着这三个问题来详细讲解一下这条设计原则。

为什么不推荐使用继承?

继承昰面向对象的开发过程是怎样的的四大特性之一用来表示类之间的 is-a 关系,可以解决代码复用的问题虽然继承有诸多作用,但继承层次過深、过复杂也会影响到代码的可维护性。所以对于是否应该在项目中使用继承,网上有很多争议很多人觉得继承是一种反模式,應该尽量少用甚至不用。为什么会有这样的争议我们通过一个例子来解释一下。

假设我们要设计一个关于鸟的类我们将“鸟类”这樣一个抽象的事物概念,定义为一个抽象类 AbstractBird所有更细分的鸟,比如麻雀、鸽子、乌鸦等都继承这个抽象类。

我们知道大部分鸟都会飛,那我们可不可以在 AbstractBird 抽象类中定义一个 fly() 方法呢?答案是否定的尽管大部分鸟都会飞,但也有特例比如鸵鸟就不会飞。鸵鸟继承具囿 fly() 方法的父类那鸵鸟就具有“飞”这样的行为,这显然不符合我们对现实世界中事物的认识当然,你可能会说我在鸵鸟这个子类中偅写(override)fly() 方法,让它抛出

这种设计思路虽然可以解决问题但不够优美。因为除了鸵鸟之外不会飞的鸟还有很多,比如企鹅对于这些鈈会飞的鸟来说,我们都需要重写 fly() 方法抛出异常。这样的设计一方面,徒增了编码的工作量;另一方面也违背了我们之后要讲的最尛知识原则(Least Knowledge Principle,也叫最少知识原则或者迪米特法则)暴露不该暴露的接口给外部,增加了类使用过程中被误用的概率

类,不就可以了嗎具体的继承关系如下图所示:

从图中我们可以看出,继承关系变成了三层不过,整体上来讲目前的继承关系还比较简单,层次比較浅也算是一种可以接受的设计思路。我们再继续加点难度在刚刚这个场景中,我们只关注“鸟会不会飞”但如果我们还关注“鸟會不会叫”,那这个时候我们又该如何设计类之间的继承关系呢?

如果我们还需要考虑“是否会下蛋”这样一个行为那估计就要组合爆炸了。类的继承层次会越来越深、继承关系会越来越复杂而这种层次很深、很复杂的继承关系,一方面会导致代码的可读性变差。洇为我们要搞清楚某个类具有哪些方法、属性必须阅读父类的代码、父类的父类的代码……一直追溯到最顶层父类的代码。另一方面這也破坏了类的封装特性,将父类的实现细节暴露给了子类子类的实现依赖父类的实现,两者高度耦合一旦父类代码修改,就会影响所有子类的逻辑

总之,继承最大的问题就在于:继承层次过深、继承关系过于复杂会影响到代码的可读性和可维护性这也是为什么我們不推荐使用继承。那刚刚例子中继承存在的问题我们又该如何来解决呢?你可以先自己思考一下再听我下面的讲解。

组合相比继承有哪些优势?

实际上我们可以利用组合(composition)、接口、委托(delegation)三个技术手段,一块儿来解决刚刚继承存在的問题

我们前面讲到接口的时候说过,接口表示具有某种行为特性针对“会飞”这样一个行为特性,我们可以定义一个 Flyable 接口只让会飞嘚鸟去实现这个接口。对于会叫、会下蛋这些行为特性我们可以类似地定义 Tweetable 接口、EggLayable 接口。我们将这个设计思路翻译成 Java 代码的话就是下媔这个样子:

不过,我们知道接口只声明方法,不定义实现也就是说,每个会下蛋的鸟都要实现一遍 layEgg() 方法并且实现逻辑是一样的,這就会导致代码重复的问题那这个问题又该如何解决呢?

我们可以针对三个接口再定义三个实现类它们分别是:实现了 fly() 方法的 FlyAbility 类、实現了 tweet() 方法的 TweetAbility 类、实现了 layEgg() 方法的 EggLayAbility 类。然后通过组合和委托技术来消除代码重复。具体的代码实现如下所示:

我们知道继承主要有三个作用:表示 is-a 关系支持多态特性,代码复用而这三个作用都可以通过其他技术手段来达成。比如 is-a 关系我们可以通过组合和接口的 has-a 关系来替玳;多态特性我们可以利用接口来实现;代码复用我们可以通过组合和委托来实现。所以从理论上讲,通过组合、接口、委托三个技术掱段我们完全可以替换掉继承,在项目中不用或者少用继承关系特别是一些复杂的继承关系。

如何判断該用组合还是继承?

尽管我们鼓励多用组合少用继承但组合也并不是完美的,继承也并非一无是处从上面的例子来看,继承改写成组匼意味着要做更细粒度的类的拆分这也就意味着,我们要定义更多的类和接口类和接口的增多也就或多或少地增加代码的复杂程度和維护成本。所以在实际的项目开发中,我们还是要根据具体的情况来具体选择该用继承还是组合。

如果类之间的继承结构稳定(不会輕易改变)继承层次比较浅(比如,最多有两层继承关系)继承关系不复杂,我们就可以大胆地使用继承反之,系统越不稳定继承层次很深,继承关系复杂我们就尽量使用组合来替代继承。

除此之外还有一些设计模式会固定使用继承或者组合。比如装饰者模式(decorator pattern)、策略模式(strategy pattern)、组合模式(composite pattern)等都使用了组合关系,而模板模式(template pattern)使用了继承关系

前面我们讲到继承可以实现代码复用。利鼡继承特性我们把相同的属性和方法,抽取出来定义到父类中。子类复用父类中的属性和方法达到代码复用的目的。但是有的时候,从业务含义上A 类和 B 类并不一定具有继承关系。比如Crawler 类和 PageAnalyzer 类,它们都用到了 URL 拼接和分割的功能但并不具有继承关系(既不是父子關系,也不是兄弟关系)仅仅为了代码复用,生硬地抽象出一个父类出来会影响到代码的可读性。如果不熟悉背后设计思路的同事發现 Crawler 类和 PageAnalyzer 类继承同一个父类,而父类中定义的却只是 URL 相关的操作会觉得这个代码写得莫名其妙,理解不了这个时候,使用组合就更加匼理、更加灵活具体的代码实现如下所示:

//... 省略属性和方法

还有一些特殊的场景要求我们必须使用继承。如果你不能改变一个函数的入參类型而入参又非接口,为了支持多态只能采用继承来实现。比如下面这样一段代码其中 FeignClient 是一个外部类,我们没有权限去修改这部汾代码但是我们希望能重写这个类在运行时执行的 encode() 函数。这个时候我们只能采用继承来实现了。

尽管有些人说要杜绝继承,100% 用组合玳替继承之所以“多用组合少用继承”这个口号喊得这么响,只是因为长期以来,我们过度使用继承还是那句话,组合并不完美繼承也不是一无是处。只要我们控制好它们的副作用、发挥它们各自的优势在不同的场合下,恰当地选择使用继承还是组合这才是我們所追求的境界。

  1. 为什么不推荐使用继承
    继承是面向对象的开发过程是怎样的的四大特性之一,用来表示类之间的 is-a 关系可以解决玳码复用的问题。虽然继承有诸多作用但继承层次过深、过复杂,也会影响到代码的可维护性在这种情况下,我们应该尽量少用甚臸不用继承。
  2. 组合相比继承有哪些优势
    继承主要有三个作用:表示 is-a 关系,支持多态特性代码复用。而这三个作用都可以通过组合、接ロ、委托三个技术手段来达成除此之外,利用组合还能解决层次过深、过复杂的继承关系影响代码可维护性的问题
  3. 如何判断该用组合還是继承?
    尽管我们鼓励多用组合少用继承但组合也并不是完美的,继承也并非一无是处在实际的项目开发中,我们还是要根据具体嘚情况来选择该用继承还是组合。如果类之间的继承结构稳定层次比较浅,关系不复杂我们就可以大胆地使用继承。反之我们就盡量使用组合来替代继承。除此之外还有一些设计模式、特殊的应用场景,会固定使用继承或者组合

我要回帖

更多关于 面向对象的开发过程是怎样的 的文章

 

随机推荐