设计模式笔记
1. 概论
1.1. 提高系统复用性的设计原则
“开一闭”原则( Open 一 Closed Principle , OCP )
里氏代换原则( Liskov substitution Principle , LSP )
依赖倒转原则 〔 Dependency Inversion principle , DIP )
接口隔离原则( Interfaces Segregation Principle , ISP )
组合聚合复用原则 〔 Composition/Aggregation Principle , CARP )
迪米特法则( Law of Demeter,LOD 〕
- 依赖倒转原则
要依赖于抽象,不要依赖于实现
表述一:应该让框架调用你的代码,而不要由你主动调用框架本身。
表述二:抽象不应依赖于细节,细节应当依赖于抽象。
表述三:要针对接口编程,不要针对实现编程。
昵称:好莱坞原则Hollywood Principle:
它和好莱坞大导演们的做派很相像。导演们常对寻求角色的人说 :
Don’t call us, we’ll call you
“别给我们打电话,我们打给你”
(不要调用我,让我来调用你)
1.2. 设计模式分类
1.2.1. 创建模式
封装动态产生对象的过程和所使用的类的信息,解决系统在创建对象时,抽象化类的实例化过程:
- Abstract Factory 抽象工厂
- Builder 建造者模式
- Factory Method 工厂方法
- Prototype 原型模式
- Singleton 单例
1.2.2. 结构性模式
考虑如何组合类和对象构成较大的结构。
1.结构性类模式:使用继承来组合接口或实现
2.结构性对象模式:对象合成实现新功能。
- Adapter 适配器模式
- Bridge 桥
- Composite 组合模式
1.2.3. 行为模式
主要解决算法和对象之间的责任分配问题。
行为模式描述了:
- 对象或类的模式
- 它们之间的通信模式。
Chain of Responsibility
Command
Interpreter
Iterator
Mediator
2. 创建模式
2.1. 简单工厂
2
使用同一个工厂类
每个工厂类可以有多于-个的工厂方法,分别负责创建不同的产品对象
- 缺点
- 工厂类集中了所有的产品创建逻辑,形成全能类(上帝类 )。当产品类有复杂的多层次等级结构时,工厂类需要判断创建产品时机和种类,增加新的产品导致工厂类的修改。使系统功能扩展困难。
- 由于简单工厂模式使用静态方法,而静态方法无法由子类继承,因此,工厂角色无法形成基于继承的等级结构。
以上缺点在工厂方法模式中得到克服。
@startuml
together {
interface Product
Product <|.d. ConcreteProduct
}
together {
class SimpleFactory {
Product create()
}
}
SimpleFactory -r-> Product
SimpleFactory -> ConcreteProduct
@enduml
@startuml
together {
interface Fruit {
void grow()
void harvest()
void plant()
}
class Grape {
void grow()
void harvest()
void plant()
--
boolean seedless()
}
class Apple {
void grow()
void harvest()
void plant()
--
int treeAge()
}
class Strawberry {
void grow()
void harvest()
void plant()
}
Fruit <|.d. Grape
Fruit <|.d. Apple
Fruit <|.d. Strawberry
}
together {
class FruitGradener {
void plant()
}
}
FruitGradener -r-> Fruit
FruitGradener --> Grape
FruitGradener --> Apple
FruitGradener --> Strawberry
@enduml
- 举例
DateFormat
XMLReaderFactory
2.2. 工厂方法
3
和简单工厂模式区别 :多态性
工厂类不再负责所有的产品的创建,而是将具体创建的工作交给子类
@startuml
together {
interface Product
Product <|.d. ConcreteProduct
}
together {
class Factory {
Product create()
}
class ConcreteFactory {
Product create()
}
ConcreteFactory .u.|> Factory
}
Factory -r-> Product
ConcreteFactory -r> ConcreteProduct
@enduml
@startuml
together {
interface Fruit {
void grow()
void harvest()
void plant()
}
class Grape {
void grow()
void harvest()
void plant()
--
boolean seedless()
}
class Apple {
void grow()
void harvest()
void plant()
--
int treeAge()
}
class Strawberry {
void grow()
void harvest()
void plant()
}
Fruit <|.d. Grape
Fruit <|.d. Apple
Fruit <|.d. Strawberry
}
together {
class FruitGradener {
void plant()
}
class GrapeGradener {
void plant()
}
class AppleGradener {
void plant()
}
class StrawberryGradener {
void plant()
}
FruitGradener <|.d. GrapeGradener
FruitGradener <|.d. AppleGradener
FruitGradener <|.d. StrawberryGradener
}
FruitGradener -r-> Fruit
GrapeGradener -r-> Grape
AppleGradener -r-> Apple
StrawberryGradener -r-> Strawberry
@enduml
2.3. 抽象工厂
4
在不指明类型的情况下,提供一个创建相联系或相依赖的对象族接口
@startuml
together {
interface ProductA
ProductA <|.d. ConcreteProductA
}
together {
interface ProductB
ProductB <|.d. ConcreteProductB
}
together {
class Factory {
Product create()
}
class ConcreteFactory {
Product create()
}
ConcreteFactory .u.|> Factory
}
Factory -r-> ProductA
Factory -r-> ProductB
ConcreteFactory -r> ConcreteProductA
ConcreteFactory -r> ConcreteProductB
@enduml
@startuml
together {
interface Window
Window <|.d. PMWindow
Window <|.d. MotifWindow
}
together {
interface ScrollBar
ScrollBar <|.d. PMScrollBar
ScrollBar <|.d. MotifScrollBar
}
together {
interface WidgetFactory {
ScrollBar createScrollBar()
Window createWindow()
}
class PMWidgetFactory {
PMScrollBar createScrollBar()
PMWindow createWindow()
}
class MotifWidgetFactory {
MotifScrollBar createScrollBar()
MotifWindow createWindow()
}
WidgetFactory .d.|> PMWidgetFactory
WidgetFactory .d.|> MotifWidgetFactory
}
together {
Class Client
}
WidgetFactory -r-> Window
WidgetFactory -r-> ScrollBar
PMWidgetFactory -r> PMScrollBar
PMWidgetFactory -r> PMWindow
MotifWidgetFactory -r> MotifScrollBar
MotifWidgetFactory -r> MotifWindow
Client -d->WidgetFactory
Client -d->Window
Client -d->ScrollBar
@enduml
2.4. 建造者模式
5
用过一个专用的Builder对象,封装对象的创建过程,有几个好处
- 更加的清晰易懂。
- 相比于若干个重载的构造函数。
- 避免对象处于不一致的状态。
- 如果用若干set方法,可能一个对象在还没有全部set完成就被外部对象使用了。
- 可以简化不可变对象的创建过程。
- 不用重载若干个构造函数。
@startuml
class Director {
}
class Builder {
buildPart1()
buildPart2()
retrieveResult()
}
class ConcreteBuilder {
buildPart1()
buildPart2()
retrieveResult()
}
class Product {
}
Director -r-> Builder: builder
ConcreteBuilder -u-|> Builder
ConcreteBuilder -r-> Product: create
@enduml
2.5. 原型模式
6
使用原型模式创建对象比直接new一个对象在性能上要好的多,因为Object类的clone方法是一个本地方法,它直接操作内存中的二进制流,特别是复制大对象时,性能的差别非常明显。
使用原型模式的另一个好处是简化对象的创建,使得创建对象就像我们在编辑文档时的复制粘贴一样简单。
@startuml
class Client {
}
interface Prototype {
clone()
}
class ConcretePrototype {
clone()
}
ConcretePrototype .u.|> Prototype
Client -r-> Prototype: prototype
@enduml
3. 结构模式
3.1. 单例模式
-
意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
-
主要解决:一个全局使用的类频繁地创建与销毁。
-
何时使用:当您想控制实例数目,节省系统资源的时候。
-
如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
-
关键代码:构造函数是私有的。
-
应用实例:
- 1、一个班级只有一个班主任。
- 2、Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。
- 3、一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。
-
优点:
- 1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
- 2、避免对资源的多重占用(比如写文件操作)。
-
缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
-
使用场景:
- 1、要求生产唯一序列号。
- 2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
- 3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。
-
注意事项:getInstance() 方法中需要使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成 instance 被多次实例化。
@startuml
class Singleton {
private Singleton instance
public Singleton getInstance()
}
Singleton --o Singleton
@enduml
3.2. 适配器模式
将一种接口转换成另一种接口
@startuml
interface Target {
request()
}
class Adapter {
request()
}
class Adaptee {
otherKindRequest()
}
Client-r->Target
Target<|-d-Adapter
Adapter-r->Adaptee: adapt
@enduml
@startuml
class Computer {
read(SD sd)
}
interface SD {
}
class SDAdaptTF {
private TF tf
}
class TF {
}
Computer-r->SD
SD<|-d-SDAdaptTF
SDAdaptTF-r->TF: adapt
@enduml
3.3. 桥接模式
意图:将抽象部分与实现部分分离,使它们都可以独立的变化。
主要解决:在有多种可能会变化的情况下,用继承会造成类爆炸问题,扩展起来不灵活。
何时使用:实现系统可能有多个角度分类,每一种角度都可能变化。
如何解决:把这种多角度分类分离出来,让它们独立变化,减少它们之间耦合。
关键代码:抽象类依赖实现类。
应用实例: 1、猪八戒从天蓬元帅转世投胎到猪,转世投胎的机制将尘世划分为两个等级,即:灵魂和肉体,前者相当于抽象化,后者相当于实现化。生灵通过功能的委派,调用肉体对象的功能,使得生灵可以动态地选择。 2、墙上的开关,可以看到的开关是抽象的,不用管里面具体怎么实现的。
优点: 1、抽象和实现的分离。 2、优秀的扩展能力。 3、实现细节对客户透明。
缺点:桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。
使用场景: 1、如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。 2、对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。 3、一个类存在两个独立变化的维度,且这两个维度都需要进行扩展。
注意事项:对于两个独立变化的维度,使用桥接模式再适合不过了。
@startuml
Abstraction o-r- Implementor
Abstraction <|-d- RefinedAbstraction
Implementor <|-d- ConcreteImplementor
@enduml
参考[^bridge_model]
3.4. 装饰者模式
是你还有你,一些拜托你
@startuml
VisualComponent <.u. TextView
VisualComponent <.u. Decorator
Decorator --> VisualComponent
Decorator <.u. ScrollDecorator
Decorator <.u. BorderDecorator
@enduml
3.5. 组合模式
用来表示树形结构
@startuml
Node o-- Node: has many of
@enduml
@startuml
class Graphic {
draw()
add(Graphic)
remove(Graphic)
getChild(int)
}
class Line {
draw()
}
class Rectangle {
draw()
}
class Text {
draw()
}
class Picture {
draw()
add(Graphic)
remove(Graphic)
getChild(int)
}
Graphic <|-d- Line
Graphic o-d- Line
Graphic <|-d- Rectangle
Graphic o-d- Rectangle
Graphic <|-d- Text
Graphic o-d- Text
Graphic <|-d- Picture
Graphic o-d- Picture
@enduml
3.6. 外观模式
为复杂的子系统创建一个简单的接口
@startuml
Client -d-> Facade
Facade -d-> SystemA
Facade -d-> SystemB
Facade -d-> SystemC
病人 -d-> 医院.接待员
namespace 医院 {
接待员 -d-> 门诊
接待员 -d-> 挂号
接待员 -d-> 化验
接待员 -d-> 取药
}
@enduml
参考8
3.7. 享元模式
意图:运用共享技术有效地支持大量细粒度的对象。
主要解决:在有大量对象时,有可能会造成内存溢出,我们把其中共同的部分抽象出来,如果有相同的业务请求,直接返回在内存中已有的对象,避免重新创建。
何时使用: 1、系统中有大量对象。 2、这些对象消耗大量内存。 3、这些对象的状态大部分可以外部化。 4、这些对象可以按照内蕴状态分为很多组,当把外蕴对象从对象中剔除出来时,每一组对象都可以用一个对象来代替。 5、系统不依赖于这些对象身份,这些对象是不可分辨的。
如何解决:用唯一标识码判断,如果在内存中有,则返回这个唯一标识码所标识的对象。
关键代码:用 HashMap 存储这些对象。
应用实例: 1、JAVA 中的 String,如果有则返回,如果没有则创建一个字符串保存在字符串缓存池里面。 2、数据库的数据池。
优点:大大减少对象的创建,降低系统的内存,使效率提高。
缺点:提高了系统的复杂度,需要分离出外部状态和内部状态,而且外部状态具有固有化的性质,不应该随着内部状态的变化而变化,否则会造成系统的混乱。
使用场景: 1、系统有大量相似对象。 2、需要缓冲池的场景。
注意事项: 1、注意划分外部状态和内部状态,否则可能会引起线程安全问题。 2、这些类必须有一个工厂对象加以控制。
@startuml
class FlyWeightPatternDemo {
main()
getRandomColor()
getRandomX()
getRandomY()
}
class ShapeFactory {
circleMap: HashMap
getCircle()
}
class shape.Shape {
draw()
}
class shape.Circle {
x, y, radius: int
}
FlyWeightPatternDemo -d-> ShapeFactory
ShapeFactory -r-> shape.Shape
namespace shape {
Shape <|-d- Circle
}
@enduml
参考:[^fly_weight][^fly_weight2]
3.8. 代理模式
意图:为其他对象提供一种代理以控制对这个对象的访问。
主要解决:在直接访问对象时带来的问题,比如说:要访问的对象在远程的机器上。在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层。
何时使用:想在访问一个类时做一些控制。
如何解决:增加中间层。
关键代码:实现与被代理类组合。
应用实例: 1、Windows 里面的快捷方式。 2、猪八戒去找高翠兰结果是孙悟空变的,可以这样理解:把高翠兰的外貌抽象出来,高翠兰本人和孙悟空都实现了这个接口,猪八戒访问高翠兰的时候看不出来这个是孙悟空,所以说孙悟空是高翠兰代理类。 3、买火车票不一定在火车站买,也可以去代售点。 4、一张支票或银行存单是账户中资金的代理。支票在市场交易中用来代替现金,并提供对签发人账号上资金的控制。 5、spring aop。
优点: 1、职责清晰。 2、高扩展性。 3、智能化。
缺点: 1、由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。 2、实现代理模式需要额外的工作,有些代理模式的实现非常复杂。
使用场景:按职责来划分,通常有以下使用场景: 1、远程代理。 2、虚拟代理。 3、Copy-on-Write 代理。 4、保护(Protect or Access)代理。 5、Cache代理。 6、防火墙(Firewall)代理。 7、同步化(Synchronization)代理。 8、智能引用(Smart Reference)代理。
注意事项: 1、和适配器模式的区别:适配器模式主要改变所考虑对象的接口,而代理模式不能改变所代理类的接口。 2、和装饰器模式的区别:装饰器模式为了增强功能,而代理模式是为了加以控制。
4. 行为模式
4.1. 责任链模式
意图:避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。
主要解决:职责链上的处理者负责处理请求,客户只需要将请求发送到职责链上即可,无须关心请求的处理细节和请求的传递,所以职责链将请求的发送者和请求的处理者解耦了。
何时使用:在处理消息的时候以过滤很多道。
如何解决:拦截的类都实现统一接口。
关键代码:Handler 里面聚合它自己,在 HandlerRequest 里判断是否合适,如果没达到条件则向下传递,向谁传递之前 set 进去。
应用实例: 1、红楼梦中的"击鼓传花"。 2、JS 中的事件冒泡。 3、JAVA WEB 中 Apache Tomcat 对 Encoding 的处理,Struts2 的拦截器,jsp servlet 的 Filter。
优点: 1、降低耦合度。它将请求的发送者和接收者解耦。 2、简化了对象。使得对象不需要知道链的结构。 3、增强给对象指派职责的灵活性。通过改变链内的成员或者调动它们的次序,允许动态地新增或者删除责任。 4、增加新的请求处理类很方便。
缺点: 1、不能保证请求一定被接收。 2、系统性能将受到一定影响,而且在进行代码调试时不太方便,可能会造成循环调用。 3、可能不容易观察运行时的特征,有碍于除错。
使用场景: 1、有多个对象可以处理同一个请求,具体哪个对象处理该请求由运行时刻自动确定。 2、在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。 3、可动态指定一组对象处理请求。
注意事项:在 JAVA WEB 中遇到很多应用。
@startuml
Handler --> Handler: successor
Handler <|-d- ConcreteHandler1
Handler <|-d- ConcreteHandler2
Client -r-> Handler
@enduml
@startuml
AbstractLogger --> AbstractLogger: nextLogger
AbstractLogger <|-d- ConsoleLogger
AbstractLogger <|-d- ErrorLogger
AbstractLogger <|-d- FileLogger
Client -r-> AbstractLogger
class AbstractLogger {
nextLogger: AbstractLogger
setNextLogger()
logMessage()
}
@enduml
4.2. 命令模式
介绍
意图:将一个请求封装成一个对象,从而使您可以用不同的请求对客户进行参数化。
主要解决:在软件系统中,行为请求者与行为实现者通常是一种紧耦合的关系,但某些场合,比如需要对行为进行记录、撤销或重做、事务等处理时,这种无法抵御变化的紧耦合的设计就不太合适。
何时使用:在某些场合,比如要对行为进行"记录、撤销/重做、事务"等处理,这种无法抵御变化的紧耦合是不合适的。在这种情况下,如何将"行为请求者"与"行为实现者"解耦?将一组行为抽象为对象,可以实现二者之间的松耦合。
如何解决:通过调用者调用接受者执行命令,顺序:调用者→接受者→命令。
关键代码:定义三个角色:1、received 真正的命令执行对象 2、Command 3、invoker 使用命令对象的入口
应用实例:struts 1 中的 action 核心控制器 ActionServlet 只有一个,相当于 Invoker,而模型层的类会随着不同的应用有不同的模型类,相当于具体的 Command。
优点: 1、降低了系统耦合度。 2、新的命令可以很容易添加到系统中去。
缺点:使用命令模式可能会导致某些系统有过多的具体命令类。
使用场景:认为是命令的地方都可以使用命令模式,比如: 1、GUI 中每一个按钮都是一条命令。 2、模拟 CMD。
注意事项:系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作,也可以考虑使用命令模式,见命令模式的扩展。
@startuml
Command <|-d- ConcreteCommand
Receiver -r-> Command
Client -r-> Receiver
@enduml
@startuml
Order <|-d- BuyStockOrder
Order <|-d- SellStockOrder
Broker -r-> Order
Client -r-> Broker
@enduml
4.3. 迭代器模式
意图:提供一种方法顺序访问一个聚合对象中各个元素, 而又无须暴露该对象的内部表示。
主要解决:不同的方式来遍历整个整合对象。
何时使用:遍历一个聚合对象。
如何解决:把在元素之间游走的责任交给迭代器,而不是聚合对象。
关键代码:定义接口:hasNext, next。
应用实例:JAVA 中的 iterator。
优点: 1、它支持以不同的方式遍历一个聚合对象。 2、迭代器简化了聚合类。 3、在同一个聚合上可以有多个遍历。 4、在迭代器模式中,增加新的聚合类和迭代器类都很方便,无须修改原有代码。
缺点:由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。
使用场景: 1、访问一个聚合对象的内容而无须暴露它的内部表示。 2、需要为聚合对象提供多种遍历方式。 3、为遍历不同的聚合结构提供一个统一的接口。
注意事项:迭代器模式就是分离了集合对象的遍历行为,抽象出一个迭代器类来负责,这样既可以做到不暴露集合的内部结构,又可让外部代码透明地访问集合内部的数据。
@startuml
class Container {
{abstract} iterator(): Iterator
}
class MyContainer {
iterator(): Iterator
}
class Iterator {
{abstract} hasNext()
{abstract} next()
}
class ConcreteIterator {
hasNext()
next()
}
Iterator <|-d- ConcreteIterator
Container <|-d- MyContainer
Container -r-> Iterator
MyContainer -r-> ConcreteIterator
Client --> Container
Client --> Iterator
@enduml
4.4. 模板方法模式
在模板模式(Template Pattern)中,一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。这种类型的设计模式属于行为型模式。
介绍
意图:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
主要解决:一些方法通用,却在每一个子类都重新写了这一方法。
何时使用:有一些通用的方法。
如何解决:将这些通用算法抽象出来。
关键代码:在抽象类实现,其他步骤在子类实现。
应用实例: 1、在造房子的时候,地基、走线、水管都一样,只有在建筑的后期才有加壁橱加栅栏等差异。 2、西游记里面菩萨定好的 81 难,这就是一个顶层的逻辑骨架。 3、spring 中对 Hibernate 的支持,将一些已经定好的方法封装起来,比如开启事务、获取 Session、关闭 Session 等,程序员不重复写那些已经规范好的代码,直接丢一个实体就可以保存。
优点: 1、封装不变部分,扩展可变部分。 2、提取公共代码,便于维护。 3、行为由父类控制,子类实现。
缺点:每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大。
使用场景: 1、有多个子类共有的方法,且逻辑相同。 2、重要的、复杂的方法,可以考虑作为模板方法。
注意事项:为防止恶意操作,一般模板方法都加上 final 关键词。
@startuml
class AbstractClass {
+templateMethod()
#primitiveMethod1()
#primitiveMethod2()
}
class ConcreteClassA {
+templateMethod()
#primitiveMethod1()
#primitiveMethod2()
}
class ConcreteClassB {
+templateMethod()
#primitiveMethod1()
#primitiveMethod2()
}
AbstractClass <|-d- ConcreteClassA
AbstractClass <|-d- ConcreteClassB
Client -r-> AbstractClass
@enduml
4.5. 观察者模式
@startuml
class Subject {
attach()
detach()
notify()
}
class ConcreteSubject{
getState()
setState()
}
Subject <|-- ConcreteSubject
class Observer {
update()
}
class ConcreteObserver {
update()
}
Observer <|-- ConcreteObserver
Subject <-l- Observer: subscribe
Subject -r-> Observer: notify
@enduml
4.6. 状态模式
在状态模式(State Pattern)中,类的行为是基于它的状态改变的。这种类型的设计模式属于行为型模式。
在状态模式中,我们创建表示各种状态的对象和一个行为随着状态对象改变而改变的 context 对象。
介绍
意图:允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。
主要解决:对象的行为依赖于它的状态(属性),并且可以根据它的状态改变而改变它的相关行为。
何时使用:代码中包含大量与对象状态有关的条件语句。
如何解决:将各种具体的状态类抽象出来。
关键代码:通常命令模式的接口中只有一个方法。而状态模式的接口中有一个或者多个方法。而且,状态模式的实现类的方法,一般返回值,或者是改变实例变量的值。也就是说,状态模式一般和对象的状态有关。实现类的方法有不同的功能,覆盖接口中的方法。状态模式和命令模式一样,也可以用于消除 if...else 等条件选择语句。
应用实例: 1、打篮球的时候运动员可以有正常状态、不正常状态和超常状态。 2、曾侯乙编钟中,'钟是抽象接口','钟A'等是具体状态,'曾侯乙编钟'是具体环境(Context)。
优点: 1、封装了转换规则。 2、枚举可能的状态,在枚举状态之前需要确定状态种类。 3、将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。 4、允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。 5、可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。
缺点: 1、状态模式的使用必然会增加系统类和对象的个数。 2、状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。 3、状态模式对"开闭原则"的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态,而且修改某个状态类的行为也需修改对应类的源代码。
使用场景: 1、行为随状态改变而改变的场景。 2、条件、分支语句的代替者。
注意事项:在行为受状态约束的时候使用状态模式,而且状态不超过 5 个。
@startuml
interface State {
doAction()
}
class StartState {
doAction()
}
class StopState {
doAction()
}
State <|.d. StartState
State <|.d. StopState
class Context {
state: State
setState()
getState()
}
State <-l- Context
@enduml
[^state_model1]
[^state_model2]
[^state_model3]
4.7. 策略模式
在策略模式(Strategy Pattern)中,一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。
在策略模式中,我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的 context 对象。策略对象改变 context 对象的执行算法。
介绍
意图:定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。
主要解决:在有多种算法相似的情况下,使用 if...else 所带来的复杂和难以维护。
何时使用:一个系统有许多许多类,而区分它们的只是他们直接的行为。
如何解决:将这些算法封装成一个一个的类,任意地替换。
关键代码:实现同一个接口。
应用实例: 1、诸葛亮的锦囊妙计,每一个锦囊就是一个策略。 2、旅行的出游方式,选择骑自行车、坐汽车,每一种旅行方式都是一个策略。 3、JAVA AWT 中的 LayoutManager。
优点: 1、算法可以自由切换。 2、避免使用多重条件判断。 3、扩展性良好。
缺点: 1、策略类会增多。 2、所有策略类都需要对外暴露。
使用场景: 1、如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。 2、一个系统需要动态地在几种算法中选择一种。 3、如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。
注意事项:如果一个系统的策略多于四个,就需要考虑使用混合模式,解决策略类膨胀的问题。
@startuml
interface Strategy {
doOperation()
}
class AddStrategy {
doOperation()
}
class SubtractStrategy {
doAction()
}
class MultiplyStrategy {
doAction()
}
Strategy <|.d. AddStrategy
Strategy <|.d. SubtractStrategy
Strategy <|.d. MultiplyStrategy
class Context {
strategy: Strategy
execute()
}
Strategy <-l- Context
@enduml
4.8. 访问者模式
在访问者模式(Visitor Pattern)中,我们使用了一个访问者类,它改变了元素类的执行算法。通过这种方式,元素的执行算法可以随着访问者改变而改变。这种类型的设计模式属于行为型模式。根据模式,元素对象已接受访问者对象,这样访问者对象就可以处理元素对象上的操作。
介绍
意图:主要将数据结构与数据操作分离。
主要解决:稳定的数据结构和易变的操作耦合问题。
何时使用:需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"这些对象的类,使用访问者模式将这些封装到类中。
如何解决:在被访问的类里面加一个对外提供接待访问者的接口。
关键代码:在数据基础类里面有一个方法接受访问者,将自身引用传入访问者。
应用实例:您在朋友家做客,您是访问者,朋友接受您的访问,您通过朋友的描述,然后对朋友的描述做出一个判断,这就是访问者模式。
优点: 1、符合单一职责原则。 2、优秀的扩展性。 3、灵活性。
缺点: 1、具体元素对访问者公布细节,违反了迪米特原则。 2、具体元素变更比较困难。 3、违反了依赖倒置原则,依赖了具体类,没有依赖抽象。
使用场景: 1、对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作。 2、需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"这些对象的类,也不希望在增加新操作时修改这些类。
注意事项:访问者可以对功能进行统一,可以做报表、UI、拦截器与过滤器。
@startuml
interface Element {
accept(Visitor visitor)
}
class ConcreteElementA {
accept(Visitor visitor)
}
class ConcreteElementB {
accept(Visitor visitor)
}
Element <|-- ConcreteElementA
Element <|-- ConcreteElementB
ObjectStructure -r-> Element
interface Visitor {
visit(ConcreteElementA a)
visit(ConcreteElementB b)
}
class Visitor1 {
visit(ConcreteElementA a)
visit(ConcreteElementB b)
}
class Visitor2 {
visit(ConcreteElementA a)
visit(ConcreteElementB b)
}
Visitor <|-- Visitor1
Visitor <|-- Visitor2
Client --> ObjectStructure
Client --> Visitor
@enduml
4.9. 中介者模式
中介者模式(Mediator Pattern)是用来降低多个对象和类之间的通信复杂性。这种模式提供了一个中介类,该类通常处理不同类之间的通信,并支持松耦合,使代码易于维护。中介者模式属于行为型模式。
介绍
意图:用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
主要解决:对象与对象之间存在大量的关联关系,这样势必会导致系统的结构变得很复杂,同时若一个对象发生改变,我们也需要跟踪与之相关联的对象,同时做出相应的处理。
何时使用:多个类相互耦合,形成了网状结构。
如何解决:将上述网状结构分离为星型结构。
关键代码:对象 Colleague 之间的通信封装到一个类中单独处理。
应用实例: 1、中国加入 WTO 之前是各个国家相互贸易,结构复杂,现在是各个国家通过 WTO 来互相贸易。 2、机场调度系统。 3、MVC 框架,其中C(控制器)就是 M(模型)和 V(视图)的中介者。
优点: 1、降低了类的复杂度,将一对多转化成了一对一。 2、各个类之间的解耦。 3、符合迪米特原则。
缺点:中介者会庞大,变得复杂难以维护。
使用场景: 1、系统中对象之间存在比较复杂的引用关系,导致它们之间的依赖关系结构混乱而且难以复用该对象。 2、想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类。
注意事项:不应当在职责混乱的时候使用。
@startuml
Mediator <|-- ConcreteMediator
Colleague <|-- ConcreteColleague1
Colleague <|-- ConcreteColleague2
Colleague -l-> Mediator
ConcreteMediator -r-> ConcreteColleague1
ConcreteMediator -r-> ConcreteColleague2
@enduml
4.10. 备忘录模式
备忘录模式(Memento Pattern)保存一个对象的某个状态,以便在适当的时候恢复对象。备忘录模式属于行为型模式。
介绍
意图:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。
主要解决:所谓备忘录模式就是在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。
何时使用:很多时候我们总是需要记录一个对象的内部状态,这样做的目的就是为了允许用户取消不确定或者错误的操作,能够恢复到他原先的状态,使得他有"后悔药"可吃。
如何解决:通过一个备忘录类专门存储对象状态。
关键代码:客户不与备忘录类耦合,与备忘录管理类耦合。
应用实例: 1、后悔药。 2、打游戏时的存档。 3、Windows 里的 ctri + z。 4、IE 中的后退。 4、数据库的事务管理。
优点: 1、给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便地回到某个历史的状态。 2、实现了信息的封装,使得用户不需要关心状态的保存细节。
缺点:消耗资源。如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存。
使用场景: 1、需要保存/恢复数据的相关状态场景。 2、提供一个可回滚的操作。
注意事项: 1、为了符合迪米特原则,还要增加一个管理备忘录的类。 2、为了节约内存,可使用原型模式+备忘录模式。
@startuml
class Originator {
getStateFromMemento(Memento memento)
saveStateToMemento()
state: int
}
class Memento {
getState()
state: int
}
class Caretaker {
mementoList: List<Memento>
get(): Memento
add(Memento memento)
}
Originator -r-> Memento
Caretaker -l-> Memento
@enduml
4.11. 解释器模式
解释器模式(Interpreter Pattern)提供了评估语言的语法或表达式的方式,它属于行为型模式。这种模式实现了一个表达式接口,该接口解释一个特定的上下文。这种模式被用在 SQL 解析、符号处理引擎等。
介绍
意图:给定一个语言,定义它的文法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子。
主要解决:对于一些固定文法构建一个解释句子的解释器。
何时使用:如果一种特定类型的问题发生的频率足够高,那么可能就值得将该问题的各个实例表述为一个简单语言中的句子。这样就可以构建一个解释器,该解释器通过解释这些句子来解决该问题。
如何解决:构建语法树,定义终结符与非终结符。
关键代码:构建环境类,包含解释器之外的一些全局信息,一般是 HashMap。
应用实例:编译器、运算表达式计算。
优点: 1、可扩展性比较好,灵活。 2、增加了新的解释表达式的方式。 3、易于实现简单文法。
缺点: 1、可利用场景比较少。 2、对于复杂的文法比较难维护。 3、解释器模式会引起类膨胀。 4、解释器模式采用递归调用方法。
使用场景: 1、可以将一个需要解释执行的语言中的句子表示为一个抽象语法树。 2、一些重复出现的问题可以用一种简单的语言来进行表达。 3、一个简单语法需要解释的场景。
注意事项:可利用场景比较少,JAVA 中如果碰到可以用 expression4J 代替。
@startuml
interface Expression {
interpret()
}
class TerminalExpression {
data
interpret()
}
class AndExpression {
expr1: Expression
expr2: Expression
interpret()
}
class OrExpression {
expr1: Expression
expr2: Expression
interpret()
}
Expression <|-- TerminalExpression
Expression <|-- AndExpression
Expression <|-- OrExpression
@enduml
5. 反模式
5.1. 开发反模式
- The Blob 胖球 上帝类
- Continuous Obsolescence 持续过时
- Lava Flow 岩浆流 Dead Code 死代码
- Ambiguous Viewpoint 模糊视角
- Functional Decomposition 功能分解
- Poltergeist 恶作剧鬼 类增殖
- Boat Anchor 船锚
- Golden Hammer 金锤 鸵鸟政策
- Dead End 死胡同
- Spaghetti Code 面条代码
- Input Kludge 输入拼凑
- Walk though a Mine Field 穿越雷区 Nothing Works
- Cut And Paste Programming 剪贴编程
- Mushroom Managerment 蘑菇管理 Pseudo-Analysis 伪分析