《图说设计模式》读书笔记
图说设计模式 一书的读书笔记
文章目录
看懂UML类图和时序图
UML类图
其中用箭头表示的类之间的关系有以下六种
关系 | 箭头 | 描述 |
---|---|---|
泛化 generalization | 空心箭头实线 | 继承(is-a)非抽象类,A->B = A继承自B |
实现 realize | 空心箭头虚线 | 继承(is-a)抽象类,A->B = A继承自B |
聚合 aggregation | 空心菱形箭头实线 | 表示对象实体之间的关系,A->B = A聚合到B上 = B由A组成 |
组合 composition | 实心菱形箭头实现 | 与聚合同样语义,但是不同的是聚合里整体和部分不是强依赖的,整体不存在了部分仍然存在;而组合里整体不存在了部分也跟着消失 |
关联 association | 直线 | 描述对象间的结构关系,是一种静态的、天然的结构,默认不强调方向。通常实现为成员变量 |
依赖 dependency | 带箭头的虚线 | 描述一个对象在运行时会用到另一个对象的关系,临时性,应该总是保持单向依赖。通常实现为类构造方法以及类方法的传入参数 |
时序图
设计原则
单一职责原则
一个类只负责一个功能领域中的相应职责
专注降低类的复杂度,实现类要职责单一
开放关闭原则
设计要对扩展开放,对修改关闭
所有面向对象原则的核心
关键:系统的抽象化设计
里式替换原则
所有引用基类(父类)的地方都必须能透明地使用其子类的对象、
实现开闭原则的重要方式之一,设计不要破坏继承关系
因此尽量使用基类类型定义对象,而在运行时再确定其子类类型,用子类对象来替换父类对象
依赖倒置原则
抽象不应该依赖于细节,细节应当依赖于抽象
系统抽象化的具体实现,是面向对象设计的主要实现机制之一
要求面向接口编程,不针对具体实现编程
在代码中传递参数时或在关联关系中,尽量引用高层的抽象基类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明、以及数据类型的转换等,而不要用具体类做这些事情
这样一来,如果系统发生变化,只需对抽象层进行扩展,而不必修改系统现有的业务逻辑,从而满足开闭原则的要求
目标:开闭原则,基础:里式代换,手段:依赖倒置
接口隔离原则
使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口
客户需要什么样的接口,就提供什么样的接口,不需要的就删除掉
类之间的依赖关系应建立在最小接口上,也就是说接口要尽量细化,同时接口中的方法应该尽量少,职责要尽可能单一
实际使用时要注意控制接口的粒度,如果太小会导致系统中接口泛滥不利于维护,太大又违反接口隔离原则,灵活性较差。一般而言,接口中仅包含为某一类客户定制的方法即可
迪米特原则(最小知识原则)
一个软件实体应当尽可能少地与其他实体发生相互作用
要求当修改系统的某一个模块时,要尽量少地影响其他模块,从而使类与类之间保持松散的耦合关系,使系统扩展相对容易
“不要和陌生人说话,只与你的直接朋友通信”:
- 一个对象的朋友包括:this;以参数形式传入到当前对象方法中的对象;当前对象的成员对象;如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友;当前对象所创建的对象
- 如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用,如果其中的一个对象需要调用另一个对象的某一个方法的话,可以通过引入一个合理的第三者转发这个调用
- 划分上:应当尽量创建松耦合的类
- 结构设计上:每一个类都应当尽量降低其成员变量和成员函数的访问权限
- 设计上:只要有可能,一个类型应当设计成不变类
- 对其他类的引用上:一个对象对其他对象的引用应当降到最低
组合复用原则
在软件设计中,尽量使用组合/聚合(has-a)而不是继承(is-a)达到代码复用的目的
因为继承使得基类与子类有较强的耦合性,通常情况下基类的内部细节对子类来说是可见的,这样基类的实现细节会暴露给子类,破坏了系统的封装性
创建型模式
对类的实例化过程进行抽象,将对象的创建和对象的使用分离
外界对于这些对象只需要知道它们共同的接口,而不清楚具体的实现细节,使得整个系统的设计更加符合单一职责原则
简单工厂模式
专门定义一个工厂类,通过传入的参数不同返回不同类的实例(被创建的实例通常都具有共同的父类)
角色:
- Factory 工厂:负责实现创建所有实例的内部逻辑
- Product 抽象产品:所创建的所有对象的父类,负责描述所有实例所共有的公共接口
- ConcreteProduct 具体产品角色:创建目标,所有创建的对象都充当这个角色的某个具体类的实例
class Factory {
createProduct(proname) {
if (proname === 'A') {
return new ConcreteProductA()
} else if (proname === 'B') {
return new ConcreteProductB()
}
}
}
分析:
-
要点:当你需要什么,只需要传入一个正确的参数,就可以获取你所需要的对象,而无需知道其创建细节
-
优点:将对象的创建和对象本身业务处理分离,将对象的创建交给专门的工厂类负责
-
最大的问题:工厂类的职责相对过重,增加新的产品需要修改工厂类的判断逻辑,这一点与开闭原则相违背;而且产品较多时工厂方法代码将会非常复杂
-
适用情况:工厂类负责创建的对象比较少;客户端只需知道传入工厂类的参数,对于如何创建对象不关心
(实际操作中还可以把调用时传入的参数存在 xml 等配置文件中,修改参数时无需修改任何源代码)
工厂方法模式
对上述简单工厂模式进行修改,不再设计一个工厂类负责所有产品的创建,而是将具体产品的创建过程交给专门的工厂子类去完成。
即,先定义一个抽象的工厂类,再定义具体的工厂子类生成具体的产品
这种抽象化的结果使这种结构可以在不修改现有抽象/具体工厂类的情况下引进新的产品,只需为新产品新建一个具体工厂类
更符合开闭原则
角色:
- Product 抽象产品
- ConcreteProduct 具体产品
- Factory 抽象工厂
- ConcreteFactory 具体工厂
class ConcreteFactory extends Factory{
factoryMethod() {
return new ConcreteProduct()
}
}
分析:
- 基于工厂角色和产品角色的多态性设计,使工厂可以自主确定创建何种产品对象,而如何创建这个对象的细节则完全封装在具体工厂内部。又被称为多态工厂模式,因为所有的具体工厂类都具有同一抽象父类
- 优点:符合开闭原则,系统中加入新产品时无需修改抽象工厂和抽象产品/客户端/其他的具体工厂和具体产品,只需新增一个具体工厂和具体产品
- 缺点:系统中类的数量会比较多,增加了系统复杂度;引入抽象层增加了理解难度
- 适用情况:
- 一个类不知道它所需要的对象的类,只需知道创建具体产品的工厂类
- 一个类通过其子类来指定创建哪个对象,符合多态性和里式替换原则
- 客户端在使用时可以无需关心是哪一个工厂子类创建产品子类,需要时再动态指定
抽象工厂模式
产品等级结构:产品的继承结构
产品族:由同一个工厂生产的,位于不同产品等级结构中的一组产品
当系统所提供的工厂所需生产的具体产品并不是一个简单的对象,而是多个位于不同产品等级结构中属于不同类型的具体产品时需要使用抽象工厂模式
工厂方法模式针对的是一个产品等级结构,而抽象工厂模式则需要面对多个产品等级结构
角色:
- Product 具体产品
- AbstractProduct 抽象产品
- ConcreteFactory 具体工厂
- AbstractFactory 抽象工厂
class ConcreteFactory1 extends AbstractFactory {
createProductA() {
return new ProductA1()
}
createProductB() {
return new ProductB1()
}
}
class ConcreteFactory2 extends AbstractFactory {
createProductA() {
return new ProductA2()
}
createProductB() {
return new ProductB2()
}
}
(A/B代表一个产品族,抽象工厂可以处理多个产品族,而工厂方法只能处理一个产品族)
分析:
- 优点:隔离了具体类的生成,使得客户并不需要知道什么被创建;每次可以通过具体工厂类创建一个产品族中的多个对象,增加或替换产品族比较方便
- 缺点:增加新的产品等级结构很复杂,需要修改抽象工厂和所有的具体工厂类,对开闭原则的支持呈现倾斜性(即增加新的产品族比较方便,但增加新的产品等级结构很复杂)
- 适用情况:
- 系统中有多个产品族,每次只使用其中某一族;
- 属于同一个产品族的产品将在一起使用;
- 系统提供一个产品类的库,所有的产品以同样的接口出现,从而使客户端不依赖于具体实现
建造者模式
复杂对象(汽车),有很多属性(汽车部件 ),对于用户而言无需知道这些细节,只想使用一辆完整汽车,因此将建造(组合部件)过程外部化到一个称作建造者的对象中
用户只需指定复杂对象的类型和内容就可以构建它们,不需要知道内部的具体构建细节
角色:
- Builder 抽象建造者:定义产品的创建方法和返回方法
- ConcreteBuilder 具体建造者
- Director 指挥者:隔离客户与生产过程,控制产品的生成过程。指挥者针对抽象建造者编程,客户只需要知道具体建造者的类型,即可通过指挥者调用建造者的相关方法,返回一个完整的产品对象
- Product 产品
class ConcreteBuilder extends Builder {
buildPartA() {
m_prod.setA("A Style ")
}
buildPartB() {
m_prod.setB("B Style ")
}
buildPartC() {
m_prod.setC("C Style ")
}
}
class Diector {
construct() {
m_pbuilder.buildPartA()
m_pbuilder.buildPartB()
m_pbuilder.buildPartC()
return m_pbuilder.getResult()
}
setBuilder(builder) {
m_pbuilder = builder
}
}
// 用户调用
const builder = new ConcreteBuilder()
const director = new Diector()
director.setBuilder(builder)
const product = director.construct()
分析:
-
优点:
- 将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象;
- 具体建造者之间相对独立,用户使用不同的具体建造者即可得到不同的产品对象;
- 可以更加精细地控制产品的创建过程;
- 增加新的具体建造者无需修改原有类库的代码,指挥者类针对的是抽象建造者编程,符合开闭原则
-
缺点:
- 建造者模式适用于产品比较相似的情况,产品之间的差异性很大时不适合使用建造者模式;
- 如果产品内部变化复杂,可能会导致需要定义很多具体建造者来实现这种变化,导致系统变得很庞大
-
适用环境:
- 需要生成的产品对象有复杂的内部结构,通常包含多个成员属性;
- 需要生成的产品对象的属性相互依赖,需要指定其生成顺序;
- 对象的创建过程独立于创建该对象的类,引入指挥者;
- 隔离复杂对象的创建和使用,并使得相同的创建过程可以创建不同的产品
-
简化:如果系统中只需一个具体建造者时,可省略抽象建造者,省略指挥者
-
与抽象工厂模式比较:
- 建造者模式返回一个组装好的完成产品,而抽象工厂模式返回一系列相关的产品(产品族);
- 抽象工厂中客户端需要实例化工厂类然后调用工厂方法取得所需对象,建造者模式可以不调用建造者的相关方法,而是通过指挥者类;
- 抽象工厂模式:汽车配件生产工厂,生产一个产品族的产品;建造者模式:汽车组装工厂,返回一辆完整的汽车
单例模式
保证一个类只有一个实例并且这个实例易于被访问:让类自身负责保存它的唯一实例,这个类可以保证没有其他实例被创建,并且它可以提供一个访问该实例的方法
要点:某个类只能有一个实例;它必须自行创建这个实例;它必须自行向整个系统提供这个实例
角色:
- Singleton 单例
// 最简单的单例模式
let timeTool = {
name: '处理时间工具库',
getISODate: function() {},
getUTCDate: function() {}
}
最简单的单例模式实现,用 js 的对象字面量实例化一个对象,因为 let 不允许重复声明所以 timeTool
不能被重新覆盖
// 惰性单例
// 只在需要的时候进行单例的创建
// 如果再次调用,返回的永远是第一次实例化的单例对象
let timeTool = (function() {
let _instance = null
function init() {
// 私有变量
let now = new Date()
// 公用属性和方法
this.name = '处理时间工具库'
this.getISODate = function() {
return now.toISOString()
}
this.getUTCDate = function() {
return now.toUTCString()
}
}
return function() {
if (!_instance) {
_instance = new init()
}
return _instance
}
})()
// singleton in es6
class SingletonApple {
constructor(name, creator, products) {
this.name = name
this.creator = creator
this.products = products
}
static getInstance(name, creator, products) {
if (!this.instance) {
this.instance = new SingletonApple(name, creator, products)
}
return this.instance
}
}
es6 的 export 出的实例即是单例
分析:
- 优点:提供了对唯一实例的受控访问并可以节约系统资源
- 缺点:因为缺少抽象层而难以扩展,且单例类职责过重
结构型模式
描述如何将类或者对象结合在一起形成更大的结构
分为类结构型模式和对象结构型模式
- 类结构型模式:关心类的组合,由多个类可以组合成一个更大的系统,在类结构型模式中一般只存在继承关系和实现关系
- 对象结构型模式:关心类与对象的组合,通过关联关系使得在一个类中定义另一个类的实例对象,然后通过该对象调用其方法。(组合复用原则:在系统中应尽量使用关联关系来替代继承关系)
适配器模式
将一个借口转换成客户希望的另一个接口(现有的类可以满足客户类的功能需要,但接口不一定是客户类所期望的)
适配器的实现就是把客户类的请求转化为对适配者的相应接口的调用
对象适配器:
类适配器:
角色:
- Target 目标抽象类,定义客户需要的接口
- Adapeter 适配器类,定义接口的实际实现
- Adaptee 适配者类(被适配的原对象类)
- Client 客户类
class Adapter extends Target {
constructor(adaptee) {
this.m_pAdaptee = adaptee
// 准确来说,这个 m_pAdaptee 应当定义为私有变量
}
request() {
this.m_pAdaptee.specificRequest()
}
}
分析:
-
优点:
- 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,而无需修改原有代码
- 增加了类的透明性和复用性,将具体的实现封装在适配者类中,对于客户端类来说是透明的,而且提高了适配者的复用性
- 灵活性和扩展性都非常好,通过使用配置文件,可以很方便地更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,完全符合“开闭原则”
- 类适配器:因为适配器是适配者类的子类,因此可以在适配器类中置换一些适配者的方法,使得适配器的灵活性更强
- 对象适配器:一个对象适配器可以把多个不同的适配者适配到同一个目标,也就是说,同一个适配器可以把适配者类和它的子类都适配到目标接口
-
缺点:
- 类适配器:对于不支持多重继承的语言,一次最多只能适配一个适配者类,而且 Target 只能为抽象类不能为具体类,其使用有一定的局限性
- 对象适配器:想置换适配者类的方法不容易
-
扩展:默认适配器模式
先设计一个抽象类实现接口,并为该接口中的每个方法提供一个默认实现(空方法),那么该抽象类的子类可有选择地覆盖父类的某些方法来实现需求,适用于一个接口不想使用其所有的方法的情况
桥接模式
将抽象部分与它的实现部分分离(由原来的继承关系改为关联关系,强关联改为弱关联),使他们都可以独立地变化
角色:
- Abstraction 抽象类:定义一个实现类接口类型的对象并可以维护该对象
- RefinedAbstraction 扩充抽象类:扩充抽象类定义的接口,实现在抽象类中定义的抽象业务方法
- Implementor 实现类接口:仅提供基本操作
- ConcreteImplementor 具体实现类:实现了实现类的接口并具体实现它,在不同的具体实现类中提供基本操作的不同实现
// 由于 JavaScript 不支持抽象类,所以没有 Abstraction 和 Implementor
// 下面实现 RefinedAbstraction 和 ConcreteImplementor
class RefinedAbstraction {
constuctor(imp) {
this.m_pImp = imp
}
operation() {
this.m_pImp.operationImp()
}
}
class ConcreteImplementor {
operationImp() {
// 具体实现
}
}
分析:
-
优点:
- 比如有两个变化维度的系统,采用桥接模式可以提高可扩充性(任意扩展一个维度都不需要修改原有系统);
- 有时类似多继承,但多继承方案违背了类的单一职责原则,复用性比较差,且类的个数非常庞大
-
缺点:增加系统的理解与设计难度,要求针对抽象进行设计与编程
-
适用:
- 抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,则可以用桥接模式使它们在抽象层建立一个关联关系
- 一个类存在两个独立变化的维度,且这两个维度都需要进行扩展
- 系统需要对抽象化角色和实现化角色进行动态耦合
装饰模式
动态地给一个对象增加一些额外的职责,不需要创造更多子类而将对象的功能加以扩展‘
方式与适配器模式相同,把继承关系变成关联关系
角色:
- Component 抽象构件
- ConcreteComponent 具体构件
- Decorator 抽象装饰类
- ConcreteDecorator 具体装饰类
class ConcreteDecorator {
constructor(component) {
this.m_pComponent = component
}
addBehavior() {
//增加新行为
}
operation() {
this.m_pComponent.operation()
this.addBehavior()
}
}
分析:
- 优点:比继承更灵活,可以通过不同的装饰类以及这些装饰类的排列组合,得到很多种不同行为的对象;修改时只需增加新的 ConcreteComponent 和 ConcreteDecorator,符合开闭原则
- 缺点:会产生很多小对象,他们之间的区别在于相互连接的方式不同,而不是它们的类或者属性值不同,增加系统复杂度,加大理解难度;更灵活也就更容易出错,难以排查
- 使用时尽量保持 ConcreteComponent 作为一个轻类,不要包含太多的逻辑和状态,而是通过装饰类对其进行扩展
外观模式
外部与一个子系统的通信必须通过一个统一的外观对象进行,为子系统中的一组接口提供了一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用
角色:
- Facade 外观
- SubSystem 子系统
class Facade {
constructor() {
this.m_SystemA = new SystemA()
this.m_SystemB = new SystemB()
this.m_SystemC = new SystemC()
}
wrapOpration() {
this.m_SystemA.operationA()
this.m_SystemB.operationB()
this.m_SystemC.operationC()
}
}
分析:
- 划分子系统符合单一职责原则,而为了使子系统间的通信和相互依赖关系达到最小,就可以引入一个外观对象
- 是迪米特法则的体现,降低原有系统的复杂度,降低客户类与子系统类的耦合度
- 优点:
- 对客户屏蔽子系统组件,客户代码变得简单
- 子系统与客户松散耦合
- 降低大型软件系统中的编译依赖性,简化了系统在不同平台之间的移植过程
- 缺点:
- 不能很好地限制客户使用子系统类,而如果对客户访问子系统类做太多的限制则减少了可变性和灵活性
- 在不引入抽象外观类的情况下,增加新的子系统可能需要修改外观类或客户端的源代码,违背了开闭原则
- 扩展:
- 多个外观类
- 不要试图通过外观类为子系统增加新行为
享元模式
通过共享技术有效地支持大量细粒度对象的复用,系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用
角色:
- Flyweight 抽象享元类
- ConcreteFlyweight 具体享元类
- UnsharedConcreteFlyweight 非共享具体享元类
- FlyweightFactory 享元工厂类:维护一个享元池用于存储具有相同内部状态的享元对象
class FlyweightFactory {
getFlyweight(type) {
// 伪代码...
if (this.m_mpFlyweight.find(type)) {
return exist one
} else {
const fw = new ConcreteFlyweight(type)
this.m_mpFlyweight.push(fw)
return fw
}
}
}
class ConcreteFlyweight extends Flyweight {
constructor(state) {
intrinsicState = state
}
operation() {
// do operation
}
}
分析:
- 核心在于享元工厂类(维护的享元池)
- 以共享的方式高效地支持大量的细粒度对象,享元对象能做到共享的关键是区分内部状态和外部状态
- 内部状态是存储在享元对象内部并且不会随环境改变而改变的状态,因此内部状态可以共享
- 外部状态是随环境改变而改变的、不可以共享的状态。享元对象的外部状态必须由客户端保存,并在享元对象被创建之后,在需要使用时再传入到享元对象内部,一个外部状态与另一个外部状态之间是相互独立的
- 优点:
- 极大减少内存中对象的数量,使得相同对象或相似对象在内存中只保存一份
- 外部状态相对独立,不会影响内部状态,从而使得享元对象可以在不同环境中被共享
- 缺点:
- 使得系统更加复杂,需要分离出内部状态和外部状态
- 读取外部状态使得运行时间变长
- 适用:
- 一个系统有大量相同或者相似的对象,这类对象的大量适用造成内存的大量耗费
- 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中
- 因为维护享元池会耗费资源,所以应当在多次重复使用享元对象时才值得使用享元模式
- 扩展:
- 单纯享元模式:所有享元对象都是可以共享的,不存在非共享具体享元类
- 复合享元模式:将一些单纯享元使用组合模式加以组合,可以形成复合享元对象,这样的复合享元对象本身不能共享,但它们可以分解成单纯享元对象,而后者则可以共享
- 享元工厂类通常只有唯一一个,可以设计成单例
代理模式
给某一个对象提供一个代理,并由代理对象控制对原对象的引用
代理对象可以在客户端和目标对象之间起到中介的作用,并且可以通过代理对象去掉客户不能看到的内容和服务,或者添加客户需要的额外服务
角色:
- Subject 抽象主题
- Proxy 代理主题
- RealSubject 真实主题
class Proxy extends Subject {
constructor() {
this.m_pRealSubject = new RealSubject()
}
request() {
this.preRequest() // 一些其他行为
this.m_pRealSubject.request() // 真正和原对象交互
this.afterRequest() // 一些其他行为
}
}
分析:
- 优点:协调调用者和被调用者,在一定程度上降低了系统的耦合度
- 缺点:有些类型的代理模式可能会造成请求的处理速度变慢;有些代理的实现比较复杂,需要额外的工作
- 使用:
- 远程(remote)代理:为一个位于不同的地址空间的对象提供一个本地的代理对象,这个不同的地址空间可以是在同一台主机中,也可是在另一台主机中
- 虚拟(virtual)代理:如果需要创建一个资源消耗较大的对象,先创建一个消耗相对较小的对象来表示,真实对象只在需要时才会被真正创建
- Copy-on-Write代理:是虚拟代理的一种,把复制(克隆)操作延迟到只有在客户端真正需要时才执行(因为深克隆是个开销较大的操作,可以使用这个代理让这个操作延迟到只有对象被用到时才被克隆)
- 保护(Protect or Access)代理:控制对一个对象的访问,可以给不同的用户提供不同级别的使用权限
- 缓冲(Cache)代理:为某一个目标操作的结果提供临时的存储空间,以便多个客户端可以共享这些结果
- 防火墙(Firewall)代理:保护目标不让恶意用户接近
- 同步化(Synchronization)代理:使几个用户能够同时使用一个对象而没有冲突
- 智能引用(Smart Reference)代理:当一个对象被引用时,提供一些额外的操作,如将此对象被调用的次数记录下来等
行为型模式
命令模式
将发送者和接受者完全解耦,没有直接引用关系,发送请求的对象只需要知道如何发送请求,而不必知道如何完成请求
将一个请求封装为一个对象,从而使我们可用不同的请求对客户进行参数化
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sLuZLI5S-1597732835874)(https://design-patterns.readthedocs.io/zh_CN/latest/_images/Command.jpg)]
角色:
- Command 抽象命令类
- ConcreteCommand 具体命令类
- Invoker 调用者
- Receiver 接受者
- Client 客户类
class Receiver {
action() {
// 定义接收到命令的行为
}
}
class Invoker {
constructor(command) {
// 这里的 command 是抽象命令接口,发送者针对抽象接口编程
this.m_pCommand = command
}
call() {
// 发出命令
this.m_pCommand.execute()
}
}
class ConcreteCommand extends Command {
constructor(receiver) {
this.m_pReceiver = receiver
}
execute() {
this.m_pReceiver.action()
}
}
分析:
- 本质是对命令进行封装,将发出命令的责任和执行命令的责任分割开。
- 优点:
- 降低系统耦合度
- 新的命令可以很容易地加入到系统中,可以比较容易地设计一个命令队列和宏命令(组合命令),可以方便地实现对请求的 undo 和 redo
- 缺点:可能会导致系统中有过多的具体命令类
- 适用:
- 系统需要将请求调用者和请求接受者解耦,使得调用者和接受者不直接交互
- 系统需要在不同的时间指定请求、将请求排队、执行请求
- 系统需要支持命令的撤掉和恢复操作
- 系统需要将一组操作组合在一起,即支持宏命令
- 扩展:宏命令
中介者模式
用一个中介对象来封装一系列的对象交互,中介者使得各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互
角色:
- Mediator 抽象中介者
- ConcreteMediator 具体中介者
- Colleague 抽象同事类
- ConcreteColleague 具体同事类
class ConcreteMediator extends Mediator {
// 一些伪代码
operation(to, msg) {
this.m_mpColleague.find(to).receivemsg(msg)
}
registered(colleague) {
if (!this.m_mpColleague.find(colleague)) {
this.m_mpColleague.push(colleague)
colleague.setMediator(this)
}
}
}
class ConcreteColleague extends Colleague {
setMediator(mediator) {
this.m_pMediator = mediator
}
sendmsg(to, msg) {
this.m_pMediator.operation(to, msg)
}
receviemsg(msg) {
// 收到消息的操作
}
}
分析:
- 中介者模式可以使对象之间的关系数量急剧减少,中介者承担两方面职责:中转作用(结构性),协调作用(行为型)
- 优点:简化对象之间交互,各同事解耦,减少子类生成,可以简化各同事类的设计和实现
- 缺点:在具体中介者类中包含了同事之间的交互细节,可能会导致具体中介者类非常复杂,使得系统难以维护
- 适用:
- 系统中对象存在复杂的引用关系,产生的相互依赖关系结构混乱且难以理解
- 一个对象由于引用了其他很多对象并直接和这些对象通信,导致难以复用该对象
- 想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类,可以通过引入中介者类实现,在中介者中定义对象
- 交互的公共行为,如果需要改变行为则可以增加新的中介者类
- 扩展:
- 例如 MVC 结构中的 Controller,负责控制视图对象 View 和模型对象 Model 之间的交互
- 符合迪米特法则
观察者模式
建立一种对象与对象之间的依赖关系(一对多),一个对象发生改变时将自动通知其他对象,其他对象将相应做出反应
角色:
- Subject 目标
- ConcreteSubject 具体目标
- Observer 观察者
- ConcreteObserver 具体观察者
class ConcreteSubject extends Subject {
attach(observer) {
this.m_vtObj.push(observer)
}
detach(observer) {
this.m_vtObj.splice(this.m_vtObj.findIndex(e => e === observer), 1)
}
notify() {
this.m_vtObj.forEach(item => item.update(this))
}
setState() {
// 改变自身状态
this.notify()
}
getState() {
// 方便 observer 调用取得状态
}
}
class ConcretObserver extends Observer {
constructor(name) {
this.m_objName = name
}
update(subject) {
this.m_observerState = subject.getState()
// 一些基于 this.m_objName 和 this.m_observerState 的操作
}
}
分析:
- 一个目标可以有多个观察者(用一个数组存),一旦目标状态发生改变,就遍历数组通知所有观察者;观察者收到通知进行自身更新
- 优点:
- 表示层(observer)与数据逻辑层(subject)的分离,定义了稳定的消息更新传递机制,抽象了更新接口,使得可以由各种各样不同的表示层作为具体观察者角色
- 观察目标和观察者之间抽象耦合
- 支持广播通信
- 符合开闭原则
- 缺点:
- 如果一个 subject 有很多直接和间接的 observer 的话,通知所有 observer 会花费很多时间
- 如果 subject 和 observer 之间存在循环依赖,会发生循环调用
- 观察者模式没有相应的机制让观察者知道目标对象是怎样发生变化的,而仅仅能知道发生了变化
- 适用:
- 一个抽象模型有两个方面,其中一个方面依赖于另一个方面
- 一个对象的改变将导致其他一个或多个对象也发生改变,而不知道具体有多少对象将发生改变
- 一个对象必须通知其他对象,而并不知道这些对象是谁
- 需要在系统中创建一个触发链,A对象的行为将影响B,B对象的行为将影响C…
- 扩展:
- MVC:model - subject(被观察的目标),view - observer(观察者),controller - 充当两者之间的中介者 mediator,避免两者直接引用
状态模式
允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类
角色:
- Context 环境类:拥有状态的对象,有时候可以充当状态管理器的角色,可以在环境类中对状态进行切换操作。针对抽象状态类进行编程
- State 抽象状态类:专门表示对象的状态,而对象的每一种具体状态类都继承了该类,并在不同具体状态类中实现了不同状态的行为,包括各种状态之间的转换。环境类对象在其内部状态改变时可以改变它的行为,对象看起来似乎修改了它的类,而实际上是由于切换到不同的具体状态类实现的
- ConcreteState 具体状态类
class Context {
constructor() {
this.m_pState = new ConcreteState(this)
}
changeState(state) {
this.m_pState = state
}
request() {
this.m_pState.handle()
}
}
class ConcreteState extends State {
constructor(context) {
this.context = context
}
handle() {
// doing something in state
// then change state
context.changeState(new ConcreteStateB(this.context))
}
}
分析:
- 优点:
- 封装了状态转换规则,状态转换逻辑与状态对象合成一体
- 枚举可能的状态
- 将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象行为
- 可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数
- 缺点:
- 必然会增加系统类和对象的个数
- 结构和实现都较为复杂,如果使用不当将导致程序结构和代码的混乱
- 不符合开闭原则,新增状态需要修改那些负责状态转换的代码,而且修改某个状态类的行为也需要修改对应类的代码
- 适用:
- 对象的行为依赖于它的状态并且可以根据它的状态改变而改变它的相关行为
- 代码中包含大量与对象状态有关的条件语句,这些条件语句的出现会导致代码的可维护性和灵活性变差,不能方便地增加和删除状态,使客户类与类库之间的耦合增强;在这些条件语句中包含了对象的行为,而且这些条件对应于对象的各种状态
- 扩展:
- 多个环境对象共享一个或多个状态对象,需要将这些状态对象定义为环境的静态成员对象
- 简单状态模式:状态相互独立,状态之间无需进行转换。此时每个状态类都封装与状态相关的操作,而无需关心状态的切换,客户端直接实例状态类然后把状态对象设置到环境类中即可。遵循开闭原则,客户端针对抽象状态类进行编程,增加新的状态类对原有系统也不造成任何影响
- 可切换状态的状态模式:在切换状态时需要在具体状态类内部调用 context.changeState(),因此状态类与环境类之间还存在关联关系或依赖关系,通过在状态类中引用环境类的对象实现调用。这种模式下增加新的状态类可能会需要修改其他状态类甚至环境类的源代码,否则无法切换
策略模式
定义一系列算法,将每一个算法封装起来,并让它们可以相互替换,策略模式让算法独立于使用它的客户而变化
角色:
- Context 环境类
- Strategy 抽象策略类
- ConcreteStrategy 具体策略类
class Context {
algorithm() {
this.m_pStrategy.algorithm()
}
setStrategy(strategy) {
this.m_pStrategy = strategy
}
}
class ConcreteStrategy extends Strategy {
algorithm() {
// 具体算法
}
}
分析:
- 把算法的责任和算法本身分隔开,委派给不同的对象管理。“准备一组算法,并将每一个算法封装起来,使得它们可以互换”
- 客户端自己决定在什么情况下使用什么具体策略角色,策略模式仅仅封装算法
- 优点:
- 完美支持“开闭原则”,用户可以在不修改原有系统的基础上选择算法或行为,也可以灵活地增加新的算法或行为
- 提供管理相关算法族的办法
- 提供可以替换继承关系的办法
- 可以避免使用多重条件转移语句
- 缺点:
- 客户端必须知道所有的策略类,并自行决定使用哪一个策略类
- 将造成很多策略类,可以通过享元模式在一定程度上减少对象的数量
- 适用:
- 如果一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为
- 在具体策略类中封装算法和相关数据结构,提高算法的保密性和安全性
- 扩展:策略模式与状态模式
- 可以通过环境类状态的个数来决定是使用策略模式还是状态模式
- 策略模式的环境类自己选择一个具体策略类,具体策略类无需关心环境类;而状态模式的环境类由于外在因素需要放进一个具体状态中,以便通过其方法实现状态的切换,因此环境类和状态类之间存在一种双向的关联关系
- 策略模式客户端需要知道所选的具体策略是哪一个;状态模式客户端无需关心具体状态,环境类的状态会根据用户的操作自动转换
- 如果系统中某个类的对象存在多种状态,不同状态下行为有差异,而且这些状态之间可以发生转换时使用状态模式;如果系统中某个类的某一行为存在多种实现方式,而且这些实现方式可以互换时使用策略模式