本文概要
持续更新中。。。学习感悟:
- 学习设计模式时,要结合类图和调用时序图理解。温故时要能够自己画出类图和时序图,这是一个咀嚼知识和消化的过程,虽然很痛苦并且进展缓慢,但是有助于理解和真正掌握知识。最近一直用一段话来鞭策自己:以为“听到就是知道,知道就是掌握”,从而造成一种短时间内智力快速上升、知识量爆棚的幻觉。” (估计跨年的时候我还在写这篇blogT^T)
- 对于设计模式的理解:首先记住这种模式的特点,然后思考在当前设计模式模式下,改变这些角色时,还符合开闭原则吗?对于这一点,在学习工厂模式的演变,体会更深。一个设计模式的优点应从以下方面考虑:1.该设计模式的使用情况; 2. 更改的时候,更改地多不多;3. 增加对象类时,符不符合开闭原则。设计模式的缺点则考虑: 1. 使用情况有什么局限之处;2. 增加某些对象类是否具有困难(开发量巨大?不符合开闭原则等等)
这里的客户端是指设计模式的调用方
1. 设计目标
- 可维护性
软件可维护性是指软件产品被修改的能力,修改包括纠正、改进或软件对环境、需求和功能规格说明变化的适应。 - 可拓展性
将当前软件系统中的可能面临的变化纳入设计(通过接口对变化进行抽象),让当前设计去适应未来不确定的变化。
当未来某些方面发生改变的时候,我们能够以最小的改动来适应这种变化。我们的改动越小,并且对这种变化的适应性越好,我们就会说这个设计的可扩展性是非常好的。 - 可复用
当前的设计可以在多处使用,甚至还可以被将来设计的代码所使用。
2. 设计模式的几个重要原则
- 单一职责原则
就一个类而言,应该仅有一个引起它变化的原因。
多于一个动机去改变一个类,那么这个类就具有多于一个职责,这时候就应该考虑类的职责分离,即需要将这个类进行拆分。
一个类承担的职责过多,就等于把这些职责耦合在一起。一个职责的变化可能会削弱或者限制这个类完成其他职责的能力。即内部耦合度高,可维护性差。
-
开闭原则
软件实体(类,模块,函数等)应该可以拓展,但是不可以修改的。即,对拓展是开放的,对更改是封闭的。
对程序中频繁变化的那些部分进行抽象,以后面对同类的变化,只需要增加新的代码即可。
-
依赖倒转原则
高层模块不应该依赖低层模块,二者都应该依赖抽线
抽象不应该依赖细节,细节应该依赖抽线 -
里氏代换原则
子类可以替代父类。
由于子类的可替代性才使得父类的模块在无需修改的情况下就可以拓展。 -
迪米特原则:
强调降低类之间的耦合性
若两个类不必直接通信,那么这两个类就不应该发生直接的相互作用。若其中一个类需要调用另一个类的某个方法时,可以通过第三方转发调用。
类之间的耦合性越低,越有利于复用,一个弱耦合的类被修改,不会对与其有关系的类造成很大的影响。 -
合成聚合复用原则:
尽量使用组合/聚合代替继承,有助于保持类的封装及单一职责,且使得类和类的继承层次会保持较小的规模。
聚合是一种“弱”拥有关系:A可以包含B,但B不是A的一部分
合成则是一种“强”拥有关系:A是B的一部分,A与B的生命周期一致。
在使用继承时,一定要在是“is-a"的关系时再考虑使用。
3. 23种常用设计模式
类图 优点 缺点
3.1创建者型模式
3.1.1 3种工厂模式
共同特点: 客户端不应当依赖于产品类实例如何被创建、组合和表达的细节,
3.1.1.1 简单工厂模式
类图
主要含有以下3种角色:
- 简单工厂对象
SimpleFactory
根据客户端提供的参数生成具体的Product - 抽象产品对象
AbstractFactory
- 具体产品对象
ConcreteProduct1
,ConcreteProduct2
,ConcreteProduct3
调用时序图
以客户端委托SimpleFactory
创建ConcreteProduct1
为例
特点
- 优点:客户端无须创建产品对象,只需要提供产品的创建参数即可。
- 缺点:违反了开闭原则:需要添加新的
ConcreteProductX
时,对已有的Product的没有影响,但是需要修改SimpleProduct
中的createProduct()
内部代码
适用场景
- Product的数量不多,避免工厂类内部的业务逻辑太过复杂;
- Product数量不再或者极少改变,那么工厂类的逻辑修改比较少;
应用实例
制定不同加密算法的密钥生成器
KeyGenerator keyGen=KeyGenerator.getInstance("DESede");
3.1.1.2 工厂模式
类图
含有四种角色:
- 抽像工厂类
Factory
定义一个创建对象的接口 - 生产对应的产品的具体工厂类
ConcreteProductXFactory
具体工厂类只生产一种具体的产品 - 产品类
AbstractProduct
- 具体产品类
ConcreteProductX
具体产品类与具体工厂类一一对应
调用时序图
以客户端委托ConcreteProduct1Factory
创建ConcreteProduct1
为例
对比简单工厂模式和工厂模式的时序调用图,几乎相同。区别在于Factory内部的职责大小不同:工厂模式对简单工厂模式的职责进行拆分,每个工厂只负责生成一种产品。
特点
- 优点:与简单工厂相比,遵守了开闭原则(增加新的产品类,只需要增加新的工厂类);客户端更换产品对象时,改动较少(只需要改动具体工厂类即可)
在简单工厂类模式中,客户端想要更换产品对象时,需要改动所有创建对象的参数,因为简单工厂模式是根据创建参数来判断创建哪一种产品的。
- 缺点:每增加一个产品类,就会增加一个具体工厂类,增加额外的开发量,类的数目也较多。
适用场景
客户端不需要知道具体的产品类的创建逻辑甚至具体产品的名称,但是需要知道创建具体产品的工厂类,因此,可以动态配置具体工厂类,将类名存储在配置文件或数据库中。
应用实例
JDBC使用不同数据库,需要先下载配置数据库的driver,再进行连接
3.1.1.3 抽象工厂模式
类图
与工厂模式相类似,一样具有四种角色:
- 抽象工厂类
Factory
- 抽象产品类:
Product1
,Product2
,Product3
- 具体工厂类:
AProductFactory
,BProductFactory
,CProductFactory
- 具体产品类:
AProduct1
,AProduct2
,AProduct3
…
与工厂模式相比,每个工厂将生成一系列产品,比如,AProductFactory生成AProduct系列产品:AProduct1, AProduct2, AProduct3.
调用时序图
特点
- 优点:与工厂类相比,客户端不需要知道生产具体产品的工厂;客户端更换产品对象时,改动较少(只需要改动具体工厂类即可);增加具体的产品很方便,不需要修改已有的系统。
- 缺点:开闭原则的倾斜性(增加新的抽象产品类,需要修改整个工厂类极其子类,改动量巨大;增加新的工厂或具体产品,改动比较少)
适用场景
出现多个产品系列的场景,并且客户端并不关心产品创建逻辑。
应用实例
在很多软件系统中需要更换界面主题,要求界面中的按钮、文本框、背景色等一起发生改变时,可以使用抽象工厂模式进行设计。
3.1.1.4 简单工厂模式 vs. 工厂模式 vs. 抽象工厂模式
只有一种抽象产品时(一种产品等级结构),抽象工厂模式就退化为工厂模式。
当每种产品只有一个具体产品,抽象工厂模式只有一个工厂来创建对象,并将创建对象的方法设计为静态方法时,抽象工厂方法退化简单工厂方法。
3.1.2 建造者模式
主要用于构建一些复杂的对象,这些对象内部构造顺序通常是稳定的,但对象内部的构建往往则是复杂多变的。
类图
一般具有5种角色:
- 产品类
Product
- 指导产品构造顺序的指导者类
Director
(非必需,有时候会跟ProductBuilder合并,如下图右侧类图所示)
该类的作用主要有两个:一方面它隔离了客户与生产过程;另一方面它负责控制产品的生成过程。 - 产品构造类
Builder
- 包含产品内部具体构造逻辑的产品构造类
ProductBuilder1
,ProductBuilder2
调用时序图
以客户端想要获得Product1实例
倘若没有Director
,调用时序图如下,
特点
- 优点:
- 客户端不必知道产品内部细节,产品的产生和产品的构建过程解耦,相同的构建过程可以创建不同种类的对象;
- 客户端想要修改产品类别,只需要修改具体的产品建造者;
- 增加新的具体构造者无需修改现有代码,扩展方便,符合“开闭原则”。
- 缺点:
- 使用范围有限制:使用建造者模式中,产品组成成分和构造过程相似,产品之间差别太大则不适合建造者模式
- 如果产品内部变化比较负责,需要很多具体建造者来适应这种变化,导致建造类继承关系臃肿。
适用场景
- 需要生成的产品对象有复杂的内部结构,这些产品对象通常包含多个成员属性。
- 需要生成的产品对象的属性相互依赖,需要指定其生成顺序。
- 隔离复杂对象的创建和使用,并使得相同的创建过程可以创建不同的产品。
应用实例
换装游戏中,给人物选择不同的帽子,上衣,裤子,鞋子,包包等。
3.1.4 单例模式
保证一个类仅有一个实例,并提供一个访问它的全局访问点
类图
调用时序图
特点
- 优点:
- 由于全局只有唯一实例,因此单例模式可以严格控制客户端的访问;
- 由于系统内存中只存在一个对象,节省系统资源;对于频繁创建和销毁的对象,单例模式无疑可以提高系统性能。
- 单例模式可以进一步进化为创建固定数目的实例(多例模式)。
- 缺点:
- 单例模式没有抽象层,扩展很困难(享元模式=多例模式+抽象层)
- 单例类的职责过”重“,既当工厂类(提供工厂方法)有充当产品角色(提供产品的功能方法)
- 注意线程安全
适用场景
- 系统只需要一个实例对象
- 对于频繁创建和销毁的对象,单例模式无疑可以提高系统性能。
应用实例
系统要求提供一个唯一的序列号生成器,用于数据库的主键
3.2 结构型模式
3.2.1 适配器模式
类图
适配器模式主要有三类角色:
- 客户端所期待的接口
Target
- 需要进行适配的对象(适配者)
Adaptee
- 通过内部包装一个
Adapter
对象,将源接口转换为目标接口
调用时序图
特点
- 优点:
- 为复用现有的类与方法,通过引入Adapter将适配者与目标接口兼容,而不需要修改原有代码;
- 增加适配者类的透明性,适配者类被封装在适配器类中,对客户端而言是透明的。
- 灵活性和拓展性好,可以在不改变原有代码基础上增加新的适配器类,符合开闭原则。
- 缺点:
对于Java、C#等不支持多重继承的语言,一次最多只能适配一个适配者类
适用场景
- 系统需要使用现有的类,而这些类的接口不符合系统的需要。
- 在系统设计之初,需要使用第三方组件的地方可以使用适配器模式(该组件与系统的接口是不同的,而系统没有必要为了迎合它而改动自己的接口)
应用实例
JDBC给出一个客户端通用的抽象接口,每一个具体数据库引擎(如SQL Server、Oracle、MySQL等)的JDBC驱动软件都是一个介于JDBC接口和数据库引擎接口之间的适配器软件。抽象的JDBC接口和各个数据库引擎API之间都需要相应的适配器软件,这就是为各个不同数据库引擎准备的驱动程序。
3.2.2 装饰模式
类图
由四类角色组成:
- 抽象构件类
Component
定义了构件的抽象接口 - 具体构件类
ConcreteComponent
- 装饰器类
Decorator
- 具体装饰器类
ConcreteDecorator1
,ConcreteDecorator2
给Component
添加新的职责additionalFunc()
调用时序图
给Component
添加了两个新的功能/职责/装饰器,使用新功能。
特点
动态地给对象添加额外的职责(新字段/方法/逻辑)
- 优点:
1. 动态地给对象添加额外的职责(新字段/方法/逻辑),比生成子类方便,不需要修改原有代码,遵守开闭原则
2. 拓展方便,给对象添加新的职责时,只需要新建新的装饰器即可,不需要修改原有代码,遵守开闭原则
3. 灵活度高,每个装饰功能可以根据需要有选择地,按顺序地使用装饰器功能。 - 缺点:
1. 装饰器和具体装饰器类增加系统的复杂性,加大学习与理解的难度
2. 这种比继承要灵活机动的特性,意味着装饰器更加容易出错和排错
适用场景
- 为已有的功能添加更多的功能(这些功能可以再动态地撤销),与此同时,还能保证接口不改变(对
Component
透明) - 当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。不能采用继承的情况主要有两类:第一类是系统中存在大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长;第二类是因为类定义不能继承(如final类).
- 需要为一批的兄弟类进行改装或加装功能,当然是首选装饰模式。
3.2.3 组合模式
类图
主要有三类对象:
- 抽象组合对象
Component
声明用于管理和访问component的方法 - 叶节点对象
Leaf
叶节点对象没有子节点 - 组合对象
Composite
可以存储,管理和访问component
调用时序图
特点
将对象组合成树形结构以表示”部分-整体“的层次结构。
- 优点:
1. 用户对叶子节点对象和组合对象的使用具有一致性:叶子节点可以被组合成更复杂的组合对象,组合对象又可以被组合,这样可以不断递归下去,形成”部分-整体“的层次结构。而用户并不需要关心处理的对象的类型。
2. 扩展方便,很方便就能增加节点 - 缺点:
组合对象增加新的功能时,需要修改正常组合结构的所有对象的,不符合开闭原则
适用场景
只要是树形结构,就要考虑使用组合模式;
只要是要体现局部和整体的关系的时候,而且这种关系还可能比较深,考虑一下组合模式吧。
应用实例
XML文件读写
3.2.4 外观模式(门面模式)
类图
由两类角色组成:
- 外观类
Facede
委派子系统对象完成客户端的请求。外观类不参与子系统内的业务逻辑。 - 子系统类
SubSystem1
,SubSystem2
,SubSystem3
,SubSystem4
处理Facade
对象指派的任务。子系统类并不知道Facade
的存在。
调用时序图
特点
- 优点:
- 减少类之间的耦合度。
Facade
降低了客户端与子系统之间耦合度。 - 子系统的改动不影响
Facade
。
- 减少类之间的耦合度。
- 缺点:
Facade
的职责过重。当外观类已经庞大到不能忍受的程度,比如一已经超过了200行的代码,虽然都是非常简单的委托操作,也建议拆分成多个外观类,否则会给以后的维护和扩展带来不必要的麻烦。
那怎么拆分呢?可以参考“单一职责”,比如一个数据库操作的门面可以拆分为查询外观类、删除外观类、更新外观类等。
适用场景
- 在分层架构中,层与层之间建立
Facade
,为每一层的众多的子系统提供一个简单的接口,降低了耦合度。 - 系统内部因不断重构演化而变得复杂,产生了很多的小类,此时可以增加
Facade
,给外部调用提供一个简单的接口,减少耦合度。 - 新系统需要调用遗留的大系统时,可以通过
Facade
实体新老系统的交互
3.2.5 享元模式
类图
四类角色:
- 享元类的抽象接口
Flyweight
- 具体享元类
ConcreteFlyweight
除几个参数unsharedfiled
以外其他基本相同, 因而在实现共享类实例时,把变化的参数有客户端调用具体的享元类时传递进去,比如setUnsharedField
. - 享元工厂
FlyweightFactory
享元模式常常和工厂模式一起使用。
用来创建和管理享元对象。当客户端请求一个享元对象时,享元工厂提供一个已创建的实例或者创建一个实例 - 不需要共享的享元类
UnsharedFlyweight
调用时序图
特点
- 优点
- 减少内存中的对象,使相同对象或相似对象在内存中只保存一份;
- 享元模式的外部状态相对独立,不会影响外部状态,从而是的享元对象可以在不同的环境中被共享。
- 缺点:
- 由于享元模式需要分离出内部状态和外部状态,使得程序逻辑变得复杂
- 享元模式需要将享元对象的状态外部化,而读取外部状态使得运行时间变长。
适用场景
- 一个系统有大量相同或者相似的对象,由于这类对象的大量使用,造成内存的大量耗费。
- 使用享元模式需要维护一个存储享元对象的享元池,而这需要耗费资源,因此,应当在多次重复使用享元对象时才值得使用享元模式。
应用实例
数据库连接池,线程池
3.2.6 代理模式
目前,很多编程语言都已经实现了代理模式,比如Java的动态代理机制。
类图
三大类角色:
- 抽象接口类
Subject
- 代理类
Proxy
保存一个RealSubject
的引用使得代理得以调用RealSubject
的方法。
Proxy
,RealSubject
具有相同的接口,使得前者可以代替后者 - 真实主题类
RealSubject
定义了代理类所代表的真实实体
调用时序图
特点
- 优点:
- 通过代理模式,将调用类与被调用类解耦
- 有效控制外部对象对被调用类的访问权限
- 在调用过程中,可以添加额外的操作
- 缺点:
- 增加委托类对象,会使程序的逻辑更为复杂
- 调用的路径变长(增加了委托类对象),可能会导致处理请求的速度变慢
适用场景
- 远程代理: 为一个位于不同的地址空间的对象提供一个本地 的代理对象,这个不同的地址空间可以是在同一台主机中,也可是在 另一台主机中
- 安全代理: 控制对一个对象的访问,可以给不同的用户提供不同级别的使用权限
- 智能代理: 当一个对象被引用时,提供一些额外的操作,如将此对象被调用的次数记录下来等
- 虚拟代理: 如果需要创建一个资源消耗较大的对象,先创建一个消耗相对较小的对象来表示,真实对象只在需要时才会被真正创建。
应用实例
- Spring AOP
- Java的动态代理机制
简单对比一下,代理模式与Java里的动态代理机制,反射的关系:
- 动态代理机制是Java实现代理模式的手段
Java的动态代理机制使得Java开发人员不用手工编写代理类,只要简单地指定一组接口及委托类对象,便能动态获得代理类。
- 动态代理的实现需要用到反射 - RPC
- 图片代理
一个很常见的代理模式的应用实例就是对大图浏览的控制。
用户通过浏览器访问网页时先不加载真实的大图,而是通过代理对象的方法来进行处理,在代理对象的方法中,先使用一个线程向客户端浏览器加载一个小图片,然后在后台使用另一个线程来调用大图片的加载方法将大图片加载到客户端。当需要浏览大图片时,再将大图片在新网页中显示。如果用户在浏览大图时加载工作还没有完成,可以再启动一个线程来显示相应的提示信息。通过代理技术结合多线程编程将真实图片的加载放到后台来操作,不影响前台图片的浏览。
3.2.7 桥接模式
3.3 行为型模式
3.3.1 策略模式
3.3.2 模板方法模式
3.3.3 观察者模式
3.3.4 状态模式
3.3.5 备忘录模式
3.3.6 迭代模式
3.3.7 命令模式
3.3.8 职责链模式
3.3.9 中介者模式
3.3.10 解释器模式
3.3.11 访问者模式
类图
调用时序图
特点
- 优点:
- 缺点:
适用场景
应用实例