什么是抽象
编程的目的是为了用计算机程序解决现实中的问题,既然要将现实的问题转换为计算机程序世界的问题,首先就需要对现实世界进行抽象,这个过程也就是上周所讲的建立模型。
抽象即从现实世界具体的问题域中提取出需要解决的关注点,以及各种概念之间的共性和关系,这样有助于我们用模型来描述解决方案,同时也有助于提升模型的可靠性和可复用性。
面向对象编程
编程语言的发展过程是从具体、底层的控制指令逐步进化为抽象、高层的自然语言。在这个过程中,面向对象编程思想的诞生可以说是编程语言发展史上的分水岭。在此之前,结构化的编程思想是按照解决问题的具体步骤来进行编排的,其核心在于处理逻辑,而面向对象则是直接抽取问题域中的概念和实体——即对象——以对象作为程序的组织单元,并将处理逻辑划分到各个对象中。
对象的三个要素分别是状态、行为、标识,状态即是对象所持有的属性值,它伴随着对象的生命周期,是对象之所以富有表达能力的关键;行为是对象具有的功能,一部分行为可以更改对象的状态,其他的行为可以用于读取状态、跟其他对象进行互通等等;而标识则是对象的唯一身份,用于区别某个对象与其他的对象。
面向对象编程中,我们用类作为对象的定义模版,通过类生成一个对象的过程叫做实例化,一个类可以生成任意多个对象。
面向对象编程的三大特性
封装
封装是创建对象的基础,它将某些具体的属性和行为包裹在对象内,一定程度上隐藏实现的细节,而只暴露少量需要被外界直接用到的接口出来,这些接口可以完成对象所具有的功能,而我们不必去关心接口背后是如何实现的。同时,封装也为代码提供了访问控制,避免关键逻辑直接暴露出来,防止了意外修改和错误调用对对象带来的危害。
继承
继承是面向对象三大特性中最符合人类认知的一项,因为人类认识世界所使用的方法正是对事物进行分类、归纳,相似的事物归为一类,它们拥有某些共性,而每个事物又有自己专有的特性。在面向对象编程中,我们将共性提取出来作为父类,而将特性写在由父类派生出的各个子类中,这样一来,各个子类均可以直接享有父类的共性,而无需重复实现,同时每个子类又还可以拥有自己的特性。甚至面向对象编程还支持对来自父类的共性进行修改覆盖。
多态
多态是真正体现面向对象编程思想的特性,它指的是子类实现父类或接口的抽象方法之后,调用方无需关心具体的子类,而只要根据抽象方法来编写调用, 运行时给定不同的子类,就可以表现出不同的形态。对于调用方来说,由于这些子类所使用的方法名和参数列表都是相同的,因此面对不同的子类,调用方完全不用做出任何更改,甚至调用方不用知道具体有哪些子类,它只要按照抽象方法的声明来调用就行了。
面向对象的三大特性其实是一脉相承的,先有封装,在封装的基础上才能实现继承,而有了来自于继承机制的基类和子类,才能实现多态。无论封装还是继承还是多态,其背后的思想精髓都是“抽象”。
代码的坏味道
面向对象编程思想最终的目的是为了实现高内聚、低耦合的程序,即相关的对象和逻辑尽可能封装在一起,而弱关联的、不同职责的部分则应当划分到不同的对象中,避免一段代码包含过多的内容,避免代码之间的调用关系过于复杂胶着。
Martin Fowler和Bob大叔都在其著作中提到了“代码的坏味道”,即不符合“高内聚、低耦合”、不符合面向对象思想的代码编写方式。
image.png
为了避免编写出“坏味道”的代码,前人经过总结得出了面向对象编程的几大设计原则,并进一步总结出了三大类、23种设计模式。无论设计原则还是设计模式,本身都是跟语言无关的,设计原则是编写代码的通用最佳实践,而设计模式是针对特定某类问题的通用解决方案。设计模式最终的目的是为了满足设计原则。
SOLID原则
SOLID是面向对象五大原则的缩写,以下对每个原则进行简介。
开放、封闭原则
代码对扩展开放,对修改关闭
它指的是我们在应对需求变化,需要变更代码的行为时,应当采用扩展的形式,而不是直接修改已有的代码。通过合适的抽象和分层,我们可以将变化用新增的对象来处理,在设计模式中,策略模式、适配器模式、观察者模式都可以用来实现这一原则。
依赖倒置原则
高层模块不依赖底层模块,两者都依赖抽象;抽象不依赖具体,具体依赖抽象
这一原则的表述乍一看很难理解,但通过两张图就可以很好地表达其思想:
image.png
这张图中,Button直接调用Lamp,因此说Button是依赖于Lamp的,一旦Lamp发生变化,Button也得跟着变,两者之间形成了紧耦合,并且这个Button只能用于Lamp,即便是用于其他的控制,由于它直接依赖Lamp,还必须把Lamp给带上。
image.png
而在遵循依赖倒置原则的设计中,我们将Button真正对外产生的影响抽象成一个接口,它提供的功能无非就是开和关,任意能够用开和关来控制的对象都可以由这个Button来控制。因此,只要Lamp实现了这一接口,它就可以被Button控制,而Button本身只需依赖接口,而无需再直接依赖Lamp。
依赖倒置原则是编写框架的关键,因为框架的作用就是提供一种通用的架构模式和工具集,简化应用程序开发的过程,确保应用程序代码结构的稳固,因此框架本身应当是越通用越好,扩展性越强越好,这正是依赖倒置原则的用武之地。
里氏替换原则
所有使用父类的地方都可以用其子类代替
这是一个很容易被忽略的原则,但真正按照这一原则来指导类的继承,会发现很多我们日常所认为的父子类关系其实是不成立的,例如,马可以用来骑,白马和黑马都是其子类,自然也可以用来骑,但是小马不能用来骑,因此不应作为马的子类。这样看上去似乎有些违背逻辑,但编程真正关注的是对象的行为,而非对象的身份,在Python中甚至有“鸭子类型”这一十分有意思的观点,因此,如何划分父子类真正的依据应该是对象的行为。
单一职责原则
一个类只能有一个引起它变化的原因
所谓职责,就是引起变化的原因,单一职责原则避免了一个类过多的依赖,从而导致其本身的进化和复用困难重重,所有的分层架构都是在为实现单一职责提供帮助,因为不同关注点的发展方向、变化频率和目标都是不同的。但是在实践中,要完美符合单一职责原则并不是那么容易,例如MVC中的Controller作为Model和View之间的粘合层,往往就包含了各方面的对接和控制。不过,我们可以尽可能地将代码拆开成多个类,并以组合的形式进行程序构建。
接口隔离原则
不应强迫客户端依赖它不需要的方法
这是指导接口设计的原则,简单来说,这一原则跟单一职责原则的思想类似,提倡使用多个尽可能小的接口,而不是一个接口包含各种各样的方法,该否则当一个方法发生变化的时候,所有接口的使用方都需要跟着改变。
网友评论