面向对象思想

注意:本笔记整理于 极客时间 设计模式之美

面向对象编程思想

现在,主流的编程范式或者是编程风格有三种,它们分别是面向过程面向对象函数式编程

面向对象、设计原则、设计模式、编程规范、重构,这五者有何关系?

关于面向对象、设计原则、设计模式、编程规范和代码重构,这五者的关系:

  1. 面向对象编程因为其具有丰富的特性(封装、抽象、继承、多态),可以实现很多复杂的设计思路,是很多设计原则、设计模式等编码实现的基础。
  2. 设计原则是指导我们代码设计的一些经验总结,对于某些场景下,是否应该应用某种设计模式,具有指导意义。比如,“开闭原则”是很多设计模式(策略、模板等)的指导原则。
  3. 设计模式是针对软件开发中经常遇到的一些设计问题,总结出来的一套解决方案或者设计思路。应用设计模式的主要目的是提高代码的可扩展性。从抽象程度上来讲,设计原则比设计模式更抽象。设计模式更加具体、更加可执行。
  4. 编程规范主要解决的是代码的可读性问题。编码规范相对于设计原则、设计模式,更加具体、更加偏重代码细节、更加能落地。持续的小重构依赖的理论基础主要就是编程规范。
  5. 重构作为保持代码质量不下降的有效手段,利用的就是面向对象、设计原则、设计模式、编码规范这些理论。

面向对象编程

  1. 面向对象编程中有两个非常重要、非常基础的概念,那就是类(class)对象(object)

  2. 面向对象编程因为其具有丰富的特性(封装、抽象、继承、多态

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

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

UML(Unified Model Language)统一建模语言。常用它来画图表达面向对象或设计模式的设计思路。但是,从开发经验来说,UML在互联网公司的项目开发中,用处可能并不大。为了文档化软件设计或者方便讨论软件设计,大部分情况下,我们随手画个不那么规范的草图,能够达意,方便沟通就够了,而完全按照UML规范来将草图标准化,所付出的代价是不值得的。

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

封装是为了保护类中的 成员变量。

抽象是为了让只让使用者知道使用方法,不用官方法的具体细节。

子类复用父类的方法时,这称为 继承。

子类重写父类的方法时,这称为 多态。

封装(Encapsulation)

首先,我们来看封装特性。封装也叫作信息隐藏或者数据访问保护类通过暴露有限的访问接口,授权外部仅能通过类提供的方式(或者叫函数)来访问内部信息或者数据

对于封装这个特性,我们需要编程语言本身提供一定的语法机制来支持。这个语法机制就是访问权限控制。例子中的private、public等关键字就是Java 语言中的访问权限控制语法。private关键字修饰的属性只能类本身访问,可以保护其不被类之外的代码直接访问。

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

如果我们对类中属性的访问不做限制,那任何代码都可以访问、修改类中的属性,虽然这样看起来更加灵活,但从另一方面来说,过度灵活也意味着不可控,属性可以随意被以各种奇葩的方式修改,而且修改逻辑可能散落在代码中的各个角落,势必影响代码的可读性、可维护性。

抽象(Abstraction)

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

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

换一个角度来考虑,我们在定义(或者叫命名)类的方法的时候,也要有抽象思维,不要在方法定义中,暴露太多的实现细节,以保证在某个时间点需要改变方法的实现逻辑的时候,不用去修改其定义

封装是为了保护类中的 成员变量。

抽象是为了让只让使用者知道使用方法,不用官方法的具体细节。

继承(Inheritance)

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

有些编程语言只支持单继承,不支持多重继承,比如Java、PHP、C#、Ruby等,而有些编程语言既支持单重继承,也支持多重继承,比如C++、Python、Perl等。

继承最大的一个好处就是代码复用。

子类继承父类,默认可以直接是使用父类中实现的方法和成员变量。

猫属于哺乳动物,从人类认知的角度上来说,是一种is-a关系。

通过继承来关联两个类,反应真实世界中的这种关系,非常符合人类的认知,而且,从设计的角度来说,也有一种结构美感。

但是,频繁的使用继承,最终的代码也会很复杂,所以有人认为继承是反设计模式的,可以考虑多用组合少用继承

多态(Polymorphism)

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

对于多态特性的实现方式,除了利用“继承加方法重写”这种实现方式之外,我们还有其他两种比较常见的的实现方式,一个是利用接口类语法,另一个是利用duck-typing(比如python中就是鸭子类型) 语法

class Logger: 
    def record(self):
        print("I write a log into file.")
        
class DB: 
    def record(self): 
        print("I insert data into db.")
        
def test(recorder): 
    recorder. record()
    
def demo():
    logger=Logger()
	db=DB()
	test(logger)
	test(db)

从这段代码中,我们发现,duck-typing实现多态的方式非常灵活。

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

多态特性能提高代码的可扩展性和复用性

我们利用多态的特性,仅用一个print)函数就可以实现遍历打印不同类型(Array、LinkedList)集合的数据。

子类复用父类的方法时,这称为 继承。

子类重写父类的方法时,这称为 多态。

面向对象与面向过程 二者的区别

  1. 面向过程编程也是一种编程范式或编程风格。它以过程(可以为理解方法、函数、操作)作为组织代码的基本单元,以数据(可以理解为成员变量、属性)与方法相分离为最主要的特点。面向过程风格是一种流程化的编程风格,通过拼接一组顺序执行的方法来操作数据完成一项功能。
  2. 面向过程编程语言首先是一种编程语言。它最大的特点是不支持类和对象两个语法概念,不支持丰富的面向对象编程特性(比如继承、多态、封装),仅支持面向过程编程。

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

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

类就是一种非常好的组织这些函数和数据结构的方式,是一种将代码模块化的有效手段。

OOP风格的代码 通过 封装、抽象、继承、多态 更易复用、易扩展、易维护

抽象类和接口

并不是所有的面向对象编程语言都支持抽象类和接口这两个语法概念,比如,C++这种编程语言只支持抽象类,不支持接口;而像Python这样的动态编程语言,既不支持抽象类,也不支持接口。

Java这种编程语言,既支持抽象类,也支持接口

抽象类具有哪些特性。总结了下面三点:

  1. 抽象类不允许被实例化,只能被继承。也就是说,你不能new一个抽象类的对象出来(Logger logger=new Logger(…);会报编译错误)。
  2. ·抽象类可以包含属性和方法。方法既可以包含代码实现(比如Logger中的log0方法),也可以不包含代码实现(比如Logger中的doLog0方法)。不包含代码实现的方法叫作抽象方法。
  3. ·子类继承抽象类,必须实现抽象类中的所有抽象方法。对应到例子代码中就是,所有继承Logger抽象类的子类,都必须重写doLog0方法。

接口都有哪些特性。总结了三点:

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

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

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

抽象类不能实例化,只能被继承。继承能解决代码复用的问题。所以,抽象类也是为代码复用而生的。避免在子类中,重复编写相同的代码。

当然仅仅用继承也是可以实现代码的复用,但是抽象类更加优雅。

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

抽象类更多的是为了代码复用,而接口就更侧重于解耦。接口是对行为的一种抽象,调用者只需要关注抽象的接口,不需要了解具体的实现,具体的实现代码对调用者透明。

接口实现了约定和实现相分离,可以降低代码间的耦合性,提高代码的可扩展性。

基于接口而非实现编程

“接口”的定义:从本质上来看,“接口”就是一组“协议”或者“约定”,是功能提供者提供给使用者的一个“功能列表”。

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

实际上,“基于接口而非实现编程”这条原则的另一个表述方式,是“基于抽象而非实现编程”。后者的表述方式其实更能体现这条原则的设计初衷。在软件开发中,最大的挑战之一就是需求的不断变化,这也是考验代码设计好坏的一个标准。越抽象、越顶层、越脱离具体某一实现的设计,越能提高代码的灵活性,越能应对未来的需求变化。好的代码设计,不仅能应对当下的需求,而且在将来需求发生变化的时候,仍然能够在不破坏原有代码设计的情况下灵活应对。而抽象就是提高代码扩展性、灵活性、可维护性最有效的手段之一。

多用组合少用继承

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

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

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值