0%

【SE】软件设计模式

  • 主要内容
    • 策略模式
    • 装饰者模式、工厂模式、单件模式、命令模式
    • 模板方法模式:封装算法
    • 迭代器和组合模式:管理良好的集合
    • 状态模式:事物的状态
  • 参考学习资料《HeadFirst 设计模式》

设计原则总结

封装变化

找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起

理解:

  • 变化的部分:每次有新的需求,会使某方面代码发生变化
  • 把这部分变化的抽取出来
  • 即:把会变化的部分取出并封装起来,以便以后可以轻易地改动或扩充此部分,而不影响不需要变化的其他部分
  • 效果:代码变化引起的不经意的后果变少,系统变得更有弹性

针对接口

针对接口编程,而不是针对实现编程

理解:

  • 实质就是,针对超类型编程
  • 明确地理解:变量的声明类型是超类型——抽象类或者接口

区分

  • 针对实现编程

    1
    2
    Dog d = new Dog(); //Dog是抽象类Animal的具体实现
    d.bark();
  • 针对接口、超类型编程

    1
    2
    3
    4
    5
    6
    Animal animal = new Dog();
    animal.makeSound(); //多态调用

    //在运行时指定具体实现的对象:
    a = getAnimal();
    a.makeSound();

多用组合

多用组合、少用继承

理解:

  • 比如在创建ModelDuck这个具体类时,在它的构造器里实例化了FlyBehavior和QuackBehavior这两个类(接口);这就是使用了组合

    1
    2
    3
    4
    5
    6
    7
    public class ModelDuck extends Duck{
    public ModelDuck() {
    flyBehavior = new FlyNoWay();
    quackBehavior = new Quack();
    }
    //其他代码
    }

松耦合

详细例子 见” 观察者模式 “

定义

为了交互对象之间的松耦合设计而努力


  • 松耦合的设计能够让我们建立有弹性的OO系统,能够应对变化,因为对象之间的互相依赖降到了最低
  • 当两个对象之间松耦合,它们依然可以交互,但是不太清楚彼此的细节;

开闭原则

  • 内容:类应该对扩展开放,对修改关闭
  • 允许类容易扩展,在不修改现有代码的情况下,就可搭配新的行为:
    • 这样设计具有弹性
    • 可以应对改变
    • 可以接受新的功能来应对改变的需求

好莱坞原则

使用了好莱坞原则的模式:

  • 工厂模式
  • 观察者模式
  • 模板方法模式

定义

(高层组件对待低层组件的方式:)别调用我们,我们会调用你

允许低层组件将自己挂钩到系统上,但高层组件会决定什么时候和怎样使用这些低层组件

说明:低层组件并不是 不可以调用高层组件中的方法。

  • 事实上,在低层组件结束时,常常会调用从超类继承而来的方法
  • 我们要做的是,避免高低层组件之间有明显的环状依赖

好莱坞原则与依赖倒置原则的关系

  • 依赖倒置原则:避免使用具体类,多使用抽象
  • 好莱坞原则:用在创建框架or组件上的一种技巧,让低层组件能够被挂钩进计算中,而且不会让高层组件依赖低层组件
    • 创建一个弹性的设计,允许低层结构能够相互操作,而且又防止其他类太过依赖它们
  • 二者都在解耦,依赖倒置原则更注重于避免依赖

预备知识

依赖(use a)

A类只作为B类中方法的参数或返回值或局部变量或调用A的静态方法

则A类与B类之间存在依赖关系

1
2
3
4
5
6
7
8
class B_Driver{
void method1(A_Car a){}//作为方法的参数
A_Car method2(){、
A_Car.method();//调用静态方法
A_Car a = new A_Car; //作为局部变量
return a; //作为返回值
}
}//B依赖于A(B要用到A)

关联
A类作为B类的属性(成员变量),则A类与B类之间存在关联关系

组合(整体 contains a 部分)

整体与部分是不可分的,部分也不能给其它整体共享,作为整体的对象负责部分的对象的生命周期。这种关系比聚合更强,也称为强聚合。

如果Acontains a B,则A需要知道B的生存周期,即可能A负责生成或者释放B,或者A通过某种途径知道B的生成和释放。

组合关系也属于关联关系,A类作为B类的属性,并且在B类中包含了A类的实例化过程,则A类与B类之间存在组合关系。

  • 比如C类实例化B类时,B类会在构造方法中对A类进行实例化,A类与B类的产生和灭亡完全同步。组合关系下B类中的A属性不可能为null。

    1
    2
    3
    4
    5
    6
    7
    8
    class C{
    new B();
    }
    class B{
    public B(){
    new A;//构造方法中对A类进行实例化
    }
    }

聚合(整体 has a 部分)

此时整体与部分之间是可分离的,它们可以具有各自的生命周期,部分可以属于多个整体对象,也可以为多个整体对象共享,所以聚合关系也常称为共享关系。

例如,公司部门与员工的关系,一个员工可以属于多个部门,一个部门撤消了,员工可以转到其它部门。

聚合关系属于关联关系,A类作为B类的属性,但A类的实例化不是在B类中实现的,则A类与B类之间存在聚合关系。

  • 比如在C类中实例化了A类,并将A类对象通过B类对象的set方法或B类的带参数构造器传入B中。聚合关系下B类中的A属性有可能为null。

    1
    2
    3
    4
    5
    6
    7
    class C{
    new A();
    }
    class B{
    void setXX(A){ XX=A};//将A类对象通过B类对象的set方法传入
    public B(A){} //将A类对象通过B类的带参数构造器传入B中
    }

策略模式

引例

如上,使用继承来提供Duck的行为存在的缺点

  • 代码在多个子类中重复
    • 有些子类的有些行为相同
  • 运行时的行为不容易改变
  • 很难知道所有鸭子的全部行为
    • 每知道一个都要在父类中添加一个
  • 改变会牵一发动全身,造成不想要的改变
    • 比如,父类中添加一个fly,所有子类都继承fly,然而并不是所有鸭子都能fly

可见,因为鸭子的行为在子类中不断地 改变,让所有子类都拥有这些行为是不恰当的


那么,使用如下的接口呢?

使用接口的话,子类的fly和quack中仍有很多重复代码,且代码无法复用


因为鸭子的行为是不断变化的,根据封装变化的原则,可以将鸭子的变化的行为从Duck类中抽取出来

  • 变化的行为:fly()、quack();它们会随着鸭子的不同而改变
  • 把这两个行为抽取出来,建立两个新类各自代表这两个行为

同时,我们想要在运行时动态地改变某种鸭子的飞行行为,根据针对接口编程的原则:

  • 每个接口代表每个行为,Duck类不负责实现接口,而是有行为类来实现接口

这样设计,可以让这些动作被其他对象复用,因为上面这些东西实际上跟Duck类已经无关。

也就是说,鸭子现在将fly和quack行为 委托给别人处理,而不是使用在Duck类或Duck的子类中的fly和quack

具体实现

策略模式定义

策略模式定义了算法族、分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于算法的客户

理解:

  • 定义了算法族:比如上面的实现FlyBehavior的一组行为类,这就是一个算法族,这个算法族里面的各种行为可以替换
  • FlyBehavior的变化和原来的Duck无关,被独立了出来

  • Context类:它是使用算法的角色,它在解决某个问题(即实现某个方法)时可以采用多种策略。在Context类中维持一个对抽象策略类(Strategy)的引用实例,用于定义所采用的策略。
  • Strategy(抽象策略类):它为所支持的算法声明了抽象方法,是所有策略类的父类,它可以是抽象类或具体类,也可以是接口。Context类通过Strategy类中声明的方法 在运行时调用ConcreteStrategy类中实现的算法。
  • ConcreteStrategy(具体策略类):它实现了在抽象策略类中声明的算法,在运行时,具体策略类将覆盖在Context类中定义的抽象策略类对象,使用一种具体的算法实现某个业务处理。

使用场景:

  • 有很多相关的类,具有不同的行为
  • 算法有很多变种
  • 算法使用的数据无需客户知晓
  • 对很多行为有很多if-else语句

好处

  • 提供一种替代继承的方法
  • 去掉if-else语句
  • 同种行为提供不同实现

缺点

  • 客户应该知道所有策略
  • 策略和Context之间的通信开销
  • 太多策略的类

观察者模式

引例一

三种布告板:1.显示当前的状况、2.气象统计、3.简单预报

一个WeatherData能够追踪来自气象站的最新的数据,WeatherData会将三个布告板的显示更新

具体流程:

  • WeatherData有getter方法,取得来自气象站的测量值:温度、湿度、气压
  • 一旦更新了新数据,一个measurementsChanged()被调用
    • 立即将三个布告板信息更新
  • 该系统必须可货站,可以任意添加或删除任何布告板

引例二

报纸的订阅:

  • 报社出版报纸
  • 客户向报社订阅报纸,只要报社有新报纸出版,就会送来。只要是订阅客户,就会一直收到报纸
  • 不想看报纸,取消订阅,就不会收到报纸
  • 只要报社孩子啊,就会有人向它订阅or取消订阅

出版者subject+订阅者subscriber=观察者模式

模式特点

  • subject管理某些数据
    • subject数据改变,通知subscriber,新的数据会以某种形式送到subscriber
  • subscriber(也就是observer) 订阅subject,以便subject管理的数据改变时,subscriber能收到更新
    • 如果没有订阅subject,就不是subscriber,当subject数据改变时就不会收到通知

模式定义

观察者模式定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并且自动更新

一对多:这个一就是Subject,多就是Observer

实现观察者模式的方法不一种,但是以包含Subject和Observer接口的类的设计最常见

  • 主题是具有状态(真正用于数据)的对象,并且可以控制这些状态
  • 观察者使用这些状态,依赖主题告诉观察者状态何时改变

松耦合的体现

  • ConcreteSubject只知道ConcreteObserver实现了Observer接口
    • ConcreteSubject不需要直到ConcreteObserver的具体类是谁,做了啥等等
  • 可以随时增加或删除ConcreteObserver

    • 因为ConcreteSubjectt唯一依赖的是一个实现Observer接口的 对象列表
  • 当增加新的ConcreteObserver时,ConcreteSubject无需修改;

    • 只要实现Obeserver接口,注册为ConcreteObserver即可
    • 改变其中任一方,并不会影响另一方
  • 可以独立地复用ConcreteSubject和ConcreteObserver

引例一的实现

  • 每个ConcreteObserver都有一个ConcreteSubject的引用,这样之后想要取消注册的话就会很方便

Java内置的观察者模式

观察者模式使用到的原则

  • 找出程序中变化的方面,然后将其和固定不变的方面向分离
    • 会改变的时主题的状态,观察者的数目和类型
    • 采用观察者模式可以改变依赖主题状态的对象的同时 不必改变主题
  • 针对接口编程,不针对实现编程
    • 主题和观察者都使用接口
    • 观察者利用主题 的接口向主题注册
    • 主题利用 观察者接口 通知观察者
    • 松耦合
  • 多用组合,少用继承
    • 利用 组合,将许多观察者组合进主题中
    • 对象之间 的依赖关系不是通过继承产生,而是运行时通过组合的方式产生

装饰者模式

  • 不用继承 如何达到复用?
    • 组合和委托 可以在运行时具有继承行为的结果
    • 利用继承设计子类的行为,是在编译时静态决定的,而且所有子类都会继承到相同的行为。
    • 如果利用组合 扩展对象的行为,就可以在运行时动态扩展
    • 通过动态地组合对象,可以写新的代码添加新功能 且 无需修改现有代码(符合开闭原则)

认识装饰者模式

  • 例如:以“饮料”为主体,在运行时以调料来“装饰”饮料

一些要点

  • 装饰者和被装饰者:相同的超类型
    • 在任何需要原始对象(被包装)的场合,都可以用装饰过的对象代替它
  • 一个 or 多个装饰者 包装一个对象
  • 对象可以在任何时候被装饰
    • 运行时 动态地、不限量地装饰
  • 装饰者可以在所委托的被装饰者的行为之前 与/或 之后,加上自己的行为,以达到特定目的
    • 这里的行为,或者说是责任,实际上指的就是…可以实现的一些功能

定义装饰者模式

  • 定义:动态地将责任 附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案

装饰者模式的类图

  • 类图分析

    • Component:它的引入可以使客户端以一致的方式处理未被装饰以及装饰之后的对象,实现客户端的透明操作

      客户端不知道处理的未被装饰的对象,还是装饰过的对象

    • ConcreteComponent:定义具体的构件,实现了Component声明的方法,ConcreteDecorator可以给它增加额外的职责

    • Decorator:用于给ConcreteComponent增加职责,但是具体职责在子类中实现。

      • 维护一个指向抽象构建对象的引用
      • 通过该引用可以调用装饰之前的构件对象的方法,并通过其扩展子类的方法,达到装饰的目的。
  • 重点说明

    • 看似Decorator扩展子Component类,但是,重点在于:装饰者和被装饰者 必须类型一致,也就是有共同的超类
      • 换言之,就是利用继承达到“类型匹配”,而不是利用继承获得“行为”
    • 因为装饰者必须能取代被装饰者,所以它们必须有相同的“接口”
    • 当装饰者与组件组合时,就是在加入新的行为,所得到的新的行为,并不继承子超类,而是由组合对象得来
      • 行为如果不是来自超类,就是子类覆盖后的版本
      • 可以在任何时候,实现新的装饰者增加新的行为

一个栗子实现

首先实现最上面的抽象类Beverage

1
2
3
4
5
6
7
8
9
public abstract class Beverage{
String discription = "Unknown Beverage";

public String getDiscription(){
return discription;
}
//cost必须在子类中实现
public abstract double cost();
}

然后实现下面的抽象类Condiment

1
2
3
4
//扩展自Beverage,这样就能取代Beverage
public abstract class CondimentDecorator extends Beverage{
public abstract String getDiscription();
}

写饮料的代码(具体组件(被装饰对象))

1
2
3
4
5
6
7
8
public class Espresso extends Beverage{
public Espresso(){
discription = "Espresso";
}//实例变量discription继承自Beverage
public double cost(){
return 1.99;
}
}

写调料代码(具体装饰者)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Mocha extends CondimentDecorator{
//要让Mocha引用一个Beverage
//也就是之前所说的:每个装饰者都有一个组件(被装饰者),也就是说,装饰者有一个实例变量来保存某个Component的引用
Beverage beverage;
//把饮料(被装饰者)当作构造器的参数,将其记录在实例变量中
public Mocha(Beverage beverage){
this.beverage = beverage;//运行时才知道
}
//利用“委托”,得到被装饰者的Discription,再附加自己的“,Mocha”
public String getDiscription(){
return beverage.getDiscription() + ",Mocha";
}
//利用“委托”,得到被装饰者的cost,再加上自己的cost
public double cost(){
return 0.20+beverage.cost();
}
}

总结

  • 装饰者模式优缺点
    • 优:比静态继承有更多的灵活性;在高层类中防止过多特征
    • 缺:太多小类(指的是 具体的装饰者类),导致复用变得复杂
  • 装饰者模式使用时机
    • 需要为一个对象动态的添加责任(功能),并且不影响其他对象
    • 处理可撤销的责任(?啥意思)
    • 当生成子类不可行的时候,比如需要大量子类来支持各种排列组合(类爆炸
  • 其他要点
    • 继承是扩展的一种形式,但 并不是获得灵活性最好的方式
    • 组合 和 委托 可以用在动态时添加新行为
    • 装饰者类 反映了它们所装饰对象的类型(装饰者类型 == 被装饰对象类型)
    • 可以将一个组件用任意数量的装饰者包装

工厂模式——创建对象

简单工厂

简单工厂 其实不是一个设计模式,而是一种编程习惯

  • 首先,看下面的代码,可以分析出变化 和不变的部分
  • 变化的:new出来pizza的口味 (type):比如cheesePizza,pepperoniPizza,这些pizza可能根据实际情况而不断发生改变(增加or删除某些口味)
  • 不变的:其他制作流程prepare,bake,cut...

  • 可见,创建pizza对象的部分是不断变化的,因此可以对 对象的创建 进行封装
    • 创建对象的代码从orderPizza()抽离出来
    • 把抽离出来的者部分代码搬到另一个新的对象SimplePizzaFactory中,这个新对象只管如何创建pizza
  • 称这个新对象 为“工厂”

建立一个简单披萨工厂:用于创建pizza

1
2
3
4
5
6
7
8
9
10
11
12
public class SimplePizzaFactory{
//所有客户都用这个createPizza方法来实例化对象
public Pizza createPizza(String type){
Pizza pizza = null;
//...中间则是从原来的orderPizza中抽离出来的代码
if(type.equals="cheese"){
pizza = new CheesePizza();
}else if...
//...
return pizza;
}
}
  • 这样做的好处
    • SimplePizzaFactory可以有许多的客户,把创建pizza的代码封装到一个类里面,当以后实现改变时 (比如增加or删除某种口味的pizza),只需要修改这一个类即可

重做PizzaStore类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class PizzaStore{
//对factory的引用
SimplePizzaFactory factory;

public PizzaStore(SimplePizzaFactory factory){
this.factory = factory;
}
//orderPizza就成了SimplePizzaFactory的客户
//意思就是 当客户orderPizza需要pizza时,就让factory做一个给它
public Pizza orderPizza(String type){
//这里将原来的new出各种口味的pizza 替换成 工厂对象的创建方法 ,不再使用new具体实例化
//使得orderPizza不再依赖于具体类型
pizza = factory.createPizza(type);

//....后面是其他方法
}
}

类图

工厂方法模式

内容太长不看总结版:

工厂方法模式 实际上就是,在 抽象超类 中写一个抽象(没有具体实现,只是个接口)的方法(工厂方法),它是用来创建对象的,这个超类中的其他方法用到这个抽象方法创建的对象,但是其他方法用的时候并不关心这个对象究竟是什么类型的。

然后,继承这个超类的子类必须去实现这个抽象方法,来创建具体的对象(确定这个对象是啥类型的)

看懂 PizzaStore的框架和类图就懂辽

  • 变化的是地区(风味):比如虽说都是cheese披萨,但在不同的地区的风味不一样,有NY的cheese披萨,有Chicago的cheese披萨等等

    回顾:对比简单工厂模式,变化的是各种口味,比如cheese披萨,pepperoni披萨等等…

  • 我们希望:让pizza的制作活动局限于PizzaStore类(说人话:各地区加盟店pizza的制作流程固定不变),同时加盟店可以自由制作该区域的风味

给PizzaStore(抽象类)使用的框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public abstract class PizzaStore{

public Pizza orderPizza(String type)
{
Pizza pizza;
//createPizza 从工厂对象中重新移回到PizzaStore里面
pizza = createPizza(type);
/**************************************************/
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
/*中间是制作流程,固定不变,希望所有加盟店的处理流程都一致*/
return pizza;
}
//下面这就是咱们所谓的 "工厂方法",抽象的,
//因为我们希望加盟店能够自由制作风味,
//所以方法具体的实现肯定在各地区的加盟店(就是PizzaStore的子类)来实现createPizza方法
protected abstract Pizza createPizza(String type);
}
  • 因此,将PizzaStore作为超类,每个地域的类型(NYPizzaStore、ChicagoPizzaStore…)继承PizzaStore,这样制作流程得以固定,而每个子类可以通过实现createPizza接口来自由决定如何制作比萨

允许子类做决定

超类的orderPizza方法并不知道正在创建的pizza是哪一种(因为人家的createPizza只是个接口)

所以在各地域的PizzaStore中分别实现各自这个createPizza接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//在NYStylePizzaStore
public class NYStylePizzaStore extends PizzaStore{
//这个NYStylePizzaStore子类 全权负责实例化哪一个具体Pizza
//扩展自PizzaStore,必须实现createPizza这个抽象方法
public Pizza createPizza(type){
if(type.equals("cheese")){
pizza = new NYStyleCheesePizza();
}else if....
}
}

//在ChicagoStylePizzaStore
public class ChicagoStylePizzaStore extends PizzaStore{
public Pizza createPizza(type){
if(type.equals("cheese")){
pizza = new ChicagoStyleCheesePizza();
}else if....
}
}
  • orderPizza 调用createPizza时,是由某个具体的披萨店子类(比如NYStyleStore..)创建披萨

    • 子类并不是实时做出这样的决定的,但从orderPizza的角度来看,只要选择在NYStylePizzaStore订购披萨,那么就是NYStylePizzaStore 这个子类决定

      严格来讲:并非由这个子类实际“做决定”,而是由顾客决定到哪一家的披萨店才决定了披萨的风味(听起来 嗯,听君一席话如听一席话般的废话….)

声明一个工厂方法

工厂方法 用来**处理对象的创建**,并将这样的行为封装在子类中,这样,(好处:)客户程序中关于超类的代码 就和 子类对象创建的代码 解耦了
1
abstract Product factoryMethod(String type);
  • 工厂方法 必须返回一个产品(比如pizza),而 超类中的其他方法 通常会使用到这个工厂方法的 返回值 (比如超类PizzaStore里的orderPizza中使用了返回的pizza)
  • 工厂方法本质是一个抽象的方法
  • 理解上面红色的那段话:
    • 处理对象的创建:这个工厂方法就是用于创建一个对象
    • 并将这样的行为封装在子类中:因为工厂方法是抽象的,必须由子类来具体实现该方法
    • 客户程序中关于超类的代码:就是超类中的代码,比如PizzaStore中的orderPizza
    • 子类对象创建的代码:也就是实际了创建具体产品的代码,比如子类NYStylePizzaStore里面的createPizza创建产品
    • ”解耦“ 的理解:在PizzaStore中,orderPizza对Pizza做了很多事(bake,cut等等),但是Pizza是抽象的,orderPizza并不知道实际哪些具体的类参与了进来,也就是 我们实现了面向抽象编程 而不是面向具体编程

一个栗子

  • 关键把下面这张图的思路看懂

Pizza本身(抽象类)

1
2
3
4
5
6
7
8
9
public abstract class Pizza{
String name;
String dough;
....
void prepare(){.....}
void bake(){....}
void cut(){....}
void box(){.....}
}

具体的Pizza子类

1
2
3
4
5
6
public class NYStyleCheesePizza extends Pizza{
public NYStyleCheesePizza(){
name="xx";dough="xx";....
}
void cut(){/*可以覆盖抽象类的方法*/}
}

这个栗子的类图

  • 所有工厂模式 都是用来封装对象的创建工厂方法模式通过让子类决定该创建的对象是什么,来达到对象创建的过程封装的目的

q3SWxP.png

  • 平行的类层级角度来看

q3puIH.png

* 重点 * 定义工厂方法模式+类图(Factory Method Pattern)

  • 工厂方法模式:定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类

    理解:比如之前的栗子中,PizzaStore(超类)定义了一个抽象的接口createPizza,它只是一个接口!!!我们只知道这个接口的目的是 告诉大家创建一个Pizza,但并没有实现!!!!

    而Pizza具体怎么创建(即:new出来的Pizza究竟是啥类型的Pizza)就需要继承PizzaStore的子类实现这个接口。

    即:NYStylePizzaStore类中的createPizza()方法里面的 比如这句代码 pizza = new NYStyleCheesePizza(); 才将Pizza给实例化成一个具体的NYStyleCheesePizza

  • 所谓“决定”,指的是 在编写创建者类(Creator)时,不需要知道实际创建的产品具体是啥

    比如之前PizzaStore里面调用pizza.cut(),pizza.bake()等等,我们不需要知道这个pizza究竟哪种口味的pizza

  • 选择了使用哪个子类,自然就决定了实际创建的产品是什么

    因为我们的子类 把 工厂方法createPizza给具体实现了

  • 当只有一个ConcreteCreator时:工厂方法模式 能够将产品的“实现”“使用”中解耦,增加or改变产品的实现 Creator也不会受影响

    这里的“实现”,也就是 产品的创建

  • 工厂方法 和创建者 并不总是抽象

    • 可以定义一个 默认的工厂方法 来生产某些具体的产品,这样即使创建者没有任何子类,依然可以创建产品(这是书上说的,?我暂时无法理解)
  • 简单工厂和工厂方法的差异

    • 简单工厂 把全部的事情(比如创建不同口味的pizza)在一个地方处理完了(这个地方就是之前的SimplePizzaFactory)
      • 简单工厂可以将对象的创建 封装起来,但不具备工厂方法的弹性:简单工厂不能变更在创建的产品
    • 工厂方法,就是创建一个框架(抽象的方法),让子类决定如何实现(实现这个抽象方法)
      • 比如:超类PizzaStore里面的orderPizza提供了一般的框架;orderPizza以工厂方法createPizza来创建具体类,制造出实际的pizza
  • ”工厂“ 的好处

    • 避免代码重复,方便维护
    • 客户在实例化对象时,只依赖接口,而不是具体类
    • 代码更有弹性,应对未来的扩展

对象依赖(依赖倒置原则)

  • 看下图代码,DependentPizzaStore这个里面的一些方法的实现 需要用到 很多其他具体的类,即所说它 依赖 于其他具体的类

    比如 要实现createPizza,就需要使用各种具体的Pizza类,比如NYStyleCheesePizza等等;

    所以坏就坏在,只要它DependentPizzaStore所依赖的这些具体的类有啥改动,就必须把DependentPizzaStore这个的代码翻出来进行改动

**依赖倒置原则**:要依赖**抽象**,不要依赖具体类
  • 不能让高层组件依赖 低层组件,而且不管高低层,都应该依赖于抽象

    高层组件:由其他低层组件定义其行为的类。比如PizzaStore是高层组件,它的行为是由Pizza定义的,(PizzaStore创建所有不同的Pizza对象)而Pizza本身是低层组件

一个PizzaStore可以制作不同类型的Pizza,这些不同类型的Pizza都是Pizza,所以可以让它们共享同一个Pizza接口,那么我们 就抽象出了一个Pizza类,而对于PizzaStore来讲,它只要制作出Pizza就行了,不用理会具体的Pizza类

抽象工厂模式

首先,抛开前面的东西,现在的栗子虽然和上面都是采用Pizza,但是跟上面的那些东西毫无关系,不要把这个栗子和之前的栗子关联起来

  • 每个不同的地区有不同的加盟店,每个加盟店都要制作Pizza,但是对于某一种比如cheese Pizza,虽然都是cheese Pizza,但是在不同加盟店所使用的原料的具体种类并不完全相同

首先,建造出抽象出来的原料工厂

1
2
3
4
5
6
7
public interface PizzaIngredientFactory{
public Dough createDough();
public Sauce createSauce();
public Cheese createCheese();
public Clams createClam();
...
}//原料工厂都需要生产Dough、Sauce等等,但具体生产的啥Dough,啥Sauce 就用子类来实现
  • 这个抽象的原料工厂(接口) 其实就相当于提供了一个大致的框架,每个区域的工厂(实现接口的类)把这个框架给具体实现
  • 所以,接下来
  • 为每个区域创建一个具体的工厂,继承自这个PizzaIngredientFactory,具体实现每一个create方法

创建纽约原料工厂

1
2
3
4
5
6
7
8
9
10
11
public class NYPizzaIngredientFactory implements PizzaIngredientFactory{
public Dough createDough(){
return new ThinCrustDough(); //这个具体的原料工厂生产的Dough就是ThinCrustDough
//要有一组原料类供工厂使用,这些类可以在合适的区域共享,
//比如在NY可以使用ThinCrustDough,在Chicago也可以使用ThinCrustDough
}
public Sauce createSauce(){
return new MarinaraSauce();
}
...//同理,下面是其他的实现的接口方法
}

重做Pizza

抽象的Pizza

1
2
3
4
5
6
7
8
public abstract class Pizza{
//每个pizza都持有一组在prepare时会用到的原料
Dough dough;
Sauce sauce;....
//
abstract void prepare();

}

具体的Pizza

1
2
3
4
5
6
7
8
9
10
11
12
public class CheesePizza extends Pizza{
PizzaIngredientFactory ingredientFactory;
//要制作Pizza,必须要一个工厂来提供原料,所以每个Pizza都要获得一个工厂
public CheesePizza(PizzaIngredientFactory ingredientFactory){
this.ingredientFactory = ingredientFactory;
}
void prepare(){
//prepare一步步制作Pizza,每当使用原料,就从工厂要
dough = ingredientFactory.createDough();
sauce = ingredientFactory,createSauce();
}
}

Pizza店

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//NewYork的Pizza店
public class NYPizzaStore extends PizzaStore{
protected Pizza createPizza(String item){
Pizza pizza = null;
//这家店由NewYork的原料工厂负责生产所有原料
PizzaIngredientFactory = new NYPizzaIngredientFactory();
if(items.equals("cheese")){
//将工厂传递给每个Pizza,Pizza从工厂中获取原料
pizza = new CheesePizza(ingredientFactory);
//....
}else if ...
return pizza;
}
}

总体实现

1
2
3
4
5
6
7
8
9
10
11
PizzaStore nyPizzaStore = new NYPizzaStore();//要有一个NY的pizza店
nyPizzaStore.orderPizza("cheese");//在店里店一个cheese Pizza
Pizza pizza = createPizza("cheese");//orderPizza调用createPizza
//传入的参数是cheese,所有要生产一个cheesePizza,需要具体的原料工厂
Pizza pizza = new CheesePizza(nyIngredientFactory);
//pizza获得工厂后,pizza.prepare()就开始准备原料
void prepare(){
dough = factory.createDough();
//...
}
//之后 pizza.bake(),cut()....

※ 定义抽象工厂模式+类图

抽象工厂模式:提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类

  • 抽象工厂的方法 经常以工厂方法的形式实现

    理解:PizzaIngredientFactory里的所有create方法都是抽象的,没有具体实现,而是每个继承它的子类覆盖实现这些方法

三种工厂模式的比较

三种类图对比总结

  • 简单工厂:用来生产 同一等级结构中的任意产品(对于新加的产品,无能为力)

  • 工厂方法:用来生产同一等级结构的固定产品(支持增加任意产品)

  • 抽象工厂:用来生产不同产品族的全部产品(对于新增加的产品,无能为力,支持增加产品族)

抽象工厂 VS 工厂方法

  • 共同点:都是用来 创建对象的,把客户从所使用的具体实际产品中解耦

  • 不同点

    • 工厂方法使用继承

      利用工厂方法创建对象,需要扩展(继承)一个类,并覆盖它的工厂方法;

    • 抽象工厂:使用对象组合;使用时机:创建产品家族&让制造的相关产品结合起来

      它可以把一群相关的产品集合起来:

      抽象工厂 提供一个用来创建一个产品家族的抽象类型,它的子类定义了具体产品实现


单件模式

  • 用来创建独一无二的,只能有一个实例的对象

    因为,有些对象我们只需要一个,如果制造多个会导致许多问题

    可以确保程序中使用的全局资源只有一份,对资源敏感的对象特别重要

  • 单件模式可以确保只有一个示例被创建,并且可以在需要时才创建

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton{
// 利用静态变量 来记录这个 唯一的实例
private static Singleton uniqueInstance;
// 声明为私有:只有Singleton类内才可以调用它
private Singleton(){};
// 利用静态方法来实例化这个唯一的实例
public static Singleton getInstance(){
if(uniqueInstance == null){
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
  • 调用Singleton里面的静态方法:Singleton.getInstance()

    getInstance():静态方法,也就是类方法;对静态方法的调用要使用类名

  • 延迟实例化:如果不需要这个实例,它就永远不会产生

  • 要想取得Singleton的一个实例,我们必须”请求“得到这么一个实例,而不是自行new 一个Singleton出来,(反正也new不出来,因为构造器是private)。所以我们可以,调用静态方法getInstance来请求得到 这个类的实例,这相当于一个全局访问点

    • 从上面的代码易知:这个实例可能是在这次调用的时候被创建出来的,也可能是之前已经创建出来的。反正它只有一个

一个栗子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ChocolateBoiler{
private static ChocolateBoiler uniqueInstance;
private boolean empty;
private boolean boiled;
private ChocolateBoiler{
empty = true;
boiled = false;
}
public static ChocolateBoiler getInstance(){
if(uniqueInstance == null){
uniqueInstance = new ChocolateBoiler();
}
return uniqueInstance;
}
public void 其他方法(){...}
}

定义单件模式

单件模式:确保一个类中只有一个实例,并提供一个全局访问点

但是上面这种经典的单件模式在多线程的情况下会出现问题

bDb6c.png

对于多线程的处理:把getInstance()变成同步方法

1
public static synchronized Singleton getInstance(){...}
  • 这样,可以迫使每个线程进入该方法之前,需要等待别的线程离开该方法,保证不会有两个线程同时进入该方法
  • 但是,只有在new uniqueInstance的时候才需要同步,所以这会拖垮性能

解决办法

getInstance的性能不是很关键,就不用解决

  • 但如果getInstance使用在程序频繁运行的地方,就必须解决,否则运行效率会大幅下降

“急切”创建实例,而不用延迟实例化的做法

  • 如果总是创建并使用单件实例or在创建和运行时负担不繁重,可以急切创建此单件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class Singleton{
    //JVM在加载这个类的时候马上创建唯一的单件实例(静态初始化实例)
    //保证在任何线程访问uniqueInstance之前,一定先创建了它
    private static Singleton uniqueInstance = new Singleton();
    private Singleton(){};
    public static Singleton getInstance(){
    return uniqueInstance;
    }
    }

双重检查加锁

  • 首先检查实例是否已经创建,若未创建,才进行同步–>保证只有第一次同步
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton{

private volatile static Singleton uniqueInstance;
private Singleton(){}
public static Singleton getInstance(){
// 检查实例,如果未创建,就进行同步
if(uniqueInstance == null){
synchronized(Singleton.class){ //再检查一次
if(uniqueInstance == null){
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}

命令模式——封装调用

刚学这个模式的时候狠狠地难受到了,一开始始终理不清书上餐厅点餐和遥控器这个两个栗子的相通点,看了两遍书才看明白,啊啊啊啊,本 一学就废 的fw真的好羡慕世界上那些理解能力超强 一学就会 的人呐QAQ

初步理解

  • 遥控器栗子:

  • 请求者:遥控器;执行者:某个家电

  • 命令对象:封装的东西–> 家电的具体的执行工作+家电

    比如,遥控器想要打开电灯,那么就把“电灯+电灯的打开动作“,封装成一个请求对象,也就是我们所说的“命令对象”;

    换种理解方式就是:一个命令对象 封装了 命令的执行者(也就是“谁执行”)+执行者的执行动作(也就是“具体怎样执行”

    遥控器的每个按钮插槽都存储一个命令对象,只要按下,命令对象就执行封装在它里面的工作

  • 用一个餐厅的栗子来对比

  • 命令模式 可以将“动作的请求者” 从“动作的执行者” 对象中解耦

第一个命令对象

  • 所有命令声明一个接口,只要命令execute,就可以让命令的接收者执行动作
1
2
3
public interface Command{
public void execute();
}
  • 首先实现一个打开电灯的命令 (这个LightOnCommand命令就是一个命令对象)
    • 它封装了执行者light+具体动作light.on())
1
2
3
4
5
6
7
8
9
10
11
12
public class LightOnCommand implements Command{
Light light;
//传入执行“开灯”命令的执行者light
public LightOnCommand(Light light){
this.light = light;
}
//一旦调用,就由这个light接受命令,执行动作
//execute封装了 执行命令的 执行者 所要执行的所有动作
public void execute(){
light.on();
}
}
  • 使用命令对象(先假设遥控器上只有一个插槽
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SimpleRemoteControl{
Command slot;//一个插槽持有一个命令对象 用来控制一个设备
public SimpleRemoteControl(){}
//设置该插槽 持有的命令是什么;
//比如传入ligthOn,就让这个插槽持有“开灯”这个命令,那么我按这个按钮,就是开灯,
//如果传入lightOff,那么这个插槽就持有“关灯”这个命令,我按这个按钮,就变成了关灯;
//所以可以传入不同的命令,实现按下当前按钮,执行不同的功能
public void setCommand(Command command){
slot = command;
}
//一旦按下按钮(发出请求),插槽持有的命令就可以执行
public void buttonWasPressed(){
slot.execute();
}
}
  • 测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//这个测试 就是命令模式的客户
public class RemoteControlTest{
public static void main(String[] args){
SimpleRemoteControl remote = new SimpleRemoteControl();
//new出来的这个light 就是请求的接收者(真正执行这个命令的东西)
Light light = new light();
//客户负责创建一个具体的命令对象,同时要设置这个命令的接收者
//相当于:顾客下了一个订单,
LightOnCommand lightOn = new LightOnCommand(light);
//remote就相当于服务员,服务员一天能接受很多很多的订单,相当于setComand可以接受不同的参数;此刻,当下,传入lightOn这个参数,代表服务员接受了lightOn这个订单
remote.setCommand(lightOn);
//然后服务员知道所有订单都支持buttonWasPressed这个方法,一旦有订单,也就是有了一个命令,这个命令的执行只需要按一下按钮就彻底执行完毕了
remote.buttonWasPressed();
//只要buttonWasPressed,这个button绑定的命令就立马执行
}
}

定义命令模式

命令模式:将“请求”封装成对象,以便使用不同的请求、队列、或者日志来参数化其他对象。命令模式也支持可撤销的操作
  • 理解
    • 参数化:比如遥控器的一个插槽,可以用 不用的请求(也就是命令) 当参数remote.setCommand(参数)

  • 遥控器除了在按下按钮时,调用这个按钮对应的命令对象的execute方法之外其他什么都不知道
  • 当遥控器有两个按钮,希望可以控制客厅和厨房的电灯时,因为命令对象里可以封装这个命令的接收者,所以只需要创建两个LightCommand命令,一个绑定客厅的电灯,一个绑定厨房的电灯,按钮一按下,各自的execute就执行

将命令指定到插槽

  • 遥控器每个插槽 对应一个命令,当按下按钮,相应命令对象的execute就会被调用,execute方法中命令的接收者(家电)的动作就会被调用(具体见下列代码)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class RemoteControl{
Command[] onCommands;
COmmand[] offCommands;
public RemoteControl{
onCommands = new Command[7];
offCommands = new Command[7];
COmmand noCommand = new NoCommand();
for(int i=0;i<7;i++){
onCommands[i] = noCommand;
offCommands[i] = noCommand;
}
}
public void setCommand(int slot,Command onCommand,Command offCommand){
onCommands[slot] = onCommand;
offCOmmands[slot] = offCommand;
}
//调用下列方法就相当于按下按钮,那么这个按钮插槽绑定了的一个命令对象就会执行execute
//这个命令对象就存储在onCommands[slot]
//所调用的execute方法里面就写了 命令的执行者和执行者的动作
public void onButtonWasPushed(int slot){
onCommands[slot].execute();
}
public void offButtonWasPushed(int slot){
offCommands[slot].execute();
}

}

实现命令

1
2
3
4
5
6
7
8
9
public class LightOffCommand implements Command{
Light light;
public LightOffCommand(Light light){
this.light = light;
}
public void execute(){
light.off();
}
}
  • NoCommand是一个空对象——什么事都不做:当不想返回一个有意义的对象就可以使用。
  • 也可以将处理null的责任转移给空对象
1
2
3
public class NoCommand implements Command{
public void execute(){} //实现一个什么都不做的命令
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class RemoteLoader{
public static void main(String[] args){
RemoteControl remoteControl = new RemoteControl();
//首先,创建所有的家电
Light livingRoomLight = new Light("Living Room");
//...创建其他家电...
//创建所有家电的命令对象
LightOnCOmmand livingRoomLightOn = new LightOnCommand(livingRoomLight);
//...创建其他命令对象...
//将创建完的命令对象加载到遥控器插槽中
remoteControl.setCommand(0,livingRommLightOn,livingRoomLightOff);
//...其他...
//上述准备完毕,按下每个插槽的开关按钮就行
remoteControl.onButtonWasPushed(0);
remoteControl.offButtonWasPushed(0);
//...按下其他开关按钮...
}
}

类图

实现撤销

  • 撤销最近的那个动作
1
2
3
4
public interface Command{
public void execute();
public void undo();
}
  • 实现具体类
1
2
3
4
5
6
7
8
9
10
11
12
public class LightOnCommand implements Command{
Light light;
public LightOnCommand(Light light){
this.light = light;
}
public void execute(){
light.on();
}
public void undo(){
light.off();//execute是打开,所以undo该是关闭
}
}
  • 修改遥控器类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class RemoteControlWithUndo{
Command[] onCommands;
Command[] offCommands;
Command undoCommand; //加入undoCommand这个实例变量,用来追踪最后被调用的命令
//不管何时撤销按钮被按下,都可以取出这个命令(在执行撤销命令之前的最后那个非撤销命令)并调用它undo方法
public RemoteControlWithUndo(){
onCommands = new Command[7];
offCommands = new Command[7];
Command noCommand = new NoCommand();
for(int i=0;i<7;i++){
OnCommands[i] = noCommand;
OffCommands[i] = noCommand;
}
undoCommand = noCommand;
}
public void setCommand(int slot,Command onCommand,Command offCommand){
onCommands[slot] = onCommand;
offCommands[slot] = offCommand;
}
public void onButtonWasPushed(int slot){
onCommands[slot].execute();
undoCommand = onCommands[slot];//当按下当前按钮时,记录当前按钮绑定的命令 到undoCommand中
}
public void undoButtonWasPushed(){
undoCommand.undo(); //当按下撤销按钮时,可以撤销最近的一个命令
}
}

使用状态实现撤销

  • 吊扇允许多种转速,允许被关闭
  • 在吊扇的命令类中加入“撤销” :实现追踪吊扇的最后设置的速度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class CeilingFanHighCommand implements Command{
CeilingFan ceilingFan;
int prevSpeed; //这就是增加的状态,来追踪吊扇之前的速度
public CeilingFanHighCommand(CeilingFan ceilingFan){
this.ceilingFan = ceilingFan;
}
public void execute(){
prevSpeed = ceilingFan.getSpeed();//在改变吊扇速度之前,先保存速度
ceilingFan.high();
}
pubilc void undo(){
if(prevSpeed == CeilingFan.HIGH){
ceilingFan.high();
}else if(prevSpeed == CeilingFan.Medium){
ceilingFan.medium();
}else if(prevSpeed == CeilingFan.LOW){
ceilingFan.low();
}else if(prevSpeed == CeilingFan.OFF){
ceilingFan.off();
}
}
}
  • 测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class RemoteLoader{
public static void main(String[] args){
RemoteControlWithUndo remoteControl = new RemoteControlWithUndo();
CeilingFan ceilingFan = new CeilingFan("Living Room");
CeilingFanHighCommand ceilingFanHigh = new CeilingFanHighCommand(ceilingFan);
//..同理,再创建一个中速命令(省略)
remoteControl.setCommand(1,ceilingFanHigh,ceilingFanOff);
//..先开启中速度,然后关闭
remoteControl.onButtonWasPushed(0);
remoteControl.offButtonWasPushed(0);
remoteControl.undoButtonWasPushed();//撤销,回到中速
remoteControl.onButtonWasPushed(1);//开启高速
remoteControl.undoButtonWasPushed();//撤销高速,回到中速

}
}
  • 如果希望能够按下按钮多次,撤销很早以前的状态,就可以使用一个堆栈记录操作命令的每一个过程;一旦按下撤销按钮,就可以从堆栈中取出最上层的命令,调用undo方法

使用宏命令

  • 我们想要在不改变遥控器的情况下,只按一个按钮,就能控制所有装置(同时打开电灯、电视等等)

  • 宏命令

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class MacroCommand implements Command{
    Command[] commands;
    public MacroCommand(Command[] commands){
    this.commands = commands;//宏命令存储一堆命令
    }
    public void execute(){ //宏命令执行时,数组存储的命令一次性全部执行
    for(int i=0;i<commands.length();i++){
    commands[i].execute();
    }
    //当然也可以撤销,按下撤销按钮,宏命令内所有命令都被撤销
    public void undo(){
    for(int i=0;i<commands.length();i++){
    commands[i].undo();
    }
    }
    }
  • 首先,创建想要进入宏的命令集合

    1
    2
    3
    4
    5
    6
    7
    //创建所有装置
    Light light = new Light("Living Room");
    TV tv = new TV("Living Room");
    //创建所有on命令
    LightOnCommand lightOn = new LightOnCommand(light);
    TVOnCommand tvOn = new TVOnCommand(tv);
    //...创建所有off命令...
  • 创建两个数组,一个记录所有开启命令,一个记录所有关闭命令

    1
    2
    3
    4
    5
    Command[] partyOn = {lightOn,tvOn};
    Command[] partyOff = {lightOff,tvOff};
    //创建对应的宏持有这两个数组
    MacroCommand partyOnMarcro = new MacroCommand(partyOn);
    MacroCommand partyOffMacro = new MacroCommand(partyOff);
  • 将宏命令指定给按钮

    1
    remoteControl.setCommand(0,partyOnMacro,partyOffMacro);

命令模式的更多用途:队列请求

老师没讲,暂时不看

命令模式的更多用途:日志请求

老师没讲,暂时不看

总结

  • 命令模式发出请求的对象执行请求的对象解耦
  • 发出请求的对象和执行请求的对象 通过 命令对象 沟通
  • 命令对象 封装命令的接收者一个or一组动作
  • 调用命令对象的excute()发出请求,接收者的动作被调用

适配器模式与外观模式

包装某些对象,让它们的接口看起来不像自己 而是像别的东西;

这样就可以在设计中,将类的接口转换成想要的接口,以便实现不同的接口

  • 适配器:将一个接口转换成另一个接口,以符合客户的期望

  • 面向对象适配器

披着鸭子皮的火鸡

  • 鸭子类

    1
    2
    3
    4
    public interface Duck{
    public void quack();
    public void fly();
    }
  • 火鸡类

    1
    2
    3
    4
    public interface Turkey{
    public void gobble();
    public void fly();
    }//被适配者
  • 假设缺鸭子对象,用火鸡冒充,但是接口不同,所以先写个适配器吧

  • 适配器(TurkeyAdapter)将 被适配者(Turkey) 适配成 目标接口(Duck)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //我们需要的是duck,但是现在只有turkey,所以这个适配器需要实现duck的接口
    //可见:适配器实现了目标接口
    public class TurkeyAdapter implements Duck{
    Turkey turkey;//利用对象组合,用Turkey的实现来满足客户对鸭子的需求
    public TurkeyAdapter(Turkey turkey){
    this.turkey = turkey; //可见:适配器持有 被适配者turkey的实例
    }
    public void quack(){//实现duck的所有方法(适配器实现了目标接口)
    turkey.gobble(); //具体实现其实是turkey的方法,把它封装到duck的quack里而已
    }
    public void fly(){
    for(int i=0;i<5;i++) turkey.fly();
    }
    }
  • 测试适配器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class DuckTestDrive{
    public static void main(String[] args){
    MallardDuck duck = new MallardDuck();
    WildTurkey turkey = new WildTurkey();
    //把这个火鸡包装进适配器中,让它看起来像鸭子
    Duck turkeyAdapter = new TurkeyAdapter(turkey);
    testDuck(turkeyAdapter);
    }
    //testDuck相当于客户,依据目标接口(Duck)实现的:duck.quack,fly
    static void testDuck(Duck duck){
    duck.quack();
    duck.fly();
    }
    }

适配器模式解析

  • 客户通过目标接口 调用 适配器的方法 对适配器发出请求

  • 适配器 使用 被适配者接口,把请求转换成 被适配者的一个或多个调用接口

  • 客户接受到调用的结果,但未察觉适配器在起转换作用

定义适配器模式+类图

适配器模式:将一个类的接口,转换成客户期望的另一个类的接口。适配器让原本接口不兼容的类可以合作无间
  • 通过创建适配器 进行接口转换

  • 好处

    • 适配器 将客户(比如testDuck)从实现的接口解耦
    • 如果想改变接口,适配器可以将改变的部分封装起来
  • 对象适配器

    适配器 和 被适配者 使用对象组合:被适配者的子类都可以搭配适配器使用

  • 适配器

    (Java 不支持多重继承

模板方法模式

最初的茶和咖啡各自的四个步骤里面包括了

  • 同样的方法:把水煮沸、倒进杯子
  • 相似的方法:沸水冲泡茶叶&沸水冲泡咖啡粉、加柠檬&加牛奶
  • 同样的执行步骤:执行顺序相同

因此,可以将其抽象泛化成一个模板方法

模板方法定义

Github:课本代码实现总结

模板方法模式 在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤

理解

  • 所谓模板,本质是个方法,一个用一组步骤来定义算法的方法
    • 保证由子类提供算法某一部分的实现,而算法结构保持不变

对模板方法进行挂钩

钩子:声明在抽象类中的(具体)方法,只有空的或者默认的实现。

有了钩子,子类可以自主决定要不要覆盖这个钩子,如果不覆盖,抽象类会提供一个默认的实现

何时使用抽象方法,何时使用钩子:
  • 子类必须提供算法中某个方法或步骤的实现:抽象方法
  • 不是必须,而是可选的:钩子

子类必须实现抽象类中的所有抽象方法

好莱坞原则与模板方法

模板方法和策略模式的对比

  • 策略模式定义了一个算法族,分别封装起来,让它们之间可以相互替换。每个算法都被封装,客户可以很方便的使用不同的算法,而且将算法的变化独立于客户
  • 模板方法模式定义了一个算法的框架,将一些步骤延迟到子类。这样对于算法的某个步骤就有不同的,但仍能控制整个算法的结构
  • 策略模式和模板方法模式都是封装算法;
    • 策略模式采用对象组合,更加灵活,依赖性比模板方法模式弱
    • 模板方法模式采用继承,对算法有更强的控制,所以依赖性比策略模式强;没有重复代码,对象更少,更高效
  • 注意:工厂方法模式是模板方法模式的一个特例

模板方法的例题

关键词:步骤、流程、某些流程不一样

1.对数据库的操作一般包括连接、打开、使用、关闭等步骤,在数据库操作模板类中我们定义了connDB(),openDB(),useDB(),closeDB()四个方法分别对应这四个步骤,对于不同类型的数据库(如SQLserver和Oracle),其操作步骤都一致,只是连接数据库connDB()方法有所区别,现使用模板方法模式对其进行设计


2.某软件公司欲为某银行的业务支撑系统开发一个利息计算模块,利息计算流程如下:
(1) 系统根据账号和密码验证用户信息,如果用户信息错误,系统显示出错提示;(具体方法)
(2) 如果用户信息正确,则根据用户类型的不同使用不同的利息计算公式计算利息(如活期账户和定期账户具有不同的利息计算公式);(这是个不同的步骤,抽象方法)
(3) 系统显示利息。(具体方法)
试使用模板方法模式设计该利息计算模块。


3.某软件公司欲为销售管理系统提供一个数据图表显示功能,该功能的实现包括如下几个步骤
(1) 从数据源获取数据;
(2) 将数据转换为XML格式;
(3) 以某种图表方式显示XML格式的数据。
该功能支持多种数据源和多种图表显示方式,但所有的图表显示操作都基于XML格式的数据,因此可能需要对数据进行转换,如果从数据源获取的数据已经是XML数据则无须转换


迭代器模式

让客户遍历你的对象而无法窥视你存储对象的方式

显然,这两种不同的菜单表现方式会让事情变得复杂化,我们需要实现一个同时使用这两个菜单的客户代码

  • 比如一个waitress类(客户)需要遍历打印菜单中的每一项 printMenu()

  • 但是这两份存储菜单项的菜单里所使用的数据结构不同

  • 导致printMenu()要分别遍历每份菜单,比如

考虑让这两个菜单实现一个相同的接口

  • 可以看到 发生变化的部分是不同集合类型所造成的遍历,因此可以考虑封装遍历

  • 因此,创建一个对象:迭代器,利用它来封装“遍历集合内的每个对象的过程”

    1
    2
    3
    4
    Iterator iterator = dinerMenu.createIterator();
    while(iterator.hasNext()){
    MenuItem menuItem = (MenuItem)iterator.next();
    }

所以,接下来的具体实现是:

  • 定义一个迭代器接口Iterator
  • 为每个菜单各自定义实现一个具体的Iterator类:DinerMenuIterator、PancakeHouseMenuIterator
  • 改写原菜单:遍历菜单时需要获得它的迭代器,所以原菜单要有一个createIterator()方法来获取对应的迭代器

  • waitress(客户代码)想要遍历所有菜单里面的所有菜单项,只需要获取到对应的迭代器,利用迭代器的方法遍历就行。


利用Java.util.Iterator来清理

  • PancakeHouseMenu:对于ArrayList不需要创建自己的迭代器,直接调用iterator()方法即可
  • DinerMenu:写一个具体的迭代器实现Iterator
  • 让waitress不依赖于具体的DinerMenu和PancakeHouseMenu类:
    • 添加一个Menu接口,只有createIterator()方法
    • 具体的Menu去实现这个Menu接口类
    • 这样waitress可以针对抽象(Menu)编程,而不是针对具体编程,实现解耦

定义迭代器模式

迭代器模式 提供一种方法顺序访问每一个聚合对象中的各个元素,而不暴露其内部的表示


  • 迭代器模式把元素之间游走的责任交给迭代器实现,而不是句合对象。
  • 使我们能够游走(遍历)聚合内的每一个元素,而又不保留其内部的表示

单一责任原则

一个类应该只有一个引起变化的原因


理解

  • 当我们允许一个类不但要完成自己的事情(管理某种集合),还同时担负起更多的责任(例如遍历)时,我们就给了这个类两个变化的原因:

    • 集合改变、类随之改变
    • 遍历方式改变、类随之改变

    比如最初的没有使用迭代器模式的DinerMenuPancakeHouseMenuDinerMenu既需要管理存储菜单项的数组,又需要参与到数组遍历中

  • 类的每个责任都有改变的潜在区域,超过一个责任,意味着超过一个改变的区域

内聚:度量一个类或模块紧密地达到单一目的或责任

  • 具有高内聚的模块或类:只支持一组相关的功能
  • 具有低内聚的模块或类:支持一组不相干的功能


组合模式

基于上面的例子,我们希望在DinerMenu中添加一个甜点的子菜单

因此,我们需要:

  • 需要某种树形结构,可以容纳菜单、子菜单、菜单项
  • 需要确定能够在每个菜单的各个项之间的游走,而且至少要像现在用迭代器一样方便
  • 需要更有弹性地在菜单项之间游走:可以只遍历甜点菜单,可以遍历餐厅整个菜单

组合模式定义

组合模式允许你 将组合对象 组合成树形结构 来表现 “整体/部分“层次结构。组合能让客户以一致的方式处理个别对象以及对象组合


  • 组合模式让我们能够用树形的方式创建对象的结构,树里面包含了组合以及个别的对象
  • 使用组合结构,我们能把相同的操作应用在组合和个别对象上;
    • 即:大多数情况下,可以忽略对象组合和个别对象的差别

利用组合设计菜单

  • 创建一个组件接口,作为菜单和菜单项的共同接口,这样就能用统一的做法来处理菜单和菜单项。
  • 实现菜单组件:MenuComponent
    • 它的目的是为叶节点和组合节点提供一个共同的接口
    • 它的方法要提供默认的实现,这样菜单项或菜单不想实现某些方法 就可以不实现

状态模式

Github:状态模式课本代码实现总结

针对一种像这种状态机的模型

我们把每个状态的行为都放在各自的状态类中,每个状态只要实现它的动作就行

所以用户施加在糖果机上的动作,只需要委托给代表当前状态的状态对象就行===》多用组合,少用继承

  • 将每个状态的行为局部化到它自己的类中
  • 将容易产生问题的if语句删除,方便维护
  • 让每个状态对修改关闭,让糖果机对扩展开放,因此可以加入新的状态类

定义状态模式

状态模式 允许对象在内部状态改变改变它的行为,对象看起来好像修改了它 的

理解:

  • 将状态封装成独立的,并将动作委托到代表当前状态的对象,行为随着内部状态而改变
    • 比如:糖果机处于不同状态,对它执行相同动作,得到的结果可能不同
  • 从客户的视角,如果说你使用的对象能够改变它的行为,那你就会觉得,这个对象实际上是从别的类实例化而来的,而实际上是使用组合通过简单引用不同的 状态对象造成的假象

状态模式和策略模式对比

它俩类图一样,但是意图不一样

所以说,千万不要从类图上区分模式,要从它的用途和目的去区分

  • 状态模式:将 一群行为封装在状态对象中,行为对于客户来说是透明的。正常情况下,客户不知道状态对象的存在

    Contex的行为可以随时委托给那些状态对象中第一个,随着时间流逝

    当前状态在状态对象集合中游走改变,以反映出context内部的状态,因此 context的行为也会跟着改变

    但是!!!!context的客户对于状态对象了解不多,甚至浑然不觉

    可以把状态模式想成是不用在context中放置许多条件判断的替代方案

    通过将行为包装进状态对象,可以通过在context内简单地改变状态对象改变context的行为

  • 策略模式中,客户通常委托所要组合的对象改变行为,由客户决定做改变

    客户通常 主动指定Context所要组合的策略对象是哪一个

    可以把策略模式想成除了继承之外的 一种弹性替代方案

    • 如果使用继承定义了一个类的行为,可能被整个行为困住
    • 但通过策略模式,可以通过组合不同的对象来改变行为
  • 代码对比

  • 例如上图策略模式的客户代码,客户是明确知道拥有的策略对象,可以由客户自己随意指定
  • 例如下图状态模式的客户代码,客户只知道要进行这些操作,而这些操作的具体实现是委托给具体的状态的,客户根本不知道当前的状态对象

  • 即是:状态模式中,客户调用gumballMachine.insertQuarter()方法,实际上会委托给持有的state状态对象insertQuarter来执行,从而进行状态转换

状态转换由谁执行

Github:一个代码例子

  • 当状态转换是固定的,就适合放在Context类
  • 当状态转换是更动态的,通常会放在状态类
    • 缺点:状态间产生了依赖

一个练习

信用卡业务系统

​ Sunny软件公司欲为某银行开发一套信用卡业务系统,银行账户(Account)是该系统的核心类之一,通过分析,Sunny软件公司开发人员发现在该系统中,账户存在三种状态,且在不同状态下账户存在不同的行为,具体说明如下:

  • 如果账户中余额大于等于0,则账户的状态为\正常状态(Normal State)**,此时用户既可以向该账户\存款**也可以从该账户\取款**

  • 如果账户中余额小于0,并且大于-2000,则账户的状态为\透支状态(Overdraft State)**,此时用户既可以向该账户存款也可以从该账户取款,但需要\按天计算利息**

  • 如果账户中余额等于-2000,那么账户的状态为\受限状态(Restricted State)**,此时用户只能向该账户存款,不能再从中取款,同时也将按天计算利息;

根据余额的不同,以上\三种状态可发生相互转换**

stateCheck()用于在每一次执行存款和取款操作后根据余额来判断是否要进行状态转换并实现状态转换,相同的方法在不同的状态中可能会有不同的实现。

上面的例子通过具体状态类来实现状态的转换,在每一个具体状态类中都包含一个stateCheck()方法,在该方法内部实现状态的转换。

除此之外,我们还可以通过Context类来实现状态转换,Context类作为一个状态管理器,统一实现各种状态之间的转换操作


多个Context类共享 状态

在有些情况下,多个Context可能需要共享同一个状态

  • 如果希望在系统中实现多个Context共享一个或多个状态对象

  • 则可以将这些State定义为Context的静态成员对象

    即,想要共享状态,需要把每个状态都指定到静态的实例变量中,如果状态需要用到Context的方法或者实例变量,还必须在handler()方法中传入一个context的引用


关于为什么定义为静态成员对象:

  • 属于类,独立于对象,即使未创建对象,也可以通过类调用静态的属性和方法

  • 由于static成员为所有实例对象所共享,当业务需求出现某个成员需要被所有实例对象所需要时,应将其设为static,当类的任何对象访问它时,存取到的都是相同的值

    • 某个成员:可以对应某个状态
    • 被所有实例对象所需要:这个状态被多个Context共享
  • static变量只会在类加载时被分配一次且一次空间,而实例对象会在每次创建新对象时都会为其分配一次新的空间

  • static成员可以通过类名访问,也可以通过对象名访问

  • static方法中只允许访问static属性或方法,不允许访问非static成员(因为非static成员在初次类加载时并未初始化)


例如:如果某系统要求两个开关对象(Context)要么处于开的状态,要么都处于关的状态,在使用时它们的状态必须保持一致,开关可以由开转换到关,也可以由关转换到开。

分析:

  • 两个开关对象——两个Context
  • 都处于开,都处于关——共享开状态和共享关状态
  • 因此,需要把这两个state定义为context的静态成员对象