系列文章目录
提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加
例如:第一章 Python 设计模式原则
提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档
设计模式
1. 概述
设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。它描述了在软件设计过程中的一些不断重复发生的问题,以及该问题的解决方案。也就是说,它是解决特定问题的一系列套路,是前辈们的代码设计经验的总结,具有一定的普遍性,可以反复使用。
1.1 起源
1995 年,艾瑞克·伽马(ErichGamma)、理査德·海尔姆(Richard Helm)、拉尔夫·约翰森(Ralph Johnson)、约翰·威利斯迪斯(John Vlissides)等 4 位作者合作出版了 《设计模式:可复用面向对象软件的基础》 一书,在此书中收录了 23 个设计模式,这是设计模式领域里程碑的事件,导致了软件设计模式的突破。
1.2 设计模式四要素
设计模式有四个基本要素:
- 模式名称(pattern name):一个助记名,它用一两个词来描述模式的问题、解决方案和效果。
- 问题(problem):描述了应该在何时使用模式。
- 解决方案(solution):描述了设计的组成成分,它们之间的相关关系以及各自的职责和协作方案。
- 效果(consequences):描述了模式应用的效果以及使用模式应该权衡的问题。
1.3 设计模式分类
创建型模式(5个):与对象的创建有关,将对象的创建与使用分离。
单例、原型、工厂方法、抽象工厂、建造者
结构型模式(7个):与对象的组装有关,将类或对象按某种布局组成更大的结构。
代理、适配器、桥接、装饰、外观、享元、组合
行为模式(11个):类或对象之间的沟通协调有关,类或对象之间怎样相互协作共同完成单个对象无法单独完成的任务,以及怎样分配职责。
模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器
1.4 Python中的设计模式特点
Python支持动态类型(duck-typing),函数是一等
公民,并且一些模式(例如,迭代器和修饰器)是内置特性。
2 面向对象
2.1 面向对象设计的基本原则
- 针对接口编程,而不是针对实现编程
- 编程时针对超类型进行编程,而不是具体某个子类。
- 超类型中各个方法的具体实现不在超类型中,而在各个子类中。
- 超类型通常是一个接口或者一个抽象类。
- 优先使用对象组合,而不是类继承。
- 用聚合/组合复用,去代替继承复用。可以避免继承所带的方法污染问题。
- 把一些特征和行为抽取出来,形成工具类。
- 然后通过聚合/组合成为当前类的属性。
- 再调用其中的属性和行为达到代码重用的目的。
class Plane:
def fly():
print("fly in the sky")
class Ship:
def swim():
print("swim in the sea")
class ManKind:
p = Plane()
s = Ship()
def fly():
p.fly()
def swim():
s.swim()
- 封装变化,将不变的与变化的内容分开
- 找出应用中可能需要的变化之处,把他们独立出来(封装),不要和哪些不需要变化的代码混在一起。
- 我们实现一个鸭子,且这个鸭子有很多种,且有各个属性。我们应该如何设计这个鸭子呢?
- 首先鸭子不变的属性有哪些? 外观,游泳。等(先定义这两个)。
- 鸭子变的属性有哪些? 有的会叫,有的会飞等。
3 UML图
3.1 面向对象分析设计工具
统一建模语言 (Unified Modeling Language,UML) 是用来设计软件的可视化建模语言。
3.2 UML 2.2中一共定义了14种图示
结构性图形(Structure diagrams) 强调的是系统式的建模:
- 静态图(static diagram)
- 类图
- 对象图
- 包图
- 实现图(implementation diagram)
- 组件图
- 部署图
- 剖面图
- 复合结构图
行为式图形(Behavior diagrams)强调系统模型中触发的事件: - 活动图
- 状态图
- 用例图
交互性图形(Interaction diagrams),属于行为图形的子集合,强调系统模型中的资料流程: - 通信图
- 交互概述图(UML 2.0)
- 时序图(UML 2.0)
- 时间图(UML 2.0)
3.3 UML中的一些概念
UML从来源中使用相当多的概念。下面仅列代表性的概念。
对于结构而言
- 执行者,属性,类,组件,接口,对象,包。
对于行为而言
- 活动,事件,消息,方法,状态,用例。
对于关系而言
- 聚合,关联,组合,相依,广义化(或继承)。
其他概念
- 构造型—这规范符号应用到的模型
- 多重性—多重性标记法与资料库建模基数对应,例如:1, 0…1, 1…*
- 角色
3.4 类图表示
@startuml
class Flight {
flightNumber : String
departureTime : DateTime
check_cancel()
}
@enduml
3.5 类与类的关系
依赖
@startuml
Class1 …> Class2
@enduml
关联
@startuml
Class1 --> Class2
@enduml
泛化
@startuml
Class1 --|> Class2
@enduml
实现
@startuml
Class1 …|> Class2
@enduml
4 设计原则-SOLID原则
原则不是具体描述操作步骤和细节,而是给出抽象的要求。用原则来判断我们的设计是否满足要求。
4.1 SOLID 原则
什么是 SOLID原则:
- S 一 单一职责原则,
- O 一 开放封闭原则,
- L 一 里氏替换原则,
- I 一 接口隔离原则,
- D 一 依赖倒置原则
谁提出来的,背景是什么《敏捷软件开发:原则、模式与实践》
SOLID原则有什么用? - 指导程序员开发出易于维护和扩展的软件。
SOLID的发展史
这几个原则之间有没有先后顺序?
4.2 单一职责原则
- 什么是职责
功能,逻辑 等。 - 使用动机(why)
若不遵守单一职责原则,即一个类有一个以上的职责,则当一个职责发生变化时,可能会影响其他职责,从而影响代码的维护。 - 如何使用(how)
核心在于职责的分解。不同的职责分开到不同的类的实现中去。 - 使用原则
- 每一个类实现的职责有清晰明确的定义。
- 一个类的修改只对自身有影响,对其他类没有影响。
- 使用示例
案例:动物奔跑和游泳拆分成两个类
4.3 开放封闭原则
- 什么是开闭
- 软件实体(类、模块、函数等)应该可以扩展,但是不可以修改。
- 即对于扩展是开放的,对于更改是封闭的。
- 通俗来说就是对于要增加的新功能或要调整的改动,尽量扩展新代码而不是修改已有代码。
- 使用动机(why)
代码面对需求改变可以保持相对稳定 - 如何使用(how)
创建抽象,隔离。 - 使用原则
- 仅对程序中呈现出频繁变化的部分做出抽象。
- 不要刻意对每个部分进行抽象,拒绝不成熟的抽象,它和抽象本身一样重要。
- 使用示例
- 计算机算子类:包括±*/;解析器类:括号解析,复数解析
增加功能时,不去改动已有的代码。当修改一个模块时,不影响其他模块。
案例:动物园展示不同动物的活动方式displayActivity。最初是通过if else判断动物的类型来展示活动方式(running,swimming)。根据开闭原则的要求,创建一个动物抽象类,抽象出moving操作,不同的动物继承抽象类,重写moving方法。displayActivity的时候,只需要调用moving方法即可。
4.4 里氏替换原则
- 什么是里氏替换
在软件里面,把父类都替换成它的子类,程序的行为没有变化。简单来说,子类型必须能够替换掉它们的父类型。 - 使用动机(why)
确保父类能够真正复用(继承),子类也能够在父类的基础上增加新的行为。 - 如何使用(how)
- 父类一般使用抽象类或接口。
- 抽象类定义公共对象和状态;接口定义公共行为。
- 子类通过继承父类和接口进行扩展。
- 使用原则
- 子类方法的参数类型必须与父类相匹配或更抽象。
- 子类的返回值类型必须与父类或其子类相匹配。
- 子类方法的异常必须与父类能抛出的异常(或其子类)相匹配。
- 子类不应该加强参数条件限制。
- 子类不能修改父类的私有成员变量。
- 使用示例
企鹅继承鸟,但是企鹅不会飞,违反了里氏替换原则。合理的做法是将飞翔的行为抽象为接口,父类鸟描述状态和公共方法(比如吃),然后会飞的子类再去实现飞翔接口,不会飞的就不用管了。
只要父类能出现的地方子类就能出现。
案例:猴子既会跑又会爬树,猴子继承陆生动物,增加新方法climbing,动物园为猴子增加一个新方法。这个新方法只有猴子类能出现。其父类不能出现。
4.5 依赖倒置原则
- 什么是依赖倒置
- 程序不应该依赖细节,细节应该依赖于抽象。
- 简单来说,就是要针对接口编程,不要针对实现编程。
- 使用动机(why)
面对不同的具体实现做到易拔插,松耦合。 - 如何使用(how)
- 接口或抽象类制定规范,不涉及任何具体的操作。
- 实现类完成展现细节的任务。
- 程序中的所有依赖关系都终止于抽象类或接口。
- 使用原则
- 高层模块不应该依赖低层模块,两个都应该依赖抽象。
- 抽象不应该依赖细节,细节应该依赖抽象。
- 使用示例
计算机接口的统一
把具有相同特征或相似功能的类,抽象成接口或抽象类。让具体的实现类继承这个抽象类,实现对应的接口。抽象类负责定义统一的方法,实现类负责具体功能的实现。
案例:动物吃食物。如果每个动物吃的事物都写成具体的品种(肉,草,虫子)那么就有有两个问题:
- 每种动物都需要为其定义一个食物类,高度依赖于细节
- 每种动物只能吃一种食物,不符合现实。
这时候就需要遵循依赖倒置原则,来对食物类重新设计,抽象出一个基类,在根据基类创建实现类。
4.6 接口隔离原则
-
什么是接口隔离原则
-
使用动机(why)
-
如何使用(how)
-
使用原则
-
使用示例
建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。为不同的类别的类建立专用接口,而不要试图建立一个很庞大的接口供所有依赖它的类调用。
案例,有的动物可以有多种行为,例如蝙蝠会飞,天鹅会飞会游泳会奔跑。因此将行为抽象成接口。
4.7 开闭原则是总纲
开放封闭原则告诉我们要对扩展开放,对修改封闭。开闭原则是整个设计的最终目标和原则。开闭原则是总纲,其他四个原则是对这个原则的具体解释。
4.8 更为简单实用的设计原则
不是所有的软件都要求符合SOLID原则,下面给出一些更为简单实用的原则。
4.9 LoD原则(迪米特原则)
- 什么是LoD原则
如果两个类不必彼此互相通信,那么这两个类就不应当发生直接的相互作用;如果其中一个类需要调用另一个类的某一个方法,可以通过第三者转发这个调用。 - 使用动机(why)
强调类之间的松耦合。类之间的耦合越弱,越有利于复用和扩展。 - 如何使用(how)
- 在类的结构设计上,每一个类都应当尽量降低成员的访问权限,不需要让别的类知道的字段或行为就不要公开。
- 类之间不直接建立联系,通过中间类来中转。
- 使用原则
- 减少公开方法和变量。
- 每个类对其他类知道的越少越好。
- 类不应该知道它所操作的对象的内部细节。
- 使用示例
跨部门办事(类和类通信),例如找运维部门修电脑,最好的做法是通过运维部门的问题反馈入口,通过入口来获取帮助。至于运维部门内部如何运作来给你帮助,由它内部解决,不影响外部使用。
一个类只需要和直接的对象进行交互,而不用濑户这个对象内部组成。
例如:a.getB().getC().getProperties()
就不是一个好的写法,而应该是
a.getCProperties()
至于getCPorperties怎么实现是类A要负责的事情。
4.10 KISS原则
保持简单和愚蠢
ABCD监听过度的反面例子
4.11 DRY
不要重复自己
- 函数级别的封装,通用函数
- 类级别的抽象,抽象出一个基类
- 泛型设计,使用装饰器来消除冗余的代码
4.12 YAGNI原则
只考虑和设计必须的功能,避免过度设计。尽快尽可能简单地让软件运行起来。
4.13 Rule of three
事不过三原则
即某个功能出现三次时,就需要进行抽象化。
4.14 CQS原则
查询命令分离原则
查询(Query):当一个方法返回一个值来回应一个问题的时候,它就具有查询的性质。
命令(Command):当一个方法要改变对象的状态的时候,它就具有命令的性质。
5 设计模式的本质
5.1 模式是针对实践中的问题而产生的
软件开发技术的学习都应该以实践为前提,只有理解实践过程中遇到的种种问题,才能明白那些技术的本质和目的是什么,因为每种新技术都是因某个/某些问题而出现的。
每个模式都描述了一个在我们的环境中不断出现的问题,并描述了该问题的解决方案的核心。通过这种方式,可以无数次地使用那些已有的解决方案,无须在重复相同的工作。
设计模式是在某种情境下,针对某种问题的典型,通用的解决方案。
- 情境:在特定情境下反复出现的情况,要求使用模式必须分清楚事实
- 问题:一般来说,就是你要实现的目标或者要解决的差距
- 解决方案:典型的、通用的、举一反三
学习设计模式,首先弄清楚软件开发过程中的问题在哪里,这些问题会有什么影响。这样才能使其然更知其所以然。
软件开发过程中的问题:
- 业务的复杂度
- 实现的复杂度
- 实现的易理解性,可维护性,可扩展性。
使用恰当的设计模式是应对软件复杂度的有效方法。
抽象是指不依赖于具体事物或实例,而是指抽取共同点或基本特征的思维过程。
设计模式是高层次的抽象方案,并不关注具体的实现细节,比如算法和数据结构。
5.2 模式的范围
模式包括架构模式、设计模式、编码模式、语言惯例。
设计模式的联合使用
- MCV 模式就是设计模式联合优化的一种模式。严格来说MCV属于架构模式。
OOA/D Object Oriented Analysis and Design 面向对象分析和设计
GRASP原则或模式,是对大量模式的归纳和总结,理解复杂现象背后的本质思想。
6 设计模式的误区
第一个误区是必须先有设计模式。设计模式是为了更好的组织代码的结构。不要为了强迫自己使用已有的设计模式而限制了你的创造力。
第二个误区是设计模式应随处使用。过度使用反而增加了代码的复杂程度。