设计模式
type
Post
status
Published
date
Jul 12, 2023
slug
design-patterns
summary
tags
category
基础Base
icon
password
UML图各种类UML表示具体类UML表示抽象类UML表示接口UML表示包表示关系实现关系 (Implements)泛化关系(Generalization)关联关系(Association)依赖关系(Dependency)聚合关系(Aggregation)组合关系(Composition)Python超简单入门基本语法常用容器类的定义设计模式 ———————创建型设计模式——————工厂模式例子思考简单工厂模式工厂方法模式抽象工厂模式原型模式单例模式例子思考模型抽象应用场景——————结构性设计模式———————*适配器模式例子思考模型抽象应用场景*桥接模式组合模式例子思考模型抽象应用场景*装饰模式例子思考模型抽象应用场景*外观模式例子思考模型抽象应用场景代理模式例子思考模型抽象 ——————行为型设计模式——————职责链模式*命令模式例子思考模型抽象应用场景解释器模式迭代器模式中介者模式备忘录模式*观察者模式例子思考模型抽象应用场景状态模式*策略模式例子思考模型抽象应用场景*桥接模式模板模式例子思考模型抽象应用场景访问者模式面向对象设计原则ISSUE
UML图
各种类
UML表示具体类
如图所示,矩形框分为三层:
第一层 :类名字。
第二层 :成员变量。其中,+ 表示public,- 表示private,# 表示protected;不带符号则表示default。
第三层 :方法。
UML表示抽象类
如图所示,同样用矩形框表示,但是抽象类的类名以及抽象方法都用斜体表示。
UML表示接口
如图所示,接口与类的表示不用的是,接口在第一层顶端用构造行<<interface>>,下面是接口的名字,第二层是方法。
还有一种叫棒棒糖表示法,展示如下图,就是类上面的一根棒棒糖,圆圈旁为接口名称,接口方法在实现类中出现。
UML表示包
表示方式如上图。两个矩形框,下方大的矩形框内指明包的名字。
表示关系
类和类,类和接口,接口和接口之间存在一定关系,主要有以下六种类型的关系:
实现、泛化、关联、依赖、聚合、组合
如图所示。
实现关系 (Implements)
指接口及其实现类之间的关系。实现关系用空心三角和虚线组成的箭头表示,从实现类指向接口。
泛化关系(Generalization)
指对象与对象之间的继承关系。泛化关系用空心三角形和实线组成的箭头表示,从子类指向父类。
关联关系(Association)
指对象和对象之间的连接。双向关联关系用带双箭头的实现或无箭头的实线双线表示;单向关联用一个带箭头的实线表示,箭头指向被关联的对象。
一个对象可以持有其它对象的数组或者集合,还可以通过放置多重性(multiplicity)表达式在关联线的末端来表示。如下表达式示例:
数字:精确的数字。
*
或者 0..*
:表示0到多个。
0..1
:表示0或者1个。
1..*
:表示1到多个。依赖关系(Dependency)
是一种弱关联关系。依赖关系用一个带虚线的箭头表示,由使用方指向被使用方,表示使用方对象持有被使用方对象的引用。
依赖关系在Java中的具体代码表现为B为A的构造器或方法中的局部变量、方法或构造器的参数、方法的返回值,或者A调用B的静态方法。
聚合关系(Aggregation)
体现的是整体和部分的关系。聚合关系用空心菱形加实线箭头表示,空心菱形在整体一方,箭头者指向部分一方。
这种关系表示的整体和部分之间是可分离的,它们具有各自的生命周期,部分可以属于多个整体,也可以为多个整体对象共享,所以聚合关系也成称为共享关系。例子:
一个员工可以属于多个部门,一个部门撤销了,员工也可以转到其它部门。
组合关系(Composition)
同样体现的是整体与部分间的包含关系。组合关系用实心菱形加实线箭头表示,实心菱形在整体一方,箭头指向部分一方。
这种关系表示的整体和部分之间是不可分的,部分也不能给其他整体共享,作为整体的对象负责部分的对象的生命周期。这种关系比聚合强,也称为强聚合。例子:
人死亡时,作为人体部分的头、躯干、四肢同时死亡。
UML图部分参考专栏:https://zhuanlan.zhihu.com/p/109655171
Python超简单入门
基本语法
作为一门弱类型的语言,变量的定义不需要再前面加类型说明,而在不同类型之间可以方便的进行切换。
Python有五个标准的数据类型:
- Numbers
- String
- List
- Tuple
- Dictionary
其中,支持四种不同的数字类型:int、long、float、comlex(复数)。
console:
常用容器
List列表是Python中使用最频繁的数据类型,用
[]
标识。
列表可以完成大多数集合类的数据结构实现,可以同时包含不同的数据,支持字符、数字、字符串,甚至可以包含列表。- 列表中值的切割也可以用到变量
[头下标:尾下标]
,就可以截取相应的列表,从左到右默认0开始,从右到左默认-1开始,下标可以为空表示取到头或尾。
+
是列表连接运算符,号重复操作。
console:
Tuple元组是另一个数据模型,元组用"()"标识,内部元素用
,
隔开,元组不能二次赋值,相当于只读列表,用法与List类似。console:
Dictionary字典是Python除列表之外最灵活的内置数据结构类型。字典用"
{ }
"标识,字典由索引key
和对应的值value
组成。注:字典中的元素通过键来存取的,而不是通过偏移来存取。console:
类的定义
使用class语句创建一个新类,class后面为类的名称并且以冒号结尾,例子:
类的帮助信息可以通过
ClassName._doc_
查看,class_suite
由类成员、方法、数据属性组成,例子:其中的
_init_
为初始化函数,也就是构造函数。访问权限:
_foo_
:定义的是特殊方法,一般是系统定义名字,类似_init_()
之类的。
_foo
:以单下划线开头的表示的是protected类型的变量,即保护类型只能允许其本身与子类进行访问。
__foo
:以双下划线的表示的是私有类型(private)的变量,只能是允许这个类本身进行访问了。
类的继承:
继承的语法结构如下:
Python继承的特点:
- 类的构造(
_init_()
方法)不会自动被调用,它需要在其派生类的构造中亲自专门调用。
- 在调用基类的方法时,需要使用
super()
前缀。
- 总是首先查找对应类型的方法,先在本类中查找调用的方法,找不到才去基类中找。
在继承元组中列了一个以上的类,那么它就叫做“多重继承”。
重载方法:
Python可以通过重写内置的方法来实现一些特殊的功能,这些方法有:
方法 | 描述 | 简答的调用 |
_init_(self [,args]) | 构造函数 | obj = className(args) |
_del_(self) | 析构方法,删除一个对象 | del obj |
_repr_(self) | 转化为供解释器读取的形式 | repr(obj) |
_str_(self) | 用于将值转化为适于人阅读的形式 | str(obj) |
_cmp_(self) | 对象比较 | cmp(obj,x) |
Python
Java
设计模式
创建型模式:主要用于创建对象
结构型模式:主要用于处理类或对象组合
行为型模式:主要用于描述对类或对象怎样交互和怎么样分配职责
类模式:处理类和子类的关系,这些关系通过继承建立,在编译时刻被确定下来,是一种静态关系
对象模式:处理对象之间的关系,这些关系在运行时变化,具有动态性
———————创建型设计模式——————
工厂模式
例子
Tony 工作的公司终于有了自己的休息区! 在这里大家可以看书、跑步、喝咖啡、玩体感游戏!开心工作,快乐生活!
现在要说的是休息区里的自制咖啡机,因为公司里有很多咖啡客,所以颇受欢迎!
咖啡机的使用也非常简单,咖啡机旁边有已经准备好的咖啡豆,想要喝咖啡,只要往咖啡机里加入少许的咖啡豆,然后选择杯数和浓度,再按一下开关,10分钟后,带着浓香味的咖啡就为你准备好了!当然,如果你想喝一些其他口味的咖啡,也可以自备咖啡豆,无论你是要拿铁还是摩卡,这些都还是问题。那问题来了,你是拿铁还是摩卡呢?
Python
Demo.py
Test.py
Java
Coffee.java
CaffeLatte.java
MochaCoffee.java
Coffeemaker.java
Main.java
在上面的示例中,我们可以通过咖啡机制作咖啡,加入不同的咖啡豆就产生不同口味的咖啡。就如同一个工厂一样,我们加入不同的配料,则会产出不同的产品,这就是程序设计中工厂模式的概念。
上面示例的类图关系如下:
思考
简单工厂模式
非常简单,其类图关系如下:
模型说明
对创建过程抽象出单独的一个类,对对象的创建和对象的使用进行分离,需要注意以下几点:
- 产品具有明显的继承关系,类型不能太多。
- 所有产品具有相同的方法和类似的功能。
优缺点
优点:
- 抽象出一个类对某类对象进行创建,分割出创建的职责,不能去创建具体的对象,只要传入适当的参数。
- 不用关系具体对象的类名称,只要传入什么参数就可以创建哪些需要的对象。
缺点:
- 不易拓展,一旦添加新的产品类型,就得修改工厂的创建逻辑。
- 产品类型较多时,创建逻辑可能过于复杂,不利于系统的维护。
工厂方法模式
模型说明
- 为解决简单工厂模式中不符合开放-封闭原则的问题
- impleFactory 进行了一个拆分,抽象出一个父类 Factory,并增加多个子类分别负责创建不同的具体产品
优缺点
优点
- 解决了简单工厂模式中不符合 开放—封闭原则 的问题,使程序更容易拓展。
- 实现简单
缺点
- 对于有多种分类的产品,或具有二级分类的产品,工厂方法模式并不适用。
多级分类
如我们有一个电子白板白板程序,可以绘制各种图形。那么画笔的绘制功能可以理解为是一个工厂,而图形可以理解为一种产品;图形可以根据形状分为直线、矩形、椭圆等,也可以根据颜色分为红色图形、绿色图形、蓝色图形等。
二级分类: 如一个家电工厂,它可能同时生产冰箱、空调和洗衣机,那么冰箱、空调、洗衣机属于一级分类;而洗衣机又可能分为高效型的和节能型的,那么高效型洗衣机和节能型洗衣机就属于二级分类。
抽象工厂模式
模型说明
适合有多个系列的产品,且每一个系列下有相同子分类的产品。
定义一个抽象的工厂类,类中定义生产每一种产品的方法,而两个具体的工厂实现类分别生产子分类1和子分类2的每一系列产品。
下面例子中,有冰箱、空调、洗衣机三个系列的产品,而每一个系列都有相同的子分类高效型和节能型。通过抽象工厂模式的类图,我们知道 Refrigerator、AirConditioner、WashingMachine 其实也可以不用继承自 HomeAppliances,因为可以把它们看成是独立的系列。当然真实项目中要根据实际应用场景而定,如果这三种家电有很多相同的属性,可以抽象出一个父类 HomeAppliances,如果差别很大则没有必要。
优缺点
优点
- 解决了具有二级分类的产品的创建。
缺点
- 如果产品的分类超过二级,如三级甚至更多的级,抽象工厂模式将会变得非常臃肿。
- 不能解决产品有多种分类多种组合的问题。
如果产品有多种分类多种组合怎么办?
如果产品有多种分类,就不能单独使用工厂模式了,需要结合其他的设计模式进行优化。如上面提到的白板程序,可以结合中介模式和工厂方法模式进行实现,
原型模式
单例模式
例子
爱情是每一个都渴望的,Tony 也是一样!自从毕业后,Tony 就一直没再谈过恋爱,离上一次的初恋也已经过去两年。一个巧合的机会,Tony 终于遇上了自己的喜欢的人,她叫 Jenny,有一头长发,天生爱笑、声音甜美、性格温和……
作为一个程序员的 Tony,直男癌的症状也很明显:天生木讷、不善言辞。Tony 自然不敢正面表白,但他也有自己的方式,以一种传统书信的方式,展开了一场暗流涌动的追求……经历了一次次屡战屡败,屡败屡战的追求之后,Tony 和 Jenny 终于在一起了!
然而好景不太长,由于种种的原因,最后 Jenny 还是和 Tony 分开了……
人生就像一种旅行,蜿蜒曲折,一路向前!沿途你会看到许多的风景,也会经历很多的黑夜,但我们无法回头!有一些风景可能很短暂,而有一些风景我们希望能够伴随自己走完余生。Tony 经历过一次被爱,也经历过一次追爱;他希望下次能找到一个可陪伴自己走完余生的她,也是他的唯一!
python
Demo.py
test.py
Java
思考
单例模式:就是一个类只能有一个对象(实例),单例就是用来控制某些事物只允许有一个个体。
Ensure a class has only one instance, and provide a global point of access to it.
模型抽象
- 重写new和init方法
Python
在 Python3的类中,new负责对象的创建,而init负责对象的初始化;new是一个类方法,而init是一个对象方法。
new是我们通过类名进行实例化对象时自动调用的,init是在每一次实例化对象之后调用的,new方法创建一个实例之后返回这个实例对象,并将其传递给 init方法的 self 参数。
在上面的示例代码中,我们定义了一个静态的 instance 类变量,用来存放 Singleton1 的对象,new 方法每次返回同一个_instance对象_(若未初始化,则进行初始化)。因为每一次通过 s = Singleton1() 的方式创建对象时,都会自动调用 init 方法来初始化实例对象;因此 isFirstInit 的作用就是确保只对 instance 对象进行一次初始化,故事剧情中的代码就是用这种方式实现的单例。
在 Java 和 C++ 这种静态语言中,实现单例模式的一个最简单的方法就是:将构造函数声明成 private,再定义一个 getInstance() 的静态方法返回一个对象,并确保 getInstance() 每次返回同一个对象即可,如下面的 Java 示例代码。
Java
- 自定义 metaclass 的方法
Python
在上面的代码中,我们定义了一个 metaclass(Singleton2)来控制对象的实例化过程。在定义自己的类时,我们通过 class CustomClass(metaclass=Singleton2) 来显示地指定 metaclass 为 Singleton2。
- 装饰器的方法
Python
装饰器的实质就是对传进来的参数进行补充,可以在原有的类不做任何代码变动的前提下增加额外的功能,使用装饰器可以装饰多个类。用装饰器的方式来实现单例模式,通用性非常好,在实际项目中用的非常多。
类图
装饰器2.0
Python
应用场景
1. 你希望这个类只有一个且只能有一个实例;
2. 项目中的一些全局管理类(Manager)可以用单例来实现。
——————结构性设计模式———————
*适配器模式
例子
身材苗条、长像出众是每个人梦寐以求的,尤其是女孩子!但很多人却因为先天的原因并不能如意,这时就需要通过服装、化妆去弥补。所谓美女,三分靠长相七分靠打扮!比如身高不够,就可以通过穿高跟鞋来弥补;如果本身就比较高,那穿不穿高跟鞋就没那么重要了。这里的高跟鞋就起着一个适配的作用,能让你的形象增高四、五厘米,下面我们就用代码来模拟一下高跟鞋在生活中的场景吧!
Python
Java
思考
适配器模式:将一个类的接口变成客户端所期望的另一种接口,从而使原本因接口不匹配而无法一起工作的两个类能够在一起工作。
Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn't otherwise because of incompatible interfaces.
模型抽象
Target 是一个接口类,是提供给用户调用的接口抽象,如上面示例中的 IHightPerson。Adaptee 是你要进行适配的对象类,如上面的 ShortPerson。Adapter 是一个适配器,是对 Adaptee 的适配,它将 Adaptee 的对象转换(或说包装)成符合 Target 接口的对象;如上面的 DecoratePerson,将 ShortPerson 的 getRealHeight 和 getShoesHeight 方法包装成 IHightPerson 的 getHeight 接口。
设计要点
- 目标(Target): 即你期望的目标接口,要转换成的接口。
- 源对象(Adaptee): 即要被转换的角色,要把谁转换成目标角色。
- 适配器(Adapter): 适配器模式的核心角色,负责把源对象转换和包装成目标对象。
优缺点
优点
- 可以让两个没有关联的类一起运行,起着中间转换的作用。
- 提高了类的复用。
- 灵活性好,不会破坏原有的系统。
缺点
- 如果原有系统没有设计好(如 Target 不是抽象类或接口,而一个实体类),适配器模式将很难实现。
- 过多地使用适配器,容易使代码结构混乱,如明明看到调用的是 A 接口,内部调用的却是 B 接口的实现。
应用场景
- 系统需要使用现有的类,而这些类的接口不符合现有系统的要求。
- 对已有的系统拓展新功能时,尤其适用于在设计良好的系统框架下增加接入第三方的接口或第三方的 SDK 时。
*桥接模式
组合模式
例子
只要你对硬件稍微有一些了解,或者打开过机箱换过组件,一定知道 CPU、内存、显卡是插在主板上的,而硬盘也是连在主板上的,在机箱的后面有一排的插口,可以连接鼠标、键盘、耳麦、摄像头等外接配件,而显示器需要单独插电源才能工作。我们可以用代码来模拟台式电脑的组成,这里假设每一个组件都有开始工作和结束工作两个功能,还可以显示自己的信息和组成结构。
Python
Java
思考
自己 DIY 组装的电脑是由各个配件组成的,在组装之前,就是一个个 CPU、硬盘、显卡等配件,不能称之为电脑,只有把它们按正确的方式组装在一起,配合操作系统才能正常运行。一般人使用电脑并不会关注内部的组成结构,只会关注一台整机。
这里有明显的部分与整体的关系,主板、电源等是电脑的一部分,而主板上又有 CPU、硬盘、显卡,它们又可以认为是主板的一部分。像电脑一样,把对象组合成树形结构,以表示“部分-整体”的层次结构的程序设计模式就叫组合模式。组合模式使得用户对单个对象和组合对象的使用具有一致性,使用组合对象就像使用一般对象一样,不便关心内部的组织结构。
可以转化为以下类图
模型抽象
类图
模型说明
优缺点
优点
- 调用简单,组合对象可以像一般对象一样使用。
- 组合对象可以自由地增加、删除组件,可灵活地组合不同的对象。
缺点
在一些层次结构太深的场景中,组合结构会变得太庞杂。
应用场景
- 对象之间具有明显的“部分-整体”的关系时,或者具有层次关系时。
- 组合对象与单个对象具有相同或类似行为(方法),用户希望统一地使用组合结构中的所有对象。
*装饰模式
例子
一个程序员,给自己搭配了一套着装:一条卡其色休闲裤、一双深色休闲皮鞋、一条银色针扣头的黑色腰带、一件紫红色针织毛衣、一件白色衬衫、一副方形黑框眼镜。但类似的着装也可以穿在其他的人身上,比如一个老师也可以这样穿:一双深色休闲皮鞋、一件白色衬衫、一副方形黑框眼镜。
我们就用程序来模拟这样一个情景。
Python
Java
思考
装饰模式:动态地给一个对象增加一些额外的职责(Responsibility),就增加对象功能来说,装饰模式比生成子类实现更为灵活。
继承关系类图
特点
- 可灵活地给一个对象增加职责或拓展功能
- 可增加任意多个装饰
- 装饰的顺序不同,可能产生不同的效果
模型抽象
类图
Component 是一个抽象类,代表具有某中功能(function)的组件,ComponentImplA 和 ComponentImplB 分别是其具体的实现子类。Decorator 是 Component 装饰器,里面有一个 Component 的对象 decorated,这就是被装饰的对象,装饰器可为被装饰对象添加额外的功能或行为(addBehavior)。DecoratorImplA 和 DecoratorImplB 分别是两个具体的装饰器(实现子类)。
优缺点
优点
- 使用装饰模式来实现扩展比继承更加灵活,它可以在不需要创造更多子类的情况下,将对象的功能加以扩展。
- 可以动态地给一个对象附加更多的功能。
- 可以用不同的装饰器进行多重装饰,装饰的顺序不同,可能产生不同的效果。
- 可以独立发展,不会相互耦合;装饰模式相当于是继承的一个替代模式。
缺点
- 与继承相比,用装饰的方式拓展功能更加容易出错,排错也更困难。对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐。
应用场景
- 有大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长。
- 需要动态地增加或撤销功能时。
- 不能采用生成子类的方法进行扩充时,如类定义不能用于生成子类。
装饰模式的应用场景非常广泛。如在实际项目开发中经常看到的过滤器,便可用装饰模式的方式去实现。如果你是 Java 程序员,对 IO 中的 FilterInputStream 和 FilterOutputStream 一定不陌生,它的实现其实就是一个装饰模式。FilterInputStream(FilterOutputStream) 就是一个装饰器,而 InputStream(OutputStream) 就是被装饰的对象。
Demo
如果你对图形图像处理有一定了解,就会知道对图像的处理其实就是对一个二维坐标像素数据的处理,如图像的灰度化、梯度化(锐化)、边缘化、二值化。这些操作顺序不同就会造成不同的效果,也是适合用装饰模式来进行封装。
*外观模式
例子
9月是所有大学的入学季,新生入学报道是学校的一项大工程,每一个学校都有自己的报道流程和方式,但都少不了志愿者这一重要角色!一来,学长学姐带学弟学妹是尊师重教的一种优良传统;二来,轻车熟路的学长学姐作为志愿者为入学新生服务,能给刚入学的新生减少了诸多不必要的麻烦。下面我们用程序来模拟一下新生报到的整个流程。
Python
Java
思考
外观模式:为子系统中的一组接口提供一个一致的界面称为外观模式,外观模式定义了一个高层接口,这个接口使得这一子系统更容易使用。
Provide a unified interface to a set of interfaces in a subsystem. Facade defines a higher-level interface that makes the subsystem easier to use.
外观模式的核心思想:用一个简单的接口来封装一个复杂的系统,使这个系统更容易使用。
模型抽象
类图
在软件的层次化结构设计中,可以使用外观模式来定义每一层系统的调用接口,层与层之间不直接产生联系,而通过外观类建立联系,降低层之间的耦合度。这时就会有如下这样的层次结构图:
例子:
模型说明
设计要点
外观模式是最简单的设计模式之一,只有两个角色。
外观角色(Facade): 为子系统封装统一的对外接口,如同子系统的一个门面。这个类一般不负责具体的业务逻辑,只是一个委托类,具体的业务逻辑由子系统完成。
子系统(SubSystem): 由多个类组成的具有某一特定功能的子系统。可以是第三方库,也可以是自己的基础库,还可能是一个子服务,为整个系统提供特定的功能或服务。
优缺点
优点:
- 实现了子系统与客户端之间的松耦合关系,这使得子系统的变化不会影响到调用它的客户端。
- 简化了客户端对子系统的使用难度,客户端(用户)无须关心子系统的具体实现方式,而只需要和外观进行交互即可。
- 为不同的用户提供了统一的调用接口,方便了系统的管理和维护。
缺点:
- 因为统一了调用的接口,降低了系统功能的灵活性。
应用场景
- 当要为一个复杂子系统提供一个简单接口时;
- 客户程序与多个子系统之间存在很大的依赖性,引入外观类将子系统与客户以及其他子系统解耦,可以提高子系统的独立性和可移植性;
- 在层次化结构中,可以使用外观模式定义系统中每一层的入口,层与层之间不直接产生联系,而通过外观类建立联系,降低层之间的耦合度。
代理模式
例子
在生活中,我们经常要找人帮一些忙:帮忙收快递,帮忙照看宠物狗。在程序中,有一种类似的设计,叫代理模式。
Python
Java
思考
代理模式(Proxy Pattern):给某一个对象提供一个代理,并由代理对象控制对原对象的引用。代理模式的英文叫做 Proxy 或 Surrogate,它是一种对象结构型模式。
在某些情况下,一个客户不想或者不能直接引用一个对象,此时可以通过一个称之为“代理”的第三者来实现间接引用。如上面的示例中,Tony 因为不在家,所以不能亲自接收包裹,但他可以叫 Wendy 来代他接收,这里 Wendy 就是代理,她代理了 Tony 的身份去接收快递。
模型抽象
三个关键要素:
- 主题(Subject):定义“操作/活动/任务”的接口类。
- 真实主题(RealSubject):真正完成“操作/活动/任务”的具体类。
- 代理主题(ProxySubject):代替真实主题完成“操作/活动/任务”的代理类。
代码框架:
Python
Java
类图:
模型说明
代理对象可以在客户端和目标对象之间起到中间调和的作用,并且可以通过代理对象隐藏不希望被客户端看到的内容和服务,或者添加客户需要的额外服务。
在实现生活中能找到非常的代理模式的模型:火车票/机票的代售点;银行支票在市场交易中用来代替现金,并提供对签发人账号上资金的控制;代表公司出席一些商务会议。
优缺点
优点
- 代理模式能够协调调用者和被调用者,在一定程度上降低了系统的耦合度。
- 可以灵活地隐藏被代理对象的部分功能和服务,也增加额外的功能和服务。
缺点
- 由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。
- 实现代理模式需要额外的工作,有些代理模式的实现非常复杂。
——————行为型设计模式——————
职责链模式
*命令模式
例子
盒马鲜生之所以这么火爆,一方面是因为中国从来就不缺像 David 这样的吃货,另一方面是因为里面的海生很新鲜,而且可以自己挑选。很多人都喜欢吃大闸蟹,但是你有没有注意到一个问题?从你买大闸蟹到吃上大闸蟹的整个过程,可能都没有见过厨师,而你却能享受美味的佳肴。这里有一个很重要的角色就是服务员,她帮你下订单,然后把订单传送给厨师,厨师收到订单后根据订单做餐。我们用代码来模拟一下这个过程。
Python
Java
思考
在上面的示例中,我们只要发一个订单就能吃到想要的加工方式的美味佳肴,而不用知道厨师是谁,更不用关心他是怎么做出来的。像点餐的订单一样,发送者(客户)与接收者(厨师)没有任何的依赖关系,我们只要发送订单就能完成想要的任务,这在程序中命令模式。
类图
命令模式:将一个请求封装成一个对象,从而让你使用不同的请求把客户端参数化,对请求排队或者记录请求日志,可以提供命令的撤销和恢复功能。
Encapsulate a request as an object, thereby letting you parametrize clients with different requests, queue or log requests, and support undoable operations.
命令模式的最大特点是将具体的命令与对应的接收者相关联(捆绑),使得调用方不用关系具体的行动执行者及如何执行,只要发送正确的命令,就能准确无误地完成相应的任务。就像军队,将军一声令下,士兵就得分秒无差,准确执行。
命令模式是一种高内聚的模式,之所以说是高内聚是因为他把它命令封装成对象,并与接收者关联在一起,从而使(命令的)请求者(Invoker)接收者(Receiver)分离。
模型抽象
Python
Java
类图
上面的类图中 Command 是核心类,表示一项任务一个动作,如示例中的订单,是所有命令的抽象类,定义了统一的执行方法 execute。具体的命令实现类 CommandA 和 CommandB 包装了命令的接收者(分别是 ReceiveA 和 ReceiveB),在执行 execute 方法时会调用接收者的实现(如 doSomething 和 function)。Receiver 是命令的接收者,也是任务的具体的执行者,如示例中的厨师。Invoker 负责命令的调用,如示例中的服务员。Client 的真正的用户,如示例中的顾客。
设计要点
命令模式中主要有四个角色,在设计命令模式时要找到并区分这些角色,具体如下。
- 命令(Command): 要完成的任务,或要执行的动作,这是命令模式的核心角色。
- 接收者(Receiver): 任务的具体实施方,或行动的真实执行者。
- 调度者(Invoker): 接受任务并发送命令,对接用户的需求并执行内部的命令,负责外部用户与内部命令的交互。
- 用户(Client): 命令的使用者,即真正的用户。
优缺点
优点
- 对命令的发送者与接收者进行解耦,使得调用方不用关系具体的行动执行者及如何执行,只要发送正确的命令即可。
- 可以很方便地增加新的命令。
缺点
- 在一些系统中可能会有很多的命令,而每一个命令都需要一个具体的类去封装,容易使命令的类急剧膨胀。
应用场景
- 希望系统发送一个命令(或信号),任务就能得到处理时,如 GUI 中的各种按钮的单击命令;再如自定义一套消息的响应机制。
- 需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互时。
- 需要请一系列的命令组合成一组操作时,可以使用宏命令的方式。
解释器模式
迭代器模式
中介者模式
备忘录模式
*观察者模式
例子
Tony 陷入白日梦中……他的梦虽然在现实世界里不能立即实现,但在程序世界里可以。程序来源于生活,下面我们就用代码来模拟 Tony 的白日梦。
Python
Java
思考
观察者模式,顾名思意就是观察与被观察的关系,比如你在烧开水得时时看着它开没开,你就是观察者,水就是被观察者;再比如说你在带小孩,你关注她是不是饿了,是不是喝了,是不是撒尿了,你就是观察者,小孩就是被观察者。
观察者模式一般是一种一对多的关系,可以有任意个(一个或多个)观察者对象同时监听某一个对象。监听的对象叫观察者(后面提到监听者,其实就指观察者,两者是等价的),被监听的对象叫被观察者(Observable,也叫主题 Subject)。被观察者对象在状态或内容发生变化时,会通知所有观察者对象,使它们能够做出相应的变化(如自动更新自己的信息)。
模型抽象
对上面的Python代码进行进一步的重构和优化,抽象出观察者模式的框架模型
类图
addObserver,removeObserver 分别用于添加和删除观察者,notifyObservers 用于内容或状态变化时通知所有的观察者。因为 Observable 的 notifyObservers 会调用 Observer 的 update 方法,所有观察者不需要关心被观察的对象什么时候会发生变化,只要有变化就是自动调用 update,只需要关注 update 实现就可以了。
实现方法2.0
Java
Python
设计要点
- 要明确谁是观察者谁是被观察者,只要明白谁是关注对象,问题也就明白了。一般观察者与被观察者之间是多对一的关系,一个被观察对象可以有多个监听对象(观察者)。如一个编辑框,有鼠标点击的监听者,也有键盘的监听者,还有内容改变的监听者。
- Observable 在发送广播通知的时候,无须指定具体的 Observer,Observer 可以自己决定是否要订阅 Subject 的通知。
- 被观察者至少需要有三个方法:添加监听者、移除监听者、通知 Observer 的方法;观察者至少要有一个方法:更新方法,更新当前的内容,作出相应的处理。
应用场景
- 对一个对象状态或数据的更新需要其他对象同步更新,或者一个对象的更新需要依赖另一个对象的更新;
- 对象仅需要将自己的更新通知给其他对象而不需要知道其他对象的细节,如消息推送。
状态模式
*策略模式
例子
随着社会的发展、时代的进步,出行交通的方式可谓是越来越多样,可以说是丰富到了千奇百怪的地步了。除了上面提到的共享单车、公交车、地铁、快车(或出租车),也可以是自驾、电动车、平衡车,甚至都可以踏个轮滑、踩个滑板过来!采用什么交通方式并不重要,重要的是你能准时来共聚晚餐,不然就只能吃残羹冷炙了,哈哈!下面用代码来模拟一下大家使用不同的出行方式参加聚餐的情景吧。
Python
Java
类图
思考
上面的示例中我们可以选择不同的出行方式去参加聚餐,可以骑共享单车,也可以坐公共汽车,亦或是踩一辆平衡车;选用什么交通工具不重要,重要的是能够实现我们的目标——准时到达聚餐的地点,我们可以根据自己的实际情况进行选择和更换不同的出行方式。这里,选择不同的交通工具,就相当于选择了不同的出行策略;在程序中也有这样一种类似的模式——策略模式。
策略模式:定义一系列算法,将每个算法都封装起来,并且使他们之间可以相互替换。策略模式使算法可以独立于使用它的用户而变化。
Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from the clients that use it.
策略模式是对算法、规则的一种封装。如上面的示例中,可以将不同的出行方式(采用的交通工具)理解成一种出行算法,将这些算法抽象出一个基类 IVehicle,并定义一系列的算法,共享单车(SharedBicycle)、快速公交(ExpressBus)、地铁(Subway)、快车(Express)。我们可以选择任意一种(实际场景肯定会选择最合适的)出行方式,并且可以方便更换出行方式。如 henry 要把出行方式由快速公交改成快车,只需要调用出改一行代码即可。
模型抽象
类型
Context 是一个上下文环境类,负责提供对外的接口,与用户的交互,屏蔽上层对策略(算法)的直接访问,如示例中的 Classmate。Strategy 是策略(算法)的抽象类,定义统一的接口,如示例中的 IVehicle。StrategyA 和 StrategyB 是具体策略的实现类,如示例中 SharedBicycle、ExpressBus 等。
设计要点
策略模式中主要三个角色,在设计策略模式时要找到并区分这些角色:
- 上下文环境(Context): 起着承上启下的封装作用,屏蔽上层应用对策略(算法)的直接访问,封装可能存在的变化。
- 策略的抽象(Strategy): 策略(算法)的抽象类,定义统一的接口,规定每一个子类必须实现的方法。
- 具备的策略: 策略的具体实现者,可以有多个不同的(算法或规则)实现。
优缺点
优点
- 算法(规则)可自由地切换。
- 避免使用多重条件判断。
- 方便拓展和增加新的算法(规则)。
缺点
- 所有策略类都需要对外暴露。
应用场景
- 如果一个系统里面有许多类,它们之间的区别仅在于有不同的行为,那么可以使用策略模式动态地让一个对象在许多行为中选择一种。
- 一个系统需要动态地在几种算法中选择一种。
- 设计程序接口时希望部分的内部实现由调用方自己实现。
*桥接模式
这个模式可以和策略模式合为一个模式,因为思想相同,代码结构也几乎一样,如下图它们的类图结构几乎相同,只是一个(策略模式)侧重于对象行为,一个(桥接模式)侧重于软件结构。
策略类图
桥接类图
桥接模式:将抽象和实现解耦,使得它们可以独立地变化。
Decouple an abstraction from its implementation so that the two can vary independently.
桥梁模式关注的是抽象和实现的分离,使得它们可以独立地发展;桥梁模式是结构型模式,侧重于软件结构。而策略模式关注的是对算法、规则的封装,使得算法可以独立于使用它的用户而变化;策略模式是行为型模式,侧重于对象行为。
模板模式
例子
在阅读电子书时,根据每个人的不同阅读习惯,可以设置不同的翻页方式,如左右平滑、仿真翻页等,不同的翻页方式,会给人以不同的展示效果。
Python
思考
模板模式:在父类中提供了一个定义算法框架的模板方法,而将算法中用到的具体的步骤放到子类中去实现,使得子类可以在不改变算法的同时重新定义该算法的某些特定步骤。
Define the skeleton of an algorithm in an operation, deferring some steps to client subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure.
代码框架
Python
Java
模型抽象
类图
Template 是一个模板类,用于定义模板方法(某种算法的框架),如示例中的 ReaderView。TemplateImplA 和 TemplateImplB 是模板类的具体子类,用于实现算法框架中的一些特定步骤。
应用场景
- 对一些复杂的算法进行分割,将其算法中固定不变的部分设计为模板方法和父类具体方法,而一些可以改变的细节由其子类来实现。即一次性实现一个算法的不变部分,并将可变的行为留给子类来实现。
- 各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复。
- 需要通过子类来决定父类算法中某个步骤是否执行,以实现子类对父类的反向控制。
访问者模式
面向对象设计原则
- 单一职责原则
类的职责要单一,不要将太多的职责放在同一个类,职责越多,可复用性越低
- 开闭原则
开:对扩展开放,闭:对修改关闭,在不修改一个软件实体的基础去扩展器功能;抽象
化是开闭原则的关键,定义一个相对稳定的抽象层,将不同的实现行为在具体的实现层完成。
- 里氏代换原则
代换:使用基类对对象定义,运行时用子类对象代替父类对象
尽量把父类设计为抽象类或者接口,让子类继承父类或实现父类的接口
- 依赖倒转原则
高层模块不应该依赖低层模块,两者都应该依赖其抽象
抽象不应该依赖细节
细节应该依赖抽象
抽象方式耦合、依赖注入
实现方法:在代码中使用抽象类,将具体类放在配置文件中
- 接口隔离原则
用多个专门的借口代替单一的总接口
- 合成复用原则
合成:组合和聚合
尽量使用对象组合/聚合,而不是通过继承来达到目的
- 迪米特法则
一个软件实体应当尽可能少地与其他实体发生相互作用(最少知识原则)
ISSUE
1. 高内聚低耦合的原则促进了:
- 重用性
- 可维护性
- 扩展性
2. 继承过度会导致:
- 脆弱性
- 重复性
- 可扩展性变差
3. 里氏替换原则要求:
- 子类对象可以替换父类对象
4. 开闭原则要求:
- 对扩展开放,对修改关闭
5. 接口隔离原则要求:
- 客户端不应该依赖不需要的接口
6. 迪米特法则要求:
- 一个对象应该尽量少地与其他对象耦合
7. 合成复用原则要求:
- 尽量使用组合,而不是继承达到复用的目的
8. 单一责任原则要求:
- 一个类只负责一项职责
9. 最少知识原则要求:
- 一个实体应该尽量少地与其他实体耦合
10.依赖倒置原则要求:
- 高层模块不应该依赖低层模块,两者都应该依赖其抽象
11.开放封闭原则要求软件实体应该是:
- 扩展开放,修改封闭
12.接口隔离原则要求:
- 优先使用聚合(composition),而不是继承(inheritance)
13.里氏替换原则要求:
- 子类对象可以替换父类对象
14.依赖倒置原则要求:
- 高层模块和低层模块都依赖其抽象
- 合成复用原则主要用于:
- 类与类之间的关系设计
16.依赖倒置原则主要用于:
- 类与类之间的依赖关系设计
17.开闭原则主要应用于:
- 类
18.迪米特法则主要应用于:
- 类与类之间的依赖关系设计
19.单一职责原则主要应用于:
- 类
20.最少知识原则主要应用于:
- 类与类之间的依赖关系设计
21.里氏替换原则主要应用于:
- 类的继承体系设计
20.高内聚低耦合原则应用于:
- 所有设计
21.聚合复用原则主要用于:
- 类与类之间的依赖关系设计
- 开闭原则是面向对象的可复用设计的基石,开闭原则是指一个软件实体应当对扩展开放,对修改关闭;里氏代换原则是指任何子类对象可以出现的地方,基类对象一定可以出现;依赖倒转原则就是要依赖于抽象,而不要依赖于实现,或者说要针对接口编程,不要针对实现编程。
Loading...