设计模式

设计模式

概念篇

容易维护、容易扩展、容易复用、紧耦合vs松耦合

紧耦合——如果我要在原来的业务代码上改需求,那么我要了解原来的业务代码,在原来的代码中修改

松耦合——我不需要在原来的业务代码中改,(比如人家以jar包的形式给你),我只需要继承或实现接口的方式加入新的业务逻辑

UML类图

依赖关系 虚线加箭头 动物与水氧气

继承 实线加空心三角形

接口实现 虚线加空心三角形

聚合关系 空心菱形和箭头的直线 整体包含部分的关系 拥有 雁群 与大雁

组合关系 实心菱形和箭头的直线 整体包含部分且部分无法单独存在的关系 头与嘴

关联关系 单向关联 双向关联 自关联 以成员变量的形式存在彼此

六大原则

1)单一职责原则

就一个类而言,应该仅有一个引起它变化的原因

2)开闭原则--开放—封闭原则

软件实体(类、模块、方法)可以扩展,但不能修改

特征: 对扩展是开放的,对修改是关闭的

“开放-封闭原则是面向对象设计的核心所在。遵循这个原则可以带来面向对象技术所声称的巨大好处,也就是可维护、可扩展、可复用、灵活性好。开发人员应该仅对程序中呈现出频繁变化的那些部分做出抽象,然而,对于应用程序中的每个部分都刻意地进行抽象同样不是一个好主意。拒绝不成熟的抽象和抽象本身一样重要。切记,切记。”

对频繁出现变化的部分抽象,而不是对每一个部分刻意抽象。拒绝不成熟的抽象和抽象本身一样重要

  • 易维护
  • 可扩展
  • 可复用
  • 灵活性好
PC电脑:
  主板、CPU、内存、显卡、硬盘
  单一职责原则——每个模块(类)只负责一项具体的业务功能——先排查内存是否有问题,而不会说先去排查CPU、硬盘
  开放-封闭原则——对修改关闭,对扩展开放   ——内存不足,咋办,只要插槽还有,接着加内存
  
  易维护——哪里出问题,直接定位,比如蓝屏一般都立马想到先排查内存是否有问题,而不会说先去排查CPU、硬盘等
  可扩展——内存不足,咋办,只要插槽还有,接着加内存
  可复用——CPU、内存拿到别的机器照样用
  灵活性好——CPU那么一个小小的东西,包含了太多的元器件,也就是说具体的细节,外边是看不到的,只留下了针脚式的接口,
     不管什么样的主板,只要符合接口要求,插上就能用——高内聚、低耦合
  

3)依赖倒转原则

抽象不应该依赖细节,细节应该依赖于抽象

白话:

针对接口编程,不要对实现编程

内存坏了,更换内存即可,不需要把主板也换了,原因无论主板、CPU、内存等都是针对接口设计,

而不是针对实现来设计,如果某个内存对应某个品牌的主板,那么换内存就要换主板,你说这个品牌的内存会销量的好吗

PC电脑的发展,和面向对象的思想完全类似,掌握规律,才能拔得先机

A高层模块不应该依赖于低层模块,两个都应该依赖于抽象

B抽象不应该依赖于细节,细节应该依赖于抽象

依赖倒转原则——简单的来说,谁也不要依赖谁,除了约定的接口,(至于具体的实现)都可以灵活自如

PC电脑中的CPU、内存、显卡与主板谁都不依赖谁,约定接口(即抽象),接口可不管具体实现的细节

收音机——各个元器件高度耦合焊接在一起

4)里氏代换原则

子类型必须能够替换掉它们的父类型

一个软件程序里如果使用了一个父类的话,一定适用于其子类,而且它觉察不出父类和子类的区别,把父类替换成子类,程序

没有任何变化

5)迪米特法则

迪米特法则又叫最少知道原则

定义:如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用,如果其中一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用

迪米特法则首先强调的前提是在类的结构设计上,每一个类都应当尽量降低成员的访问权限

迪米特法则其根本思想,是强调了类之间的松耦合

我们在程序设计时,类之间的耦合越弱,越有利于复用,一个处在弱耦合的类被修改,不会对有关系的类造成波及。

6)合成/聚合复用原则

优先使用对象合成/聚合,而不是类继承

合成:整体与部分的关系 ,部分无法脱离整体单独存活 如:头与嘴, 嘴是头的一部分 即:合成则是一种强的‘拥有’关系,体现了严格的部分和整体的关系,部分和整体的生命周期一样;

聚合:整体与部分的关系,部分可以单独存活 如雁群与大雁 ,汽车与发动机 即:弱的‘拥有’关系,体现的是A对象可以包含B对象,但B对象不是A对象的一部分。

优点:优先使用对象的合成/聚合将有助于你保持每个类被封装,并被集中在单个任务上。这样类和类继承层次会保持较小规模,并且不太可能增长为不可控制的庞然大物

image-20220318184554539

模式篇

1策略模式

  • 定义:它定义了算法家族,分别封装起来,让它们之间可以相互替换,此模式让算法的变化,不会影响到使用的客户。
  • 概述:策略模式封装了变化
  • 应用场景:需要根据条件区分来选择不同的业务规则
  • 优点:1.减少了算法与使用算法的耦合 2.简化了单元测试,每一个策略就是一个单独的类,可随时对某个算法测试,而不需一次测所有的算法

image-20220316163950619

2装饰模式

  • 动态的给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成子类更为灵活
  • 装饰模式是为已有功能动态地添加更多功能的一种方式
  • 优点

​ 把类中的装饰功能从类中搬移去除,这样可以简化原有的类。

​ 有效地把类的核心职责和装饰功能区分开了。而且可以去除相关类中重复的装饰逻辑

  • 使用场景

    当系统需要新功能的时候,是向旧的类中添加新的代码。这些新加的代码通常装饰了原有类的核心职责或主要行为,比如用西装或嘻哈服来装饰小菜,但这种做法的问题在于,它们在主类中加入了新的字段,新的方法和新的逻辑,从而增加了主类的复杂度,就像你起初的那个‘人’类,而这些新加入的东西仅仅是为了满足一些只在某种特定情况下才会执行的特殊行为的需要。而装饰模式却提供了一个非常好的解决方案,它把每个要装饰的功能放在单独的类中,并让这个类包装它所要装饰的对象,因此,当需要执行特殊行为时,客户代码就可以在运行时根据需要有选择地、按顺序地使用装饰功能包装对象了

    • 使用注意
    • 装饰模式的装饰顺序很重要哦,比如加密数据和过滤词汇都可以是数据持久化前的装饰功能,但若先加密了数据再用过滤功能就会出问题了,最理想的情况,是保证装饰类之间彼此独立,这样它们就可以以任意的顺序进行组合了。

image-20220316170854228

image-20220316171540727

    Person person = new Person("张三");
        DecorateJc jc = new DecorateJc();
        DecorateMz mz = new DecorateMz();
        DecorateXz xz = new DecorateXz();
        jc.decorate(person);
        mz.decorate(jc);
        xz.decorate(mz);
        xz.show();

3代理模式

  • 定义:为其它对象提供一种代理以控制对这个对象的访问

  • 使用场景

    1.远程代理,也就是为一个对象在不同的地址空间提供局部代表,这样可以隐藏一个对象在在于不同地址空间的事实。

    2.虚拟代理,是根据需要创建开销很大的对象。通过它来存放实例化需要很长时间的真实对象

    3.安全代理,用来控制真实对象访问时的权限

    4.智能指引,是指当调用真实的对象时,代理处理另外一些事

image-20220316163824366

4简单工厂

  • 简单工厂模式的最大优点在于工厂类中包含了必要的逻辑判断,根据客户端的选择条件动态实例化相关的类,对于客户端来说,去除了与具体产品的依赖
  • 缺点:不但对扩展开放了,对修改也开放了,这样就违背了开闭原则,于是乎工厂方法来了
  • 利用‘反射’可以解决避免分支判断的问题

image-20220316163651394

5工厂方法

  • 定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类
  • 工厂方法模式实现时,客户端需要决定实例化哪一个工厂来实现运算类,选择判断的问题还是存在的,也就是说,工厂方法把简单工厂的内部逻辑判断移到了客户端代码来进行。你想要加功能,本来是改工厂类的,而现在是修改客户端
  • 利用‘反射’可以解决避免分支判断的问题

image-20220316163604390

6原型模式

  • 用原型实例指定创建对象的各类,并且通过拷贝这些原型创建新的对象。
  • 原型模式其实就是从一个对象再创建另外一个可定制的对象,而且不需知道任何创建的细节。
  • java中提供了Cloneable接口,Object中有一个方法Clone(),这样你就只需要实现这个接口就可以完成原型模式了。
  • 优点:每NEW一次,都需要执行一次构造函数,如果构造函数的执行时间很长,那么多次的执行这个初始化操作就实在是太低效了。一般在初始化的信息不发生变化的情况下,克隆是最好的办法。这既隐藏了对象创建的细节,又对性能是大大的提高。不用重新初始化对象,而是动态地获得对象运行时的状态
  • 浅复制与深复制
    • 如果字段是值类型的,则对该字段执行逐位复制,如果字段是引用类型,则复制引用但不复制引用的对象;因此,原始对象及其复本引用同一对象。什么意思呢,就是说如果你的‘简历’类当中有对象引用,那么引用的对象数据是不会被克隆过来的。
    • 它是浅表复制,所以对于值类型,没什么问题,对引用类型,就只是复制了引用,对引用的对象还是指向了原来的对象
    • ‘浅复制’,被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用都仍然指向原来的对象。但我们可能更需要这样的一种需求,把要复制的对象所引用的对象都复制一遍。
    • 深复制把引用对象的变量指向复制过的新对象,而不是原有的被引用的对象。

image-20220316163432888

7模板方法

  • 定义:定义一个操作中的算法骨架而将一些步骤延迟到子类中,模板方法可以使用子类不改变一个算法的结构即可重新定义该算法的某些特定步骤。
  • 归纳:模板方法就通过把不变的行为搬到超类,去除了子类中的重复代码来体现它的优势
  • 优点: 模板方法模式就是提供了一个很好的代码复用平台
  • 当不变的和可变的行为在方法的子类实现中混合在一起的时候,不变的行为就会在子类中重复出现。我们通过模板方法模式把这些行为搬移到单一的地方,这样就帮助子类摆脱重复的不变行为的纠缠。
  • 场景: Java类库的设计中,通常都会利用模板方法模式提取类库中的公共行为到抽象类中。

image-20220316215748365

image-20220320003411787

8外观模式(Facade)

  • 定义:为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
  • 使用场景

​ 这要分三个阶段来说,首先,在设计初期阶段,应该要有意识的将不同的两个层分离,比如经典的三层架构,就需要考虑在数据访问层和业务逻辑层、业务逻辑层和表示层的层与层之间建立外观Facade,这样可以为复杂的子系统提供一个简单的接口,使得耦合大大降低。其次,在开发阶段,子系统往往因为不断的重构演化而变得越来越复杂,大多数的模式使用时也都会产生很多很小的类,这本是好事,但也给外部调用它们的用户程序带来了使用上的困难,增加外观Facade可以提供一个简单的接口,减少它们之间的依赖。第三,在维护一个遗留的大型系统时,可能这个系统已经非常难以维护和扩展了,但因为它包含非常重要的功能,新的需求开发必须要依赖于它。此时用外观模式Facade也是非常合适的。你可以为新系统开发一个外观Facade类,来提供设计粗糙或高度复杂的遗留代码的比较清晰简单的接口,让新系统与Facade对象交互,Facade与遗留代码交互所有复杂的工作。

image-20220316221906253

image-20220316222134258

9建造者模式

  • Builder是为创建一个Product对象的各个部件指定的抽象接口

  • ConcreteBuilder它是具体建造者,实现Builder接口,构造和装配各个部件。

  • Product 具体的(要建造的)产品

  • Director它是构建一个使用Builder接口的对象

  • 优点:建造者模式的好处就是使得建造代码与表示代码分离,由于建造者隐藏了该产品是如何组装的,所以若需要改变一个产品

    ​ 的内部表示,只需要再定义一个具体的建造者就可以了

  • 使用场景

    ​ 它主要是用于创建一些复杂的对象,这些对象内部构建间的建造顺序通常是稳定的,但对象内部的构建通常面临着复杂的变化。

    ​ 建造者模式是在当创建复杂对象的算法应该独立于该对象的组成部分以及它们的装配方式时适用的模式。

image-20220317001943752

image-20220317002352695

10观察者模式

  • 观察者模式又称发布订阅模式
  • 定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在发生变化时,会通知所有的观察者,使它们能自动更新自己
  • 观察者模式动机:将一个系统分割成一系列相互协作的类有一个很不好的副作用,那就是需要维护相关对象间的一致性。我们不希望为了维持一致性而使各类紧密耦合,这样会给维护、扩展和重用都带来不便
  • 使用场景:当一个对象的改变需要同时改变其他对象的时候。而且它不知道具体有多少对象有待改变时,应该考虑使用观察者模式。一个抽象模型有两个方面,其中一方面依赖于另一方面,这时用观察者模式可以将这两者封装在独立的对象中使它们各自独立地改变和复用。观察者模式所做的工作其实就是在解除耦合。让耦合的双方都依赖于抽象,而不是依赖于具体。从而使得各自的变化都不会影响另一边的变化。
  • 什么时候用接口、什么时候用抽象类:现实编程中,具体的观察者完全有可能是风马牛不相及的类,但它们都需要根据通知者的通知来做出Update()的操作,所以让它们都实现下面这样的一个接口就可以实现
  • 不足之处

image-20220317015812180

image-20220317015842054

事件委托

img

11抽象工厂模式

  • 定义:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类
  • 抽象工厂加反射
  • IFactory是一个抽象工厂接口,它里面应该包含所有的产品创建的抽象方法。而ConcreteFactory1和ConcreteFactory2就是具体的工厂了。
  • 通常是在运行时刻再创建一个ConcreteFactory类的实例,这个具体的工厂再创建具有特定实现的产品对象,也就是说,为创建不同的产品对象,客户端应使用不同的具体工厂。
  • 缺点:比如原先有具体的工厂sqlserver、access现在要加orcale,那就要加oracle的具体工厂,如果又加其它,其它就会有很多的工厂类,这时候可用简单工厂优化,不过就会产生if-else、switch判断
  • 使用反射来帮我们克服抽象工厂模式的先天不足了,做法将这个状态的变化做成可配置的字符串类型(如数据库连接类型,提前在xml配置文件里配置好,复用反射来选择需要创建的目标类)

image-20220317184547706

image-20220317184252465

image-20220317184343802

12状态模式

  • 当一个对象的内在状态改变时,允许其改变行为,这个对象看起来像是改变了类
  • 状态模式主要解决的是当控制一个对象状态转换的条件表达式过于复杂时的情况。把状态的判断逻辑转移到表示不同状态的一系列类当中,可以把复杂的判断逻辑简化。当然,如果这个状态判断很简单,那就没必要用‘状态模式’了。
  • 关键点:复杂的判断逻辑简化
  • 优点:将特定的状态相关的行为都放入一个对象中,由于所有与状态相关的代码都存在于某个ConcreteState中,所以通过定义新的子类可以很容易地增加新的状态和转换。状态模式通过把各种状态转移逻辑分布到State的子类之间,来减少相互间的依赖
  • 使用场景:当一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为时,就可以考虑使用状态模式了。如果业务需求某项业务有多个状态,通常都是一些枚举常量,状态的变化都是依靠大量的多分支判断语句来实现,此时应该考虑将每一种业务状态定义为一个State的子类。这样这些对象就可以不依赖于其他对象而独立变化了,某一天客户需要更改需求,增加或减少业务状态或改变状态流程

image-20220317215818692

  • State类,抽象状态类,定义一个接口以封装与Context的一个特定状态相关的行为。
  • ConcreteState类,具体状态,每一个子类实现一个与Context的一个状态相关的行为。
  • Context类,维护一个ConcreteState子类的实例,这个实例定义当前的状态。

13适配器模式

  • 定义:将一个类转换成客户希望的另外一个接口,Adapter模式将使得原本由于接口不兼容而不能一起工作的那些类可以一起工作
  • 扁鹊的医术:如果能事先预防接口不同的问题,不匹配问题就不会发生;在有小的接口不统一问题发生时,及时重构,问题不至于扩大;只有碰到无法改变原有设计和代码的情况时,才考虑适配。事后控制不如事中控制,事中控制不如事前控制。
  • 适配器模式当然是好模式,但如果无视它的应用场合而盲目使用,其实是本末倒置了。
  • NBA我需要翻译 :原因不可能让姚明立马学会英语,不可能让教练和其它球员立马学会英语
  • 如果能事先预防接口不同的问题,不匹配问题就不会发生;在有小的接口不统一问题发生时,及时重构,问题不至于扩大;只有碰到无法改变原有设计和代码的情况时,才考虑适配。事后控制不如事中控制,事中控制不如事前控制。
  • 使用场景:适配器模式主要应用于希望复用一些现存的类,但是接口又与复用环境要求不一致的情况

image-20220317231355777

14备忘录模式

  • 优点:当角色的状态改变的时候,有可能这个状态无效,这时候就可以使用暂时存储起来的备忘录将状态复原。忘录可以把复杂的对象内部信息对其他的对象屏蔽起来
  • 使用场景:Memento模式比较适用于功能比较复杂的,但需要维护或记录属性历史的类,或者需要保存的属性只是众多属性中的一小部分时,Originator可以根据保存的Memento信息还原到前一状态。如果在某个系统中使用命令模式时,需要实现命令的撤销功能,那么命令模式可以使用备忘录模式来存储可撤销操作的状态

image-20220318001230530

  • Originator(发起人):负责创建一个备忘录Memento,用以记录当前时刻它的内部状态,并可使用备忘录恢复内部状态。Originator可根据需要决定Memento存储Originator的哪些内部状态。
  • Memento(备忘录):负责存储Originator对象的内部状态,并可防止Originator以外的其他对象访问备忘录Memento。备忘录有两个接口,Caretaker只能看到备忘录的窄接口,它只能将备忘录传递给其他对象。Originator能够看到一个宽接口,允许它访问返回到先前状态所需的所有数据
  • Caretaker(管理者):负责保存好备忘录Memento,不能对备忘录的内容进行操作或检查

image-20220318001713953

15组合模式

  • 解决整体与部分可以被一致对待问题

  • 组合模式让客户可以一致地使用组合结构和单个对象。

  • 定义:将对象组合成树形结构以表示“部分-整体”的层次结构。组合模式使得用户单个对象和组合对象的使用具有一致性。

  • 透明方式与安全方式

    “树可能有无数的分枝,但只需要反复用Composite就可以实现树状结构了。小菜感觉如何?”
    “有点懂,但还是有点疑问,为什么Leaf类当中也有Add和Remove,树叶不是不可以再长分枝吗?”
    “是的,这种方式叫做透明方式,也就是说在Component中声明所有用来管理子对象的方法,其中包括Add、Remove等。这样实现Component接口的所有子类都具备了Add和Remove。这样做的好处就是叶节点和枝节点对于外界没有区别,它们具备完全一致的行为接口。但问题也很明显,因为Leaf类本身不具备Add()、Remove()方法的功能,所以实现它是没有意义的。”
    “哦,那么如果我不希望做这样的无用功呢?也就是Leaf类当中不用Add和Remove方法,可以吗?”
    “当然是可以,那么就需要安全方式,也就是在Component接口中不去声明Add和Remove方法,那么子类的Leaf也就不需要去实现它,而是在Composite声明所有用来管理子类对象的方法,这样做就不会出现刚才提到的问题,不过由于不够透明,所以树叶和树枝类将不具有相同的接口,客户端的调用需要做相应的判断,带来了不便。”
    “那我喜欢透明式,那样就不用做任何判断了。”
    “开发怎么能随便有倾向性?两者各有好处,视情况而定吧。”
    
  • 使用场景

    “当你发现需求中是体现部分与整体层次的结构时,以及你希望用户可以忽略组合对象与单个对象的不同,统一地使用组合结构中的所有对象时,就应该考虑用组合模式了。”
    “哦,我想起来了。以前曾经用过的ASP.NET的TreeView控件就是典型的组合模式应用。”
    “又何止是这个,你应该写过自定义控件吧,也就是把一些基本的控件组合起来,通过编程写成一个定制的控件,比如用两个文本框和一个按钮就可以写一下自定义的登录框控件,实际上,所有的Web控件的基类都是System.Web.UI.Control,而Control基类中就有Add和Remove方法,这就是典型的组合模式的应用。”
    

    image-20220318014945701

16迭代器模式

  • 定义:提供一种方法顺序访问一个聚集对象中各个元素,而又不暴露该对象的内部表示

  • 当你需要对聚集有多种方式遍历时,可以考虑用迭代器模式

  • 当你需要访问一个聚集对象,而且不管这些对象是什么都需要遍历的时候,你就应该考虑用迭代器模式

    当你需要访问一个聚集对象,而且不管这些对象是什么都需要遍历的时候,你就应该考虑用迭代器模式。另外,售票员从车头到车尾来售票,也可以从车尾向车头来售票,也就是说,你需要对聚集有多种方式遍历时,可以考虑用迭代器模式。由于不管乘客是什么,售票员的做法始终是相同的,都是从第一个开始,下一个是谁,是否结束,当前售到哪个人了,这些方法每天他都在做,也就是说,为遍历不同的聚集结构提供如开始、下一个、是否结束、当前哪一项等统一的接口。
    
  • 迭代器(Iterator)模式就是分离了集合对象的遍历行为,抽象出一个迭代器类来负责,这样既可以做到不暴露集合的内部结构,又可让外部代码透明地访问集合内部的数据。迭代器模式在访问数组、集合、列表等数据时,尤其是数据库数据操作时,是非常普遍的应用,但由于它太普遍了,所以各种高级语言都对它进行了封装,所以反而给人感觉此模式本身不太常用了。

    所以迭代器模式实用价值远不如学习价值大
    

image-20220318031405506

17单例模式

  • 保证一类仅有一个实例,并提供一个访问它的全局访问点
  • 使用场景:单例模式因为Singleton类封装它的唯一实例,这样它可以严格地控制客户怎样访问它以及何时访问它。简单地说就是对唯一实例的受控访问
  • 饿汉式:即静态初始化的方式,它是类一加载就实例化的对象,所以要提前占用系统资源—无并安全问题,缺点—提前占用系统资源
  • 懒汉式:又会面临着多线程访问的安全性问题,需要做双重锁定这样的处理才可以保证安全——双重检测锁

image-20220318044426121

18桥接模式

  • 定义:将抽象部分与它的实现部分分离,使得它们都可以独立的变化

  • 什么叫抽象与它的实现分离,这并不是说,让抽象类与其派生类分离,因为这没有任何意义。实现指的是抽象类和它的派生类用来实现自己的对象

  • 由于实现的方式有多种,桥接模式的核心意图就是把这些实现独立出来,让它们各自地变化。这就使得每种实现的变化不会影响其他实现,从而达到应对变化的目的。

    这里需要理解一下,什么叫抽象与它的实现分离,这并不是说,让抽象类与其派生类分离,因为这没有任何意义。实现指的是抽象类和它的派生类用来实现自己的对象[DPE]。就刚才的例子而言,就是让‘手机’既可以按照品牌来分类,也可以按照功能来分类。”
    按品牌分类实现结构图

    image-20220318185524054

    image-20220318185555143

image-20220318184434911

19命令模式

  • 定义:将一个请示封装成一个对象,从而使你可以用不同的请示对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。
  • 优点:它能较容易地设计一个命令队列;第二,在需要的情况下,可以较容易地将命令记入日志;第三,允许接收请求的一方决定是否要否决请求。第四,可以容易地实现对请求的撤销和重做;第五,由于加进新的具体命令类不影响其他的类,因此增加新的具体命令类很容易。其实还有最关键的优点就是命令模式把请求一个操作的对象与知道怎么执行一个操作的对象分割开。
  • 比如命令模式支持撤销/恢复操作功能,但你还不清楚是否需要这个功能时,你要不要实现命令模式?其实应该是不要实现。敏捷开发原则告诉我们,不要为代码添加基于猜测的、实际不需要的功能。如果不清楚一个系统是否需要命令模式,一般就不要着急去实现它,事实上,在需要的时候通过重构实现这个模式并不困难,只有在真正需要如撤销/恢复操作等功能时,把原来的代码重构为命令模式才有意义。

image-20220318225559487

image-20220318225457754

20职责链模式

  • 定义:使多个对象都有机会处理请求,从而避免信息的发送者和接收者之间的耦合关系。将这个对象连成一条链条,并沿着这条链传递该请求,直到有一个对象处理它为止。
  • 这里发出这个请求的客户端并不知道这当中的哪一个对象最终处理这个请求,这样系统的更改可以在不影响客户端的情况下动态地重新组织和分配责任。
  • 当客户提交一个请求时,请求是沿链传递直至有一个ConcreteHandler对象负责处理它。
  • 优点:
    • 接收者和发送者都没有对方的明确信息,且链中的对象自己也并不知道链的结构。结果是职责链可简化对象的相互连接,它们仅需保持一个指向其后继者的引用,而不需保持它所有的候选接受者的引用[DP]。
    • 这也就大大降低了耦合度了。随时地增加或修改处理一个请求的结构。增强了给对象指派职责的灵活性
  • 注意点:确是很灵活,不过也要当心,一个请求极有可能到了链的末端都得不到处理,或者因为没有正确配置而得不到处理,这就很糟糕了。需要事先考虑全面。

image-20220318225938434

ConcreteHandler类,具体处理者类,处理它所负责的请求,可访问它的后继者,如果可处理该请求,就处理之,否则就将该请求转发给它的后继者。

21中介者模式

  • 中介者模式很容易在系统中应用,也很容易在系统中误用。当系统出现了‘多对多’交互复杂的对象群时,不要急于使用中介者模式,而要先反思你的系统在设计上是不是合理

  • 由于把对象如何协作进行了抽象,将中介作为一个独立的概念并将其封装在一个对象中,这样关注的对象就从对象各自本身的行为转移到它们之间的交互上来,也就是站在一个更宏观的角度去看待系统。

  • 由于ConcreteMediator控制了集中化,于是就把交互复杂性变为了中介者的复杂性,这就使得中介者会变得比任何一个ConcreteColleague都复杂。事实上,联合国安理会秘书长的工作应该是非常繁忙的,谁叫他就是‘全球最大的官’呢。也正因为此,中介者模式的优点来自集中控制,其缺点也是它,使用时是要考虑清楚

    做法:(相当于A要向B租房,通过中介M,A、B都要认识这个中介M,相当于A、B都持有这个M,M要将A的租金需求告诉B,同时M要将B的租金条件告诉A,相当于A要同时持有B和A)

  • 使用场景:中介者模式一般应用于一组对象以定义良好但是复杂的方式进行通信的场合

    • image-20220319001605306
  • image-20220319001520698

22享元模式

  • 定义:运用共享技术有效的支持大量细粒度的对象

  • 虽说享元模式更多的时候是一种底层的设计模式,但现实中也是有应用的。比如说休闲游戏开发中,像围棋、五子棋、跳棋等,它们都有大量的棋子对象,你分析一下,它们的内部状态和外部状态各是什么

     ```
     在享元对象内部并且不会随环境改变而改变的共享部分,可以称为是享元对象的内部状态,而随环境改变而改变的、不可以共享的状态就是外部状态了。事实上,享元模式可以避免大量非常相似类的开销。在程序设计中,有时需要生成大量细粒度的类实例来表示数据。如果能发现这些实例除了几个参数外基本上都是相同的,有时就能够受大幅度地减少需要实例化的类的数量。如果能把那些参数移到类实例的外面,在方法调用时将它们传递进来,就可以通过共享大幅度地减少单个实例的数目。也就是说,享元模式Flyweight执行时所需的状态是有内部的也可能有外部的,内部状态存储于ConcreteFlyweight对象之中,而外部对象则应该考虑由客户端对象存储或计算,当调用Flyweight对象的操作时,将该状态传递给它。
     ```
    
  • 使用场景

在某些情况下,对象的数量可能会太多,从而导致了运行时的资源与性能损耗。那么我们如何去避免大量细粒度的对象,同时又不影响客户程序,是一个值得去思考的问题,享元模式,可以运用共享技术有效地支持大量细粒度的对象。不过,你也别高兴得太早,使用享元模式需要维护一个记录了系统已有的所有享元的列表,而这本身需要耗费资源,另外享元模式使得系统更加复杂。为了使对象可以共享,需要将一些状态外部化,这使得程序的逻辑复杂化。因此,应当在有足够多的对象实例可供共享时才值得使用享元模式
  • 注意:应当在有足够多的对象实例可供共享时才值得使用享元模式

image-20220319043113397

image-20220319043326062

23解释器模式

  • 定义:给定一下语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该文法的表示来解释语言中的句子。

  • 解释器模式需要解决的是,如果一种特定类型的问题发生的频率足够高,那么可能就值得将该问题的各个实例表述为一个简单语言中的句子。这样就可以构建一个解释器,该解释器通过解释这些句子来解决该问题

     ```
     比方说,我们常常会在字符串中搜索匹配的字符或判断一个字符串是否符合我们规定的格式,此时一般我们会用什么技术?”
     “是不是正则表达式?”
     “对,非常好,因为这个匹配字符的需求在软件的很多地方都会使用,而且行为之间都非常类似,过去的做法是针对特定的需求,编写特定的函数,比如判断Email、匹配电话号码等等,与其为每一个特定需求都写一个算法函数,不如使用一种通用的搜索算法来解释执行一个正则表达式,该正则表达式定义了待匹配字符串的集合。而所谓的解释器模式,正则表达式就是它的一种应用,解释器为正则表达式定义了一个文法,如何表示一个特定的正则表达式,以及如何解释这个正则表达式。
     
     像IE、Firefox这些浏览器,其实也是在解释HTML文法,将下载到客户端的HTML标记文本转换成网页格式显示到用户。
     不过编写一个浏览器的程序,当然要复杂得多。
     ```
    
“看起来好像不难,但其实真正做起来应该还是很难的吧。”
“是的,你想,用解释器模式,就如同你开发了一个编程语言或脚本给自己或别人用。这当然是难了。”
“我的理解是,解释器模式就是用‘迷你语言’来表现程序要解决的问题,以迷你语言写成‘迷你程序’来表现具体的问题。”
“嗯,迷你这个词用得很好,就是这样的意思。通常当有一个语言需要解释执行,并且你可将该语言中的句子表示为一个抽象语法树时,可使用解释器模式[DP]。”
“解释器模式有什么好处呢?”
“用了解释器模式,就意味着可以很容易地改变和扩展文法,因为该模式使用类来表示文法规则,你可使用继承来改变或扩展该文法。也比较容易实现文法,因为定义抽象语法树中各个节点的类的实现大体类似,这些类都易于直接编写[DP]。”
“除了像正则表达式、浏览器等应用,解释器模式还能用在什么地方呢?”
“只要是可以用语言来描述的,都可以是应用呀。比如针对机器人,如果为了让它走段路还需要去电脑面前调用向前走、左转、右转的方法,那也就太傻了吧。当然应该直接是对它说,‘哥们儿,向前走10步,然后左转90度,再向前走5步。’”
“哈,机器人听得懂‘哥们儿’是什么意思吗?”
“这就看你写的解释器够不够用了,如果你增加这个‘哥们儿’的文法,它就听得懂呀。说白了,解释器模式就是将这样的一句话,转变成实际的命令程序执行而已。而不用解释器模式本来也可以分析,但通过继承抽象表达式的方式,由于依赖倒转原则,使得对文法的扩展和维护都带来了方便。”
“哈,难道说,C#、Java这些高级语言都是用解释器模式的方式开发的?”
“当然不是那么简单了,解释器模式也有不足的,解释器模式为文法中的每一条规则至少定义了一个类,因此包含许多规则的文法可能难以管理和维护。建议当文法非常复杂时,使用其他的技术如语法分析程序或编译器生成器来处理[DP]”。
“哦,原来还有语法分析器、编译器生成器这样的东东。

image-20220319053550320

image-20220319053608650

24访问者模式

  • 定义:表示一个作用于某对象结构中各元素的操作,它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。

  • 使用的前提条件:访问者模式适用于数据结构相对稳定的系统。它把数据结构和作用于结构上的操作之间的耦合解脱开,使得操作集合可以相对自由地演化。

    • 比如根据不同状态下看男人和女人所做出的反应。这里关键就在于人就只分为男人和女人,这个性别的分类是稳定的,所以可以在状态类中,增加‘男人反应’和‘女人反应’两个方法,(假如再一个超人,就得增加‘超人反应’)方法个数是稳定的,不会很容易的发生变化。而‘人’抽象类中有一个抽象方法‘接受’,它是用来获得‘状态’对象的。每一种具体状态都继承‘状态’抽象类,实现两个反应的方法。
      
    • “本来我是想直接来谈访问者模式的,但是为什么我突然会愿意和你聊男人和女人的对比呢,原因就在于你说了一句话:‘男女对比这么多的原因是因为人类在性别上就只有男人和女人两类。’而这也正是访问者模式可以实施的前提。”
      “这个前提是什么呢?”
      “你想呀,如果人类的性别不止是男和女,而是可有多种性别,那就意味‘状态’类中的抽象方法就不可能稳定了,每加一种类别,就需要在状态类和它的所有下属类中都增加一个方法,这就不符合开放-封闭原则。”
      “哦,也就是说,访问者模式适用于数据结构相对稳定的系统?”
      “对的,它把数据结构和作用于结构上的操作之间的耦合解脱开,使得操作集合可以相对自由地演化。
      
  • 使用场景:

  • ```
    访问者模式的目的是要把处理从数据结构分离出来。很多系统可以按照算法和数据结构分开,如果这样的系统有比较稳定的数据结构,又有易于变化的算法的话,使用访问者模式就是比较合适的,因为访问者模式使得算法操作的增加变得容易。反之,如果这样的系统的数据结构对象易于变化,经常要有新的数据对象增加进来,就不适合使用访问者模式。   
    ```
    
  • 优点:访问者模式的优点就是增加新的操作很容易,因为增加新的操作就意味着增加一个新的访问者。访问者模式将有关的行为集中到一个访问者对象中。

  • 缺点:那访问者的缺点其实也就是使增加新的数据结构变得困难了。

  • 注意

    • GoF四人中的一个作者就说过:‘大多时候你并不需要访问者模式,但当一旦你需要访问者模式时,那就是真的需要它了。’事实上,我们很难找到数据结构不变化的情况,所以用访问者模式的机会也就不太多了。这也就是为什么你谈到男人女人对比时我很高兴和你讨论的原因,因为人类性别这样的数据结构是不会变化的。
      

image-20220319063738768

image-20220319062805000

需要提一下当中用到一种双分派的技术,首先在客户程序中将具体状态作为参数传递给“男人”类完成了一次分派,然后“男人”类调用作为参数的“具体状态”中的方法“男人反应”,同时将自己(this)作为参数传递进去。这便完成了第二次分派。双分派意味着得到执行的操作决定于请求的种类和两个接收者的类型。‘接受’方法就是一个双分派的操作,它得到执行的操作不仅决定于‘状态’类的具体状态,还决定于它访问的‘人’的类别

image-20220319062824193

posted @ 2022-04-06 20:58  随风笔记  阅读(58)  评论(0)    收藏  举报