admin管理员组

文章数量:1539570

文章目录

  • 二十三种设计模式
    • 代码设计原则
    • 为什么要学习设计模式
    • 第一种设计模式 策略设计模式
    • 观察者(Observe)模式
    • 装饰者模式
    • 工厂模式
      • 简单工厂模式
      • 工厂方法模式
      • 依赖倒置原则
      • 抽象工厂模式
      • 工厂方法和抽象工厂模式的区别
    • 命令模式
    • 适配器模式
    • 模板方法模式
    • 状态模式
    • 责任链设计模式
    • 我们学习了这么多种设计模式,那么我们什么时候使用它们呢?

二十三种设计模式

代码设计原则

OO面向对象设计原则:

  • 封装变化。
  • 多用组合,少用继承。
  • 针对接口编程,而不是针对实现类编程。
  • 为交互对象之间的松耦合设计而努力。
  • 业务类应该对扩展开放,对修改关闭。
    依赖倒置原则(其实也就是依赖抽象,而不要依赖具体的实现类)
  • 别找我,我会找你。(模板方法模式)
  • 一个类只给他一个责任,只让他实现一个功能。

为什么要学习设计模式

设计模式都是一些牛逼的开发人员,它们总结的智慧和经验,如果你遇到了和他们一样的问题,设计模式可以帮助你少走弯路,帮助你解决问题。使用设计模式最好的方法就是,把设计模式都装进脑子里,然后在你的设计和已有的中,去寻找何处可以使用它们。以往是代码的复用,但我们这里是经验的复用。

知道抽象,继承,多态这些概念,并不会马上让你变成好的面向对象设计者。设计大师关心的是建立弹性的设计,可以维护,可以应付改变。

第一种设计模式 策略设计模式

假如你在公司使用继承完成了一个鸭子游戏,你设计了一个鸭子超类,并让各种鸭子继承此超类。超类如下:

//鸭子超类
class Duck{
    public void swim(){
        System.out.println("我正在游泳");
    }
    public abstract void display();
}
//因为所有的鸭子都会游泳,所以可以直接在超类中写上swim方法的具体实现。但是我们在电脑屏幕上要显示的鸭子的外观可能不同,有的鸭子是红色的头,有的鸭子是白色的头,有的鸭子是绿色的头,因此我们需要把display方法写成是抽象方法,然后让子类具体的去实现它。

//绿头鸭子
class MallardDuck extends Duck{
    public void display(){
        System.out.println("在电脑屏幕上显示的是绿头鸭子");
    }
}

//红头鸭子
class RedHeadDuck extends Dunck{
    public void display(){
        System.out.println("在电脑屏幕上显示的是红头鸭子");
    }
}

假如你在公司做的鸭子游戏效果还算不错,而你设计的时候,主要是使用上面的继承体系来设计的,并且你觉得你设计的非常完美,可扩展性也算不错。

但是后面,公司有了一个紧急需求,它们需要让有的鸭子会飞,就把这个需求交给你了,你拿到这个需求之后,觉得非常简单,你觉得很容易就可以完成一个需求,你就需要在超类中加入一个fly()方法就行了,

Duck
swim()
display()
fly()

这样子类继承它的时候就会变成一个会飞的鸭子。你兴致勃勃的和你的主管说你的想法。但是换来的确实你的主管的一顿臭骂,你主管说,你如果这样设计的话,那么所有的子类鸭子都会获取到会飞的行为,到时候在用户的电脑屏幕上,当他玩你设计的鸭子游戏的时候,在屏幕上就会出现会飞的木头鸭子,或者是会飞的纸盒鸭子。

听完主管的话,你思考了一下,确实是这样的,然后你又想出了一个方案,就是你不把fly的行为加到Duck超类里面,你单独写一个Flyable接口,然后把fly()方法放到Flyable接口里面,如果是木头鸭子子类或者是纸盒鸭子子类,就不让它们实现Flyable接口,这样我们屏幕上就不会出现会飞的木头鸭子和纸盒鸭子了。即:

class Duck{
    public void swim(){
        System.out.println("我正在游泳");
    }
    public abstract void display();
}

interface Flyable{
    void fly();
}

//绿头鸭子
class MallardDuck extends Duck implements Flyable{
    public void display(){
        System.out.println("在电脑屏幕上显示的是绿头鸭子");
    }
    
    public void fly(){
        System.out.println("我正在用翅膀飞翔");
    }
}

//红头鸭子
class RedHeadDuck extends Duck implements Flyable{
    public void display(){
        System.out.println("在电脑屏幕上显示的是红头鸭子");
    }
    
    public void fly(){
        System.out.println("我正在用翅膀飞翔");
    }
}

//木头鸭子,因为木头鸭子不会飞行,所以不用让其实现Flyable接口
class WoodDuck extends Dunck{
    public void display(){
        System.out.println("在电脑屏幕上显示的是红头鸭子");
    }
}

经过上面的改进,你觉得你的代码变得非常完美了,再也不会有会飞的木头鸭子出现在游戏玩家的屏幕上了,但你没想到的是,你的主管对此还是不满意,你的主管说,如果你这样设计,你的程序中就会出现很多重复的代码,比如说你上面的红头鸭子和绿头鸭子,它们的飞行行为都是一样的,只需要一个fly()的具体实现代码就行了,但是你在红头鸭子和绿头鸭子中的fly()方法里写了两段重复的代码,那假如后面又出现了成千上万个相同飞行行为的鸭子,你是不是要写成千上万段重复的代码。

听完主管的话之后,你觉得又很有道理,但你现在需要怎么做呢?你脑子里突然冒出了一个个软件设计原则:**找出应用中可能需要变化之处,把他们独立出来,不要和那些不需要变化的代码混在一起。**换句话说,如果每次新的需求一来,都会使某方面的代码发生变化,那么你就可以确定,这部分代码需要被抽出来,和其它稳定的代码有所区分。把会变化的部分取出并封装起来,以便以后可以轻易的改动或扩充此部分,而不影响不需要变化的其它部分。其实也就是把会变化的部分抽取出来,抽取成一个接口,然后把这个接口和原来的类组合起来。

因为有的鸭子不会飞行,有的鸭子飞行的时候翅膀幅度很大,有的鸭子飞行的时候翅膀幅度很小,所以飞行行为算是会变化的东西。因此我们有一个接口FlyBehavior,还有它对应的类,负责实现具体的行为,如下:

interface FlyBehavior{
    void fly();
}

class FlyNoWay implements FlyBehavior{
    void fly(){
        System.out.println("不会飞行");
    }
}

class FlyFast implements FlyBehavior{
    void fly(){
        System.out.println("飞行的时候翅膀扇动频率很快");
    }
}

class FlySlow implements FlyBehavior{
    void fly(){
        System.out.println("飞行的时候翅膀扇动频率很慢");
    }
}

假如红头鸭子飞行的时候翅膀扇动频率很快,绿头鸭子飞行的时候翅膀扇动频率很慢,木头鸭子不会飞行,那么我们就可以使用组合对象的方式,把鸭子飞行的行为委托给其它对象,如下:

class RedHeadDuck extends Duck{
    FlyBehavior flyBehavior;
    
    public RedHeadDuck(){
        flyBehavior = new FlyFast();
    }
    
    public void display(){
        System.out.println("在电脑屏幕上显示的是红头鸭子");
    }
    
    public void fly(){
        flyBehavior.fly();
    }
}

class MallardDuck extends Duck{
    FlyBehavior flyBehavior;
    
    public MallardDuck(){
        flyBehavior = new FlySlow();
    }
    
    public void display(){
        System.out.println("在电脑屏幕上显示的是绿头鸭子");
    }
    
    public void fly(){
        flyBehavior.fly();
    }
}

class WoodDuck extends Dunck{
    FlyBehavior flyBehavior;
    
    public WoodDuck(){
        flyBehavior = new FlyNoWay();
    }
    
    public void display(){
        System.out.println("在电脑屏幕上显示的是绿头鸭子");
    }
    
    public void fly(){
        flyBehavior.fly();
    }
}

//像上面的设计,就是把鸭子的行为不由鸭子类处理,而是委托给其它的类处理。这也是我们的一个设计原则,要多使用组合,少使用继承。这样一来我们即使用了继承代码的复用的好处,又没有了继承带来的包袱。并且使用上面的设计之后,就不会再出现上一阶段的代码复用了,一段相同的代码只会出现一次。

上面的这种设计模式叫做策略设计模式。

观察者(Observe)模式

认识观察者模式。我们看看报纸和杂志的订阅是怎么回事:

  1. 报社的业务就是出版报纸。
  2. 向某家报社订阅报纸,只要他们有新报纸出版,就会给你送来。只要你是他们的订户,你就会一直收到新报纸。
  3. 当你不想再看报纸的时候,取消订阅,他们就不会再送新报纸来。
  4. 只要报社还在运营,就会一直有人(或单位)向他们订阅报纸或取消订阅报纸。

出版社+订阅者=观察者模式

如果你了解报纸的订阅是怎么回事,其实就知道观察者模式是怎么回事,值是名称不一样:出版社改称为“主题”(Subject),订阅者改称为“观察者”(Observer)。

当你试图勾勒观察者模式时,可以利用报纸订阅服务,以及出版社和订阅者比拟这一切。在真实世界中,你通常会看到观察者模式被定义成:定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。主题和观察者定义了一对多的关系。观察者依赖于此主题,只要主题状态一有变化,观察者就会被通知。根据通知的风格,观察者可能因此新值而更新。

松耦合的威力。当两个对象之间松耦合,它们依然可以交互,但是不太清除彼此的细节。观察者模式提供了一种对象设计,让主题和观察者之间松耦合。为什么呢?关于观察者的一切,主题只知道观察者实现了某个接口(也就是Observer接口)。主题不需要知道观察者的具体类是谁,做了些什么或其它任何细节。任何时候我们都可以增加新的观察者。因为主题唯一依赖的东西是一个实现Observer接口的对象列表,所以我们可以随时增加观察者。改变主题或观察者其中一方,并不会影响另一方。因为两者是松耦合的,所以只要他们之间的接口仍被遵守,我们就可以自由地改变他们。松耦合的设计之所以能让我们建立有弹性的OO系统,能够应对变化,是因为对象之间的互相依赖降到了最低。

下面是一个观察者模式的例子:

有一个气象站(相当于一个主题Subject),气象站可以随时监测到空气中的湿度,温度和气压。我们有很多个气象展示面板(相当于是多个观察者),每当气象站的状态更新之后,所有的展示面板数据都会更新。下面是代码:

//主题接口,也就是被观察者接口
public interface Subject{
    //往主题中注册一个观察者
    public void registerObserver(Observer o);
    //从主题中删除一个观察者
    public void removeObserver(Observer o);
    //通知所有订阅主题的观察者,提醒它们改变信息
    public void notifyObservers();
}

//观察者接口
public interface Observer{
    //当气象站的气象观测值发生改变之后,观察者也就是气象信息展示面板中的信息需要更新
    public void update(float temp,float humidity,float pressure);
}

//把气象信息展示到面板的时候,需要此接口
public interface DisplayElement{
    //当面板需要显示时,调用此方法
    public void display();
}

//定义气象站,主题
public class WeatherData implements Subject{
    //所有订阅主题的观察者
    private ArrayList<Observer> observers = new ArrayList<Observer>();
    //气象站检测空气中的温度
    private float temperature;
    //气象站检测空气中的湿度
    private float humidity;
    //气象站检测空气中的压力
    private float pressure;
    public WeatherData(){

    }

    //把观察者注册到主题中
    public void registerObserver(Observer o){
        observers.add(o);
    }

    //把观察者从主题中删除
    public void removeObserver(Observer o){
        int i = observers.indexOf(o);
        if(i >= 0){
            observers.remove(i);
        }
    }

    //通知所有的观察者
    public void notifyObservers(){
        for (int i = 0; i < observers.size(); i++){
            Observer observer = observers.get(i);
            observer.update(temperature,humidity,pressure);
        }
    }

    //测量改变
    public void measurementChanged(){
        notifyObservers();
    }

    //气象站检测到空气中的指数变化之后,调用测量改变的方法
    public void setMeasurement(float temperature,float humidity,float pressure){
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementChanged();
    }

}

//观察者一
public class CurrentConditionsDisplay implements Observer, DisplayElement {
    private float temperature;
    private float humidity;
    private float pressure;
    private Subject weatherData;

    //在创建观察者的时候,就把观察者注册到相应的主题里面
    public CurrentConditionsDisplay(Subject weatherData){
        this.weatherData = weatherData;
        weatherData.registerObserver(this);
    }

    //主题通知观察者更新,是通过update方法
    public void update(float temperature,float humidity,float pressure){
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        display();
    }

    //观察者取消订阅主题
    public void cancel(){
        weatherData.removeObserver(this);
    }

    public void display(){
        System.out.println("观察者一 --》 当前温度:" + temperature +" 当前湿度:" + humidity + " 当前气压:" + pressure);
    }
}


//观察者二
public class CurrentConditionsDisplay2 implements Observer, DisplayElement {
    private float temperature;
    private float humidity;
    private float pressure;
    private Subject weatherData;

    //在创建观察者的时候,就把观察者注册到相应的主题里面
    public CurrentConditionsDisplay2(Subject weatherData){
        this.weatherData = weatherData;
        weatherData.registerObserver(this);
    }

    //主题通知观察者更新,是通过update方法
    public void update(float temperature,float humidity,float pressure){
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        display();
    }

    //观察者取消订阅主题
    public void cancel(){
        weatherData.removeObserver(this);
    }

    public void display(){
        System.out.println("观察者二 --》 当前温度:" + temperature +" 当前湿度:" + humidity + " 当前气压:" + pressure);
    }
}

//测试
public class Demo {
    public static void main(String[] args) {
        WeatherData weatherData = new WeatherData();
        //观察者一注册到主题
        CurrentConditionsDisplay currentConditionsDisplay = new CurrentConditionsDisplay(weatherData);
        //观察者二注册到主题
        CurrentConditionsDisplay2 currentConditionsDisplay2 = new CurrentConditionsDisplay2(weatherData);

        //主题状态更新之后,观察者一和观察者二都可以收到通知
        weatherData.setMeasurement(36,80,18);
        weatherData.setMeasurement(40,84,20);
        weatherData.setMeasurement(34,83,29);

    }
}

//输出结果
观察者一 --》 当前温度:36.0 当前湿度:80.0 当前气压:18.0
观察者二 --》 当前温度:36.0 当前湿度:80.0 当前气压:18.0
观察者一 --》 当前温度:40.0 当前湿度:84.0 当前气压:20.0
观察者二 --》 当前温度:40.0 当前湿度:84.0 当前气压:20.0
观察者一 --》 当前温度:34.0 当前湿度:83.0 当前气压:29.0
观察者二 --》 当前温度:34.0 当前湿度:83.0 当前气压:29.0

到目前为止,我们已经从无到有地完成了观察者模式了,但是,Java Api有内置的观察者模式。java.util包(package)内包含最基本的Observer接口与Observable类,这和我们的Subject接口与Observer接口很相似。Observer接口与Observable类使用上更方便,因为许多功能都已经事先准备好了。

Java内置的观察者模式运作方式,和我们在气象站中的实现类似,但有一些小差异。最明显的差异是WeatherData(也就是我们的主题)现在扩展自Observable类,并继承到一些增加,删除,通知观察者的方法(以及其他方法)。Java版本的用法如下:

如何把对象变成观察者?如同以前一样,实现观察者接口(java.util.Observer),然后调用任何Observale对象的addObserver()方法。不想再当观察者时,调用deleteObserver()方法就可以了。可被观察者也就是主题要如何送出通知呢?首先,你需要利用扩展java.util.Observalbe接口产生“可观察者”类,也即是主题类。然后需要两个步骤:

  1. 先调用setChanged()方法,标记状态已经改变的事实。
  2. 然后调用两种notifyObservers()方法中的一个。notifyObservers()或notifyObservers(Object arg)当通知时,此版本可以传送任何的数据对象给每一个观察则会。观察者里对应的方法update(Observale o,Object arg),主题本身当做第一个变量好让观察者知道是哪个主题通知它的。第二个参数arg正是传入notifyObservers的数据对象。

如果你想要“推”(push)数据给观察者,你可以把数据当做数据对象传送给notifyObservers(arg)方法。否则,观察者就必须从可观察者对象中“拉”(pull)数据。

setChanged()方法用来标记状态已经改变的事实,好让notifyObservers()知道当它被调用时应该更新观察者。如果调用notifyObservers()之前没有先调用setChanged(),观察者就不会被通知。让我们看看Observale内部,以了解这一切:

setChanged(){
	changed = true;
}

notifyObservers(Object arg){
    if(changed){
        for every observer on the list{
            call update(this,arg);
        }
        changed = false;
    }
}

notifyObservers(){
    notifyObservers(null);
}

这样做有其必要性。setChanged()方法可以让你更新观察者时,有更多的弹性,你可以更适当地通知观察者。比方说,如果没有setChanged()方法,我们的气象站测量是如此敏锐,以至于温度计读数每十分之一度就会更新,这会造成WeatherData对象持续不断地通知观察者,我们并不希望看到这样的事情发生。如果我们希望半度以上才更新,就可以在温度差距到达半度时,调用setChanged(),进行有效的更新。

使用java内置的Api重写观察者模式,如下:

//主题
public class WeatherData extends Observable {
    private float temperature;
    private float humidity;
    private float pressure;

    public WeatherData(){

    }

    //我们这里主题在通知观察者的时候,调用的是notifyObservers()方法,没有主动的传输数据对象,也就是在此方法里加一个数据参数。
    //我们称这种方式为pull拉,如果给参数加了一个数据参数,我们称这种方式为push
    public void measurementsChanged(){
        setChanged();
        notifyObservers();
    }

    public void setMeasurements(float temperature,float humidity,float pressure){
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }

    public float getTemperature(){
        return  temperature;
    }

    public float getHumidity(){
        return humidity;
    }

    public float getPressure(){
        return  pressure;
    }
}

//观察者一
class CurrentConditionsDisplay implements Observer, DisplayElement {
    private float temperature;
    private float humidity;
    private float pressure;
    private Observable observable;

    //在创建观察者的时候,就把观察者注册到相应的主题里面
    public CurrentConditionsDisplay(Observable observable){
        this.observable = observable;
        observable.addObserver(this);
    }

    //主题通知观察者更新,是通过update方法
    public void update(Observable observable,Object args){
        if(observable instanceof WeatherData){
            WeatherData weatherData = (WeatherData)observable;
            this.temperature = weatherData.getTemperature();
            this.humidity = weatherData.getHumidity();
            this.pressure = weatherData.getPressure();
            display();
        }
    }

    //观察者取消订阅主题
    public void cancel(){
        observable.deleteObserver(this);
    }

    public void display(){
        System.out.println("观察者一 --》 当前温度:" + temperature +" 当前湿度:" + humidity + " 当前气压:" + pressure);
    }
}

//观察者二
class CurrentConditionsDisplay2 implements Observer, DisplayElement {
    private float temperature;
    private float humidity;
    private float pressure;
    private Observable observable;

    //在创建观察者的时候,就把观察者注册到相应的主题里面
    public CurrentConditionsDisplay2(Observable observable){
        this.observable = observable;
        observable.addObserver(this);
    }

    //主题通知观察者更新,是通过update方法
    public void update(Observable observable,Object args){
        if(observable instanceof WeatherData){
            WeatherData weatherData = (WeatherData)observable;
            this.temperature = weatherData.getTemperature();
            this.humidity = weatherData.getHumidity();
            this.pressure = weatherData.getPressure();
            display();
        }
    }

    //观察者取消订阅主题
    public void cancel(){
        observable.deleteObserver(this);
    }

    public void display(){
        System.out.println("观察者二 --》 当前温度:" + temperature +" 当前湿度:" + humidity + " 当前气压:" + pressure);
    }
}

//测试类
public class Demo {
    public static void main(String[] args) {
        com.atguigu.test.WeatherData weatherData = new WeatherData();
        //观察者一注册到主题
        com.atguigu.test.CurrentConditionsDisplay currentConditionsDisplay = new CurrentConditionsDisplay(weatherData);
        //观察者二注册到主题
        com.atguigu.test.CurrentConditionsDisplay2 currentConditionsDisplay2 = new CurrentConditionsDisplay2(weatherData);

        //主题状态更新之后,观察者一和观察者二都可以收到通知
        weatherData.setMeasurement(36,80,18);
        weatherData.setMeasurement(40,84,20);
        weatherData.setMeasurement(34,83,29);

    }
}

//输出结果
观察者一 --》 当前温度:36.0 当前湿度:80.0 当前气压:18.0
观察者二 --》 当前温度:36.0 当前湿度:80.0 当前气压:18.0
观察者一 --》 当前温度:40.0 当前湿度:84.0 当前气压:20.0
观察者二 --》 当前温度:40.0 当前湿度:84.0 当前气压:20.0
观察者一 --》 当前温度:34.0 当前湿度:83.0 当前气压:29.0
观察者二 --》 当前温度:34.0 当前湿度:83.0 当前气压:29.0

java.util.Observable的黑暗面

是的,你注意到了!如同你所发现的,可观察者是一个“类”而不是一个“接口”,更糟的是,它甚至没有实现一个接口。不幸的是,java.util.Observable的实现有许多问题,限制了它的使用和复用。这并不是说它没有提供有用的功能,我们只是想提醒大家注意一些事实。

Observale是一个类

你已经从我们的原则中得知这不是一件好事,但是,这到底会造成什么问题呢?

首先因为Observabel是一个“类”,你必须设计一个类继承它。如果某类想要同时具有Observable类和另一个超类的行为,就会陷入两难,毕竟Java不支持多重继承。这限制了Observable的复用潜力。

Observable将关键的方法保护起来

如果你看看Observable API,你会发现setChanged()方法被保护起来了(被定义成protected),哪又怎么样呢?这意味着:除非继承自Observable,否则你无法创建Observable实例并组合到你自己的对象中来。这个设计违反了第二个设计原则:“多使用组合,少使用继承”。

装饰者模式

一旦你熟悉了装饰的技巧,你将能够在不修改任何底层代码的情况下,给你的(或别人的)对象赋予新的职责。

我们下面会举一个星巴克咖啡的例子,首先掌握几个用英文单词:

Coffee 咖啡

CappuccinoCoffee 卡布奇诺咖啡

LatteCoffee 拿铁咖啡

CaramelCoffee 焦糖咖啡

SteamedMilk 燕奶

Soy 豆浆

Mocha 摩卡

我们现在有一个Coffee类,这个类是一个抽象类,店内所提供的所有咖啡都必须继承自此类,如下图:

上图中,Coffee是咖啡抽象类,星巴克店内所有的咖啡种类都需要继承自此抽象类。此抽象类当中的cost()是一个抽象方法,没有具体的实现,后期具体子类根据cost()方法设置此种咖啡对应的价格;而getDescription()方法则是输出description描述,后期可以由具体的子类具体的设计。

初看这样的继承体系,确实挺不错的,可以实现多态。但是这种设计方式有没有什么问题呢?有的。比如如果你想在店内出新的咖啡种类,出了三种新的咖啡,加有燕奶的卡布奇诺咖啡CappuccionoConffeeWithSteamedMilk,加有豆浆的卡布奇诺咖啡CappuccionoConffeeWithSoy,加有摩卡的卡布奇诺咖啡CappuccionoConffeeWithMocha,那你是不是就需要在原来的体系中加入这三种新的子类,如果后面拿铁咖啡和焦糖咖啡中也想加入一些装饰品,那么是不是我们的类体系结构中的类就更多了呢?这样我们的设计是不是就非常臃肿了呢?简直就是类爆炸。

**那么我们该怎么解决这个类爆炸的问题呢?**我们可以把各种装饰品放到Coffee抽象类中,如下图:

从上图中可以看出来,我们把所有的咖啡装饰品,stramedMilk热奶,soy豆浆,mocha摩卡都放到了抽象类Coffee中;然后设置了一些set,has方法,去判断我们当前的子类中是否需要加入这些装饰品;比如我们现在如果想要加有燕奶的卡布奇诺咖啡CappuccionoConffeeWithSteamedMilk,加有豆浆的卡布奇诺咖啡CappuccionoConffeeWithSoy,加有摩卡的卡布奇诺咖啡CappuccionoConffeeWithMocha,我们就不必再多创建三个子类了,我们只需要在卡布奇诺咖啡CappuccionoConffee子类中,根据具体的情况设置stramedMilk热奶,soy豆浆,mocha摩卡这三种属性的值就行了;这样我们在最后调用cost()方法计算当前咖啡的价格的时候,就可以根据它是否有装饰品即调用hasXxx()去判断该不该加入具体的价格了。

像上面这种设计思路确实是可以避免类爆炸,产生很多的子类,我们现在完美的解决了类爆炸的问题。但是会不会存在新的问题呢?我们软件设计有一个原则是业务类应该对扩展开放,对修改关闭。如果是按照我们上面的设计,现在只有三种装饰品stramedMilk热奶,soy豆浆,mocha摩卡,那如果我们再加入第四种装饰品的话,我再计算卡布奇诺咖啡CappuccionoConffee的具体价格的时候,我是不是要修改一下原有的cost()方法,因为原有的cost()方法没有对第四种装饰进行判断,如果咖啡中有第四种装饰品,它的价格需要增加,因此目前的问题是我们的业务类没有对修改关闭,我们后期还是不得不修改我们的业务类的代码,所以当前的设计原则肯定不是最优的设计原则。那么我们该怎么设置最优的设计原则呢?

答案是,像解决这种牵涉到装饰品的现实问题,比如说咖啡里面加豆浆,燕奶,摩卡,茶水里面加红茶叶,绿茶叶,像解决这种现实问题,最优的解决策略是装饰者模式。那怎样把上面的设计改成是装饰者模式呢?

我们要以咖啡为主体,然后在运行时以调料来装饰咖啡。比方说,如果顾客想要加豆浆和加摩卡的卡布奇诺咖啡,那我们需要做四步:

  • 拿一个卡布奇诺CappuccionoConffee咖啡对象。
  • 以豆浆Soy对象装饰它
  • 以摩卡Mocha对象装饰它
  • 调用cost()方法,并依赖委托将调料的价格加上去

**好了!但如何“装饰”一个对象,而“委托”又如何与此搭配使用呢?给一个暗示:把装饰者对象当成“包装者”。**结果如下图:

现在加入我们要计算一个加油豆浆和摩卡的卡布奇诺咖啡的总的价格,我们就可以直接调用最外层的装饰对象Mocha的cost()方法,此方法返回的就是Mocha装饰品+Soy装饰品+卡布奇诺咖啡的总的价格。

装饰者模式动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案。让我们看一下咖啡装饰者模式的类图,如下图:

把装饰者模式的设计变成真正的代码,如下:

//先写顶层的Coffee抽象类
public abstract class Coffee {
    String description = "Unknown Coffee";
    
    public String getDescription() {
        return description;
    }
    
    public abstract double cost();
}

//然后写Coffee装饰者顶层父类
public abstract class CoffeeDecorator extends Coffee {
    public abstract String getDescription();
} 

//写具体的咖啡子类
public class CappuccinoCoffee extends Coffee {
    public CappuccinoCoffee() {
        description = "cappuccinoCoffee";
    }
    
    public double cost() {
        return 1.99;
    }
}

public class LatteCoffee extends Coffee {
    public LatteCoffee() {
        description = "latteCoffee";
    }
    
    public double cost() {
        return 2.89;
    }
}

public class CaramelCoffee extends Coffee {
    public CaramelCoffee() {
        description = "caramelCoffee";
    }
    
    public double coast() {
        return 1.77;
    }
}

//写具体的装饰品对象
public class Soy extends CoffeeDecorator {
    Coffee coffee;
    
    public Soy(Coffee coffee) {
        this.coffee = coffee;
    }
    
    public String getDescription() {
        return coffee.getDescription() + ",Soy";
    }
    
    public double cost() {
        return 0.56 + coffee.cost();
    }
}

public class Mocha extends CoffeeDecorator {
    Coffee coffee;
    
    public Soy(Coffee coffee) {
        this.coffee = coffee;
    }
    
    public String getDescription() {
        return coffee.getDescription() + ",Mocha";
    }
    
    public double cost() {
        return 0.71 + coffee.cost();
    }
}

public class SteamedMilk extends CoffeeDecorator {
    Coffee coffee;
    
    public Soy(Coffee coffee) {
        this.coffee = coffee;
    }
    
    public String getDescription() {
        return coffee.getDescription() + ",SteamedMilk";
    }
    
    public double cost() {
        return 0.56 + coffee.cost();
    }
}

//测试代码
public class Test {
    public static void main(String[] args) {
        CappuccinoCoffee cappuccinoCoffee = new CappuccinoCoffee();
        System.out.println(cappuccinoCoffee.getDescription() + ",价格为" + cappuccinoCoffee.cost());
        
        Coffee cappuccinoCoffee2 = new CappuccinoCoffee();
        cappuccinoCoffee2 = new Soy(cappuccinoCoffee2);
        cappuccinoCoffee2 = new Mocha(cappuccinoCoffee2);
        System.out.println(cappuccinoCoffee2.getDescription() + ",价格为" + cappuccinoCoffee2.cost());
    }
}

//输出结果为
caramelCoffee,价格为1.77
caramelCoffee,Soy,Mocha,价格为3.26

用了上面的装饰者设计模式之后,可以发发现,我们的程序扩展性大大提高了,现在是对业务类的修改完全关闭了,对扩展开放。如果我们想要加一个带有不同装饰品的卡布奇诺咖啡,只需要新加一个装饰品对象就行了,直接扩展一个装饰品新对象。

工厂模式

简单工厂模式

CappuccinoCoffee 卡布奇诺咖啡

LatteCoffee 拿铁咖啡

CaramelCoffee 焦糖咖啡

使用new的方式创建一个对象,而不使用工厂模式,有什么缺点呢?先看一下下面的这个例子,如下:

Coffee orderCoffee(String type) {
    Coffee coffee;
    
    if(type.equals("cappuccinoCoffee")) {
        coffee = new CappuccinoCoffee();
    } else if(type.equals("latteCoffee")) {
        coffee = new LatteCoffee();
    } else if(type.equals("caramelCoffee")) {
        coffee = new CaramelCoffee();
    }
    
    coffee.cup(); //咖啡装杯
    coffee.box(); //咖啡装盒
}

像上面的业务方法orderCoffee咖啡订单中,目前我们是有三种咖啡,我们会根据调用者传入的咖啡类型,去决定实例化哪种咖啡。但是如果后期我们想要再增加一种咖啡种类,我们是不是就必须要修改orderCoffee这个方法,再增加一个if条件。答案是肯定的。因此我们就违背了软件设计的原则“对修改关闭,对扩展打开”;

因此我们可以把会产生变化的东西单独封装起来,只保存不会变化的东西。像创建对象是会发生变化的,因此我们需要封装起来,而像咖啡装杯和咖啡装盒,这两个流程是固定的,因此不用封装。

封装创建对象的代码如下:

//建立一个简单的咖啡工厂
public class SimpleCoffeeFactory {
    public Coffee createCoffee(String type) {
        Coffee coffee = null;
        
        if(type.equals("cappuccinoCoffee")) {
        	coffee = new CappuccinoCoffee();
    	} else if(type.equals("latteCoffee")) {
        	coffee = new LatteCoffee();
    	} else if(type.equals("caramelCoffee")) {
        	coffee = new CaramelCoffee();
    	}
    }
}

//重做CoffeeStore类
public class CoffeeStore {
    SimpleCoffeeFactory factory;
    
    public CoffeeStore(SimpleCoffeeFactory factory) {
        this.factory = factory;
    }
    
    public Coffee orderCoffee(String type) {
    	Coffee coffee;
    	factory.createCoffee(type);
    
    	coffee.cup(); //咖啡装杯
    	coffee.box(); //咖啡装盒
        return coffee;
	}
}

像上面这样写一个简单工厂,就可以把我们创建对象的逻辑和我们真实的业务逻辑分离开,达到一个解耦的效果,如果后期我们添加了新的咖啡类型,我们只需要去Factory工厂中去扩展就行,不用修改原有的业务代码逻辑,也就是不用修改orderCoffee这里面的逻辑。

工厂方法模式

我们现在可以通过SimpleCoffeeFactory简单工厂,创建出普通的卡布奇诺咖啡,拿铁咖啡,还有焦糖咖啡。但是如果我们现在根据顾客的需求,需要在咖啡店内增加美国口味的卡布奇诺咖啡,我需要怎么办呢?当前的简单工厂SimpleCoffeeFactory只能创建普通的卡布奇诺咖啡,因此我要新建一类工厂AmericaCoffeeSimpleFactory,这类工厂专门生产美国口味的咖啡,以满足不同的顾客需求,如下:

public class AmericaCoffeeSimpleFactory {
    public Coffee createCoffee(String type) {
        Coffee coffee = null;
        
        if(type.equals("americaCappuccinoCoffee")) {
        	coffee = new AmericaCappuccinoCoffee();
    	} else if(type.equals("americaLatteCoffee")) {
        	coffee = new AmericaLatteCoffee();
    	} else if(type.equals("americaCaramelCoffee")) {
        	coffee = new AmericaCaramelCoffee();
    	}
        
        return coffee;
    }
}

然后我们必须要改变我们原来的业务逻辑,也就是CoffeeStore这个业务类里面的代码,因为我们这里的代码之前只有一个工厂,现在我们需要增加一个AmericaCoffeeSimpleFactory工厂,并且需要在orderCoffee业务逻辑方法中判断使用哪个工厂。因此很明显,你能够发现问题。这种简单工厂的模式仍然会违反“对修改关闭,对扩展打开”这个原则。

不再给咖啡店CoffeeStore使用Factory工厂类,而是在CoffeeStore咖啡店中加入一个抽象的工厂方法,用来创建不同类型的对象。代码如下:

public abstract class CoffeeStore {
    
    public Coffee orderCoffee(String type) {
    	Coffee coffee;
    	coffee = createCoffee(type);
    
    	coffee.cup(); //咖啡装杯
    	coffee.box(); //咖啡装盒
	}
    
    abstract Coffee createCoffee(String type);
}

现在已经有了一个CoffeeStroe咖啡商店作为超类,我们可以定义两种不同的咖啡商店,用于生成不同地域口味风格的咖啡。比如定义一个AmericaCoffeeStore咖啡商店用于生成美国风味的咖啡,定义一个JapanCoffeeStore咖啡商店用于生成日本风味的咖啡。如下:

//美国风味的咖啡商店
public class AmericaCoffeeStore extends CoffeeStore {
    public Coffee createCoffee(String type) {
        Coffee coffee = null;
        
        if(type.equals("americaCappuccinoCoffee")) {
        	coffee = new AmericaCappuccinoCoffee();
    	} else if(type.equals("americaLatteCoffee")) {
        	coffee = new AmericaLatteCoffee();
    	} else if(type.equals("americaCaramelCoffee")) {
        	coffee = new AmericaCaramelCoffee();
    	}
        
        return coffee;
    }
}

//日本风味的咖啡商店
public class JapanCoffeeStore extends CoffeeStore {
    public Coffee createCoffee(String type) {
        Coffee coffee = null;
        
        if(type.equals("japanCappuccinoCoffee")) {
        	coffee = new JapanCappuccinoCoffee();
    	} else if(type.equals("japanLatteCoffee")) {
        	coffee = new JapanLatteCoffee();
    	} else if(type.equals("japanCaramelCoffee")) {
        	coffee = new JapanCaramelCoffee();
    	}
        
        return coffee;
    }
}

//当然我们不能只有商店工厂,却没有具体的产品。我们这里为了代码简便把所有的产品细节都忽略了。

测试工厂方法模式,如下:

public class Test {
    public static void main(String[] args) {
        CoffeeStore americaCoffeeStore = new AmericaCoffeeStore();
        CoffeeStore japanCoffeeStore = new JapanCoffeeStore();
        
        Coffee coffee = americaCoffeeStore.orderCoffee("americaCappuccinoCoffee");
        System.out.println(coffee.getName());
        
        coffee = japanCoffeeStore.orderCoffee("japanCappuccinoCoffee");
        System.out.println(coffee.getName());
    }
}

使用上面的工厂方法模式之后,可以发现当前已经满足了设计原则“对扩展打开,对修改关闭”,如果我们想要增加新的地域风格的咖啡,我们只需要重新写一个新的地域风格的CoffeeStore咖啡商店子类即可,不需要修改任何原有逻辑的代码。

平行的类层级

我们的产品类,和创建产品的创建类,它们两个是平行的关系,为什么说是平行的呢?因为他们两个都有各自的抽象类,如下图:

创建类和产品类实现了完全解耦,二者唯一的联系是在工厂的子类也就是创建者的子类中,去创建一个具体的产品,比如说在AmCoffeeStore美国风味的咖啡商店中去创建AmCappuccinoCoffee或者AmLatteCoffee。

**工厂方法模式定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类。**工厂方法模式能够封装具体类型的实例化。看看下面的类图,抽象的Creator提供了一个创建对象的方法的接口,也称为“工厂方法”。在抽象的Creator中,任何其他实现的方法,都可能使用到这个工厂方法所制造出来的产品,但只有子类真正实现这个工厂方法并创建产品。类图如下:

注意在工厂模式中,抽象创建者里面的factoryMethod()工厂方法是抽象的,具体创建什么样的对象,会延迟到子创建者里面去决定。但是创建者里面的anOperation()操作方法,是创建者其中之一个创建方法,这个方法是一个实现方法,它会拿到产品的抽象父类,然后执行一些通用的方法。比如我们上面例子中的抽象创建者对象是CoffeeStore,这个对象中有一个orderCoffee方法,这个方法就是一个抽象创建者中已经实现的操作方法,因为我们不管是什么咖啡,最后做好了都需要进行装杯和装盒,因此我们就在抽象创建者里面的orderCoffee方法里面进行一些通用的处理操作。

依赖倒置原则

比如我们现在想要开一个CoffeeStore店,那么我们就需要去想这个咖啡店能生产出什么样的咖啡,比如说卡布奇诺咖啡,拿铁,焦糖咖啡。这样的话我们的咖啡店依赖的类就太多了,那么什么是依赖倒置原则呢?依赖倒置原则其实就是要求我们从下往上想,我们可以想一下能不能给卡布奇诺咖啡,拿铁咖啡,焦糖咖啡一个抽象父类,然后这样的话我们的咖啡店就能只依赖这个咖啡父类这一个类了,管理起来也是好管理的。这就是依赖倒置原则,其实换种说法就是一种软件设计原则“依赖抽象,不要依赖具体的实现类”。

抽象工厂模式

像上面的工厂方法模式,已经很大程度的进行解耦了。但是还有没有什么问题呢?我们需不需要再引入一个新的模式呢?答案是需要的,为什么呢?因为目前可以发现,我们存在两种卡布奇诺咖啡,AmericaCappuccinoCoffee美国卡布奇诺咖啡,和JapanCappuccinoCoffee日本卡布奇诺咖啡,这两种咖啡的做法是完全一致的,唯一的区别就是使用的原料不同。比如说美国的卡布奇诺咖啡使用的是美国咖啡豆和美国果酱,而日本的卡布奇诺咖啡使用的是日本的咖啡豆和日本果酱。那么既然是做法相同,原料不同,就说明原料是可以变化的东西,那么我们能不能把可以变化的东西给封装起来呢?然后使用组合?答案是可以的,需要怎么做呢?

//首先定义一个抽象咖啡工厂接口,这个工厂用来生产地域原料咖啡豆
public interface CoffeeFactory {
    //创建咖啡豆产品
    CoffeeBean createCoffeeBean();
    //创建果酱产品
    Jam createJam();
}

//然后定义具体地域的咖啡工厂,也就是定义America美国和Japan日本的咖啡工厂
public class AmericaCoffeeFactory implements CoffeeFactory {
    CoffeeBean createCoffeeBean() {
        return new AmericaCoffeeBean();
    }
    
    Jam createJam() {
        return new AmericaJam();
    }
}

public class JapanCoffeeFactory implements CoffeeFactory {
    CoffeeBean createCoffeeBean() {
        return new JapanCoffeeBean();
    }
    
    Jam createJam() {
        return new JapanJam();
    }
}

//有了上面的两个不同地域的咖啡原料工厂,我们就没必要写两个卡布奇诺咖啡即AmericaCappuccinoCoffee和JapanCappuccinoCoffee了,我们可以把变化的原料部分封装起来,封装给一个工厂,我们可以通过组合来做。这样不管后面出现多少个地域的卡布奇诺咖啡,我们只需要写一个CappuccinoCoffee卡布奇诺咖啡类就行了。怎么写CappuccinoCoffee类呢?如下:

//首先重写抽象的咖啡类
public abstract class Coffee {
    //咖啡名字
    String name;
    //咖啡原料咖啡豆
    CoffeeBean coffeeBean;
    //咖啡原料果酱
    Jam jam;
    
    //此抽象方法是咖啡子类,根据不同的咖啡原料工厂为咖啡原料赋值的
    abstract void prepare();
    
    //咖啡装杯,不管什么样的咖啡此步操作不变
    void cup() {
        System.out.println("把咖啡装到杯子里面");
    }
    
    //咖啡装盒,不管什么样的咖啡此步操作不变
    void box() {
        System.out.println("把咖啡杯子装到咖啡盒子里面");
    }
}

//接着把之前的AmericaCappuccinoCoffee和JapanCappuccinoCoffee两个类,使用一个CappuccinoCoffee加组合工厂对象的方法代替,代码如下:
public class CappuccinoCoffee extends Coffee {
    CoffeeFactory coffeeFactory;
    
    public CappuccinoCoffee(CoffeeFactory coffeeFactory) {
        this.coffeeFactory = coffeeFactory;
        prepare();
    }
    
    public void prepare() {
        coffeeBean = coffeeFactory.createCoffeeBean();
        jam = coffeeFactory.createJam();
    }
}

//重写日本咖啡商店和美国咖啡商店
//美国风味的咖啡商店
public class AmericaCoffeeStore extends CoffeeStore {
    public Coffee createCoffee(String type) {
        Coffee coffee = null;
        CoffeeFactory coffeeFactory = new AmericaCoffeeFactory();
        
        if(type.equals("americaCappuccinoCoffee")) {
        	coffee = new CappuccinoCoffee(coffeeFactory);
    	} else if(type.equals("americaLatteCoffee")) {
        	coffee = new LatteCoffee(coffeeFactory);
    	} else if(type.equals("americaCaramelCoffee")) {
        	coffee = new CaramelCoffee(coffeeFactory);
    	}
        
        return coffee;
    }
}

//日本风味的咖啡商店
public class JapanCoffeeStore extends CoffeeStore {
    public Coffee createCoffee(String type) {
        Coffee coffee = null;
        CoffeeFactory coffeeFactory = new JapanCoffeeFactory();
        
        if(type.equals("japanCappuccinoCoffee")) {
        	coffee = new CappuccinoCoffee(coffeeFactory);
    	} else if(type.equals("japanLatteCoffee")) {
        	coffee = new LatteCoffee(coffeeFactory);
    	} else if(type.equals("japanCaramelCoffee")) {
        	coffee = new CaramelCoffee(coffeeFactory);
    	}
        
        return coffee;
    }
}

//从上面的例子可以发现,我们之前是需要美国和日本的卡布奇诺咖啡两个类,但是现在我们只需要一个卡布奇诺咖啡类,再配合我们的组合,组合工厂对象,就能够只写一个卡布奇诺咖啡的类了。这样能大大减少我们的类,比如说如果后期你的程序中出现了伊拉克卡布奇诺咖啡,你也不再需要写一个对应的咖啡子类了,用CappuccinoCoffee卡布奇诺类组合伊拉克工厂(父类抽象工厂)就可以了。
//抽象工厂模式也能够实现解耦的操作,比如说现在我们可以只有一个CappuccinoCoffee卡布奇诺咖啡,它的代码始终不用变,我们可以通过不同的抽象工厂实现来生成不同地域风味的卡布奇诺咖啡。什么叫做松耦合,其实就是说类与类之间的影响比较小,我们改变一个A类之后,B类没必要跟着改变。

工厂方法和抽象工厂模式的区别

相同点:首先这两个模式都是为了让创建对象和我们的实际代码解耦的。

不同点:

  • 对于工厂方法模式,它使用的是继承,如果你想使用工厂方法模式创建一个对象,那你必须要一个子类去继承父类,然后具体去定义创建什么样的对象,创建什么样的对象只能在子类中决定;而对于抽象工厂模式,它使用的是组合,它会组合一个工厂对象。
  • 对于抽象工厂模式可以帮助我们创建一类产品,比如CoffeeFactory工厂对象和CappuccinoCoffee卡布奇诺咖啡,LatteCoffee拿铁咖啡,CaramelCoffee焦糖咖啡组合之后,就可以通过工厂帮助我们生产美国风味的卡布奇诺咖啡,美国风味的拿铁咖啡和美国风味的焦糖咖啡;而对于工厂方法模式只能帮助我们创建一个产品,比如说像下面的这个使用工厂方法的美国咖啡商店,只能一次通过工厂方法createCoffee为我们创建美国卡布奇诺咖啡,美国拿铁咖啡,美国焦糖咖啡中的一种。
public class AmericaCoffeeStore extends CoffeeStore {
    public Coffee createCoffee(String type) {
        Coffee coffee = null;
        
        if(type.equals("americaCappuccinoCoffee")) {
        	coffee = new AmericaCappuccinoCoffee();
    	} else if(type.equals("americaLatteCoffee")) {
        	coffee = new AmericaLatteCoffee();
    	} else if(type.equals("americaCaramelCoffee")) {
        	coffee = new AmericaCaramelCoffee();
    	}
        
        return coffee;
    }
}

命令模式

为什么要使用命令模式呢?因为命令模式可以把请求者对象,和执行者对象二者解耦。具体的做法就是,在请求者对象和执行者对象中间,会加入一个命令对象,通过这个命令对象,可以把我们的请求者对象和执行者对象解耦。

假如现在你有两个执行者对象,一个是灯Light,一个是电视机TV。现在有一个请求者,比如说遥控器,它想要对灯或者是电视机TV进行操作,那么我们的代码如下:

public void client() {
    SimpleRemoteControl simpleRemoteControl = new SimpleRemoteControl();
    roomOwner.execute("lightOn");
}

class SimpleRemoteControl {
    Light light = new Light();
    TV tv = new TV();
    
    public void execute(String command) {
        if(command.equal("lightOn")) {
            light.on();
        } else if(command.equal("lightOff")) {
            light.off();
        } else if(command.equal("tvOn")) {
            tv.on();
        } else if(command.equal("tvOff")) {
            tv.off();
        }
    }
}

class Light {
    public void on() {
        System.out.println("打开电灯");
    }
    
    public void off() {
        System.out.println("关闭电灯");
    }
}

class TV {
    public void on() {
        System.out.println("打开电视");
    }
    
    public void off() {
        System.out.println("关闭电视");
    }
}

对于上面的代码,你有没有看出来有什么问题呢?最大的问题就是请求者对象和执行者对象的依赖耦合度太高了,现在我们房间里只有电灯和电视这两个执行者对象,那假如我们在后面房间里又增加了洗衣机 ,电风扇这些执行者对象,我们的SimpleRemoteControl请求者对象就不得不修改它本来的代码,这会违反我们软件设计的原则“对修改关闭,对拓展打开”。为什么执行者对象变了之后,我们的请求者对象也需要变呢?主要还是因为我们的请求者对象和执行者对象之间的联系太紧密了,耦合度太高了,我们需要降低二者之间的耦合度,让它们之间的依赖没那么紧密,这样其中一者改变之后,另外一者就不需要改变了。而我们让执行者对象和请求者对象解耦的方法,就是使用命令模式,代码如下:

//首先实现一个命令接口,这个命令接口里面只需要一个方法execute
public interface Command {
    public void execute();
}

//实现一个打开电灯的命令
public class LightOnCommand implements Command {
    Light light;
    
    public LightOnCommand(Light light) {
        this.light = light;
    }
    
    public void execute() {
        light.on();
    }
}

//实现一个关闭电灯的命令
public class LightOffCommand implements Command {
    Light light;
    
    public LightOffCommand(Light light) {
        this.light = light;
    }
    
    public void execute() {
        light.off();
    }
}

//实现一个打开电视机的命令
public class TVOnCommand implements Command {
    TV tv;
    
    public TVOnCommand(TV tv) {
        this.tv = tv;
    }
    
    public void execute() {
        tv.on();
    }
}

//实现一个关闭电视机的命令
public class TVOffCommand implements Command {
    TV tv;
    
    public TVOffCommand(TV tv) {
        this.tv = tv;
    }
    
    public void execute() {
        tv.off();
    }
}

//为了避免空指针异常,我们需要一个空的命令对象,这样可以保证点击遥控器的按钮之后,什么命令都不执行。
public class NoCommand implements Command {
    public void execute() {}
}

//遥控器请求者对象
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;
    }
    
    public void onButtonWasPushed(int slot) {
        onCommands[slot].execute();
    }
    
    public void offButtonWasPushed(int slot) {
        offCommands[slot].execute();
    }
}

//测试代码
public class RemoteLoader {
    public static void main(String[] args) {
        RemoteControl remoteControl = new RemoteControl();
        
        Light light = new Light();
        TV tv = new TV();
        
        LightOnCommand lightOnCommand = new LightOnCommand(light);
        LightOffCommand lightOffCommand = new LightOffCommand(light);
        TVOnCommand tvOnCommand = new TVOnCommand(tv);
        TVOffCommand tvOffCommand = new TVOffCommand(tv);
        
        remoteControl.setCommand(0, lightOnCommand, lightOffCommand);
        remoteControl.setCommand(0, tvOnCommand, tvOffCommand);
        
        remoteControl.onButtonWasPushed(0);
        remoteControl.offButtonWasPushed(0);
        remoteControl.onButtonWasPushed(1;
        remoteControl.offButtonWasPushed(1;
        
    }
}

//通过上面的命令模式的设计,我们就可以把请求者对象RemoteControl遥控器,和执行者对象Light,TV解耦了。我们的请求者对象不再直接依赖执行者对象。而是根据依赖倒置原则,我们把所有请求者对象依赖的执行者对象抽象出来,让我们的请求者对象只依赖这一个抽象就可以了。但是因为我们的执行者对象,它们不属于同一个类,就是它们没有相同的父类,因此我们就自己设计了一个命令对象Command,然后把对应的执行者对象组合进来,现在的情况是所有的执行者对象都依赖于Command对象,因此我们的请求者对象只需要依赖Command对象,那么就把我们的请求者对象和执行者对象们联系起来了,并且可以实现解耦,当执行者对象无论怎么变化的时候,都不会影响到我们请求者对象。

遥控器如下图:

适配器模式

适配器模式将一个类的接口,转换成客户期望的另一个接口。适配器让原本接口不兼容的类可以合作无间。使用适配器,主要是负责进行代码解耦的,在有些情况下,如果你不使用适配器模式,新加一种类之后,你就必须要改变原有的代码。而如果使用了适配器模式,你只需要把新加的类通过和一个适配器组合来适配系统中原有的接口,我们的源代码就可以支持我们新加的类,我们就不用修改源代码了,这就实现了解耦的功能。

比如下面的这个例子:

//目前的系统中只有Duck鸭子接口,如下

//鸭子超类接口
public interface Duck {
    //鸭子呱呱叫
    public void quack();
    //鸭子飞行
    public void fly();
}

//子类绿头鸭
public class MallardDuck implements Duck {
    public void quack() {
        System.out.println("Quack");
    }
    
    public void fly() {
        System.out.println("I'm flying");
    }
}

//现在有一个执行鸭子具体行为的方法
public class DuckTestDrive {
    static void testDuck(Duck duck) {
        duck.quack();
        duck.fly();
    }
}

//可以发现,原有的代码方法testDuck的参数是Duck duck,它必须要求我们传入的是Duck对象。但是这个时候,我们想要这个方法也能够传入火鸡对象,这看似是不可能的,因为火鸡不属于Duck

//定义火鸡
public interface Turkey {
    //火鸡不会呱呱叫,只会咯咯叫
    public void gobble();
    //火鸡会飞,虽然飞的不远
    public void fly();
}

//这是火鸡的一个具体实现
public class WildTurkey implements Turkey {
    public void gobble() {
        System.out.println("Gobble gobble");
    }
    
    public void fly() {
        System.out.println("i'm flying a short distance");
    }
}

//目前遇到一个情况,你想要把火鸡对象传递给testDuck方法,让这个方法调用火鸡对象叫和飞的方法。但是就目前来看,肯定是不可能的,因为火鸡的超类接口是Turkey,不是Duck。那么我们现在要怎么做呢?我们可以定义一个火鸡适配器,让此适配器组合适配者火鸡对象,此适配器需要适配Duck鸭子接口,如下
public class TurkeyAdapter implements Duck {
    Turkey turkey;
    
    public TurkeyAdapter(Turkey turkey) {
        this.turkey = turkey;
    }
    
    public void quack() {
        turkey.gobble();
    }
    
    public void fly() {
        turkey.fly();
    }
}

//然后我们就可以定义一个火鸡适配器,看似是一个Duck鸭子对象,实则调用的都是火鸡的内部行为。然后这个适配器就可以传递给testDuck方法了,因为这个适配器的超类接口是Duck
Turkey turkey = new Turkey();
Duck turkeyAdapter = new TurkeyAdapter(turkey);
testDuck(turkeyAdapter);

//通过上面编写一个火鸡适配器,我们增加一个火鸡类之后,就可以在不修改原有代码的基础上,去使用原有的代码。如果没有编写火鸡适配器,那么你就必须再重新写一个testTurkey(Turkey turkey)方法。而且你的火鸡不能再使用原有的逻辑,这会让你的代码变得臃肿。因为你有针对鸭子和火鸡有两套不同的代码。而是用火鸡适配器之后,不管是你是想要调用火鸡的叫的行为和飞的行为,还是调用鸭子的叫的行为和飞的行为,你都可以使用testDuck这一个方法来实现。

模板方法模式

**模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。**这个模式是用来创建一个算法的模板的。什么是模板?如你所见的,模板就是一个方法。更具体地说,这个方法将算法定义成一组步骤,其中的任何步骤都可以是抽象的,由子类负责实现。这可以确保算法的结构保持不变,同时由子类提供部分实现。牵涉到的软件设计原则是:别找我,我会找你。

来看一个冲咖啡和冲茶的例子:

咖啡冲泡法:

​ (1)把水煮沸。

​ (2)用沸水冲咖啡粉。

​ (3)把冲好的液体倒入杯子里。

​ (4)加糖和牛奶。

茶冲泡法:

​ (1)把水煮沸。

​ (2)用沸水浸泡茶叶。

​ (3)把冲好的液体倒入杯子里。

​ (4)加柠檬。

定义咖啡和茶的代码,如下:

//咖啡
public class Coffee {
    void prepareRecipe() {
        boilWater();
        brewCoffeeGrinds();
        pourInCup();
        addSugarAndMilk();
    }
    
    public void boilWater() {
        System.out.println("把水煮沸");
    }
    
    public void brewCoffeeGrinds() {
        System.out.println("用沸水冲咖啡粉");
    }
    
    public void pourInCup() {
        System.out.println("把冲好的液体倒入杯子里");
    }
    
    public void addSugarAndMilk() {
        System.out.println("加糖和牛奶");
    }
}

//茶
public class Tea {
    void prepareRecipe() {
        boilWater();
        steepTeaBag();
        pourInCup();
        addLemon();
    }
    
    public void boilWater() {
        System.out.println("把水煮沸");
    }
    
    public void steepTeaBag() {
        System.out.println("用沸水浸泡茶叶");
    }
    
    public void pourInCup() {
        System.out.println("把冲好的液体倒入杯子里");
    }
    
    public void addLemon() {
        System.out.println("加柠檬");
    }
}

//在上面的咖啡类和茶类中我们发现了重复的代码,这是好现象。这表示我们需要清理一下设计了。在这里,既然茶和咖啡是如此相似,似乎我们应该将共同的部分抽取出来,放进一个基类中。不管是冲咖啡还是冲茶,我们发现第一步和第三步都是一样的,所以这部分重复的代码可以提取出来放到基类中。而第二步和第四步,虽然加的物品不一样,但是具体的动作也是相似的,因此我们可以定义成抽象方法。

//抽象出来超类
public abstract class CaffeineBeverage {
    //因为我们的模板方法其实相当于是一个固定的算法,我们不希望子类中会去覆盖这个算法,因此我们加上一个final修饰符
    final void prepareRecipe() {
        boilWater();
        brew();
        pourInCup();
        addCondiments();
    }
    
    abstract void brew();
    
    abstract void addCondiments();
    
    void boilWater() {
       System.out.println("把水煮沸"); 
    }
    
    void pourInCup() {
        System.out.println("把冲好的液体倒入杯子里");
    }
}

//重写咖啡类
public class Coffee extends CaffeineBeverage {
    public void brew() {
        System.out.println("用沸水冲咖啡粉");
    }
    
    public void addCondiments() {
        System.out.println("加糖和牛奶");
    }
}

//重写茶类
public class Tea {
    public void brew() {
        System.out.println("用沸水浸泡茶叶");
    }
    
    public void addCondiments() {
        System.out.println("加柠檬");
    }
}

//在改造后的例子中,prepareRecipe方法就是我们的模板方法,它是一个算法的模板,本例中的算法是用来制作饮料的算法。在这个模板中,算法内的每一个步骤都被一个方法代表了。模板方法定义了一个算法的步骤,并允许子类为一个或多个步骤提供实现。

所以模板方法模式的结构设计更具体的设计是如何的呢?请看下面的代码:

abstract class AbstractClass {
    //这个是模板方法,定义一个算法的骨架结构
    final void templateMethod() {
        primitiveOperation1();
        primitiveOperation2();
        concreteOperation();
        hook();
    }
    
    //这是算法中的一个步骤,具体的步骤是什么每一个子类都不一样,因此这个方法必须由子类去实现。
    abstract void primitiveOperation1();
    
    //这是算法中的一个步骤,具体的步骤是什么每一个子类都不一样,因此这个方法必须由子类去实现。
    abstract void primitiveOperation2();
    
    //这个方法是所有子类共用的代码,因此我们不希望它会改变,不希望子类去覆盖它,因此我们加上了一个final修饰符。
    final void concreteOperation() {
        // 这里是实现
    }
    
    //这个是一个具体的方法,但是它什么都不用做。我们可以有“默认不做事的方法”,我们称这种方法为“hook”钩子函数。子类可以视情况决定要不要覆盖它们。钩子函数其实就相当于是一个条件判断,如果满足钩子条件我们就执行某些逻辑代码。
    void hook() {}

}

下面看一下使用钩子函数之后,我们制造饮料的超类会有什么特殊效果,如下:

public abstract class CaffeineBeverageWithHook {
    void prepareRecipe() {
        boilWater();
        brew();
        pourInCup();
        if(customerWantsCondiments()) {
            addCondiments();
        }
    }
    
    abstract void brew();
    
    abstract void addCondiments();
    
    void boilWater() {
       System.out.println("把水煮沸"); 
    }
    
    void pourInCup() {
        System.out.println("把冲好的液体倒入杯子里");
    }
    
    //这个方法就是一个钩子函数,超类中默认钩子函数的返回值是true,就是默认子类会添加材料,如果有的子类不想进行第四步操作,可以覆盖超类的钩子函数,把返回值设置成false,这样子类制造饮料的时候,就不用进行第四步操作了。
    boolean customerWantsCondiments() {
        return true;
    }
}

特殊的模板方法模式

模板方法模式是一个很常见的模式,到处都是。尽管如此,你必须拥有一双锐利的眼睛,因为模板方法有许多实现,而他们看起来并不一定和书上所讲的设计一致。

比如说,下面的这个排序的例子。Java数组类的设计者提供给我们一个方便的模板方法用来排序。让我们看看这个方法如何运行:

public static void sort(Object[] a) {
    Object aux[] = (Object[])a.clone();
    mergeSort(aux, a, 0, a.length, 0);
}

private static void mergeSort(Object src[], Object dest[], int low, int high, int off) {
    for(int i = low; i < high; i++) {
        for(int j = i; j > low && ((Comparabe)dest[j-1]).compareTo((Comparable)dest[j])>0; j--) {
            swap(dest, j, j-1);
        }
    }
}

//比如说我们现在有一个鸭子Ducks数组,我们在使用sort方法排序这个数组的时候,sort方法就相当于是一个模板方法,这个模板方法依赖于Duck鸭子对象中的compareTo()方法的具体实现。因此这其实就是一个模板方法模式,虽然这里的例子和我们的教科书里的定义不太一样,因为我们教科书里的必须有子类继承父类,而我们的Duck鸭子对象,并不是Array数组对象的子类。但是,并不是所有的模板方法模式都和我们的教科书里定义的完全相同,我们这里也是属于模板方法模式。

public class Duck implements Comparable {
    String name;
    int weight;
    
    public Duck(String name, int weight) {
        this.name = name;
        this.weight = weight;
    }
    
    public String toString() {
        return name + " weights " + weight;
    }
    
    public int compareTo(Object object) {
        Duck otherDuck = (Duck)object;
        
        if(this.weight < otherDuck.weight) {
            return -1;
        } else if(this.weight == otherDuck.weight) {
            return 0;
        } else {
            return 1;
        }
    }
}

状态模式

状态模式允许对象在内部状态改变时改变它的行为。

假设我们现在有一个糖果状态机,顾客给它投入钱,它可以给顾客放出糖果。假设这个糖果状态机里面现在有四种状态,分别是没有25分状态,有25分状态,售出糖果状态,糖果售罄状态。这四种状态以及这四种状态的转换如下图:

解释说明:上图中是糖果状态机中的四种状态,第一种状态是“没有25分钱”状态,在这个状态的时候通过动作“投入25分钱”可以转换为“有25分钱”状态;而在“有25分钱”状态通过动作“退回25分钱”可以转换到“没有25分钱”状态,通过动作“转动曲柄”可以转换到“售出糖果”状态;而在“售出糖果”状态通过动作“发放糖果”可以转换到“没有25分钱”状态或者是“糖果售罄”状态。其中糖果机对于顾客来说的外部行为就三个动作,一个是“投入25分钱”,一个是“退回25分钱”,另一个是“转动曲柄”。像其他的动作“发放糖果”这是糖果机内部进行的动作。还有一点需要注意,有一些操作是没有意义的,比如说如果你在“没有25分钱”状态想要售出糖果,这是不可能的,这是无意义的操作。还有一点需要提醒一下,如果糖果机当前的状态是售出糖果状态,那么此状态会一直准备出糖果给顾客。

根据上面的状态切换图,写一个糖果机,代码如下:

public class GumballMachine {
    //售罄状态
    final static int SOLD_OUT = 0;
    //没有25分钱状态
    final static int NO_QUARTER = 1;
    //有25分钱状态
    final static int HAS_QUARTER = 2;
    //售出糖果状态
    final static int SOLD = 3;
    
    //糖果状态机的当前状态
    int state = SOLD_OUT;
    //糖果状态机内部现有的糖果数量
    int count = 0;
    
    public GumballMachine(int count) {
        this.count = count;
        if(count > 0) {
            state = NO_QUARTER;
        }
    }
    
    //“投入25分钱”动作
    public void insertQuarter() {
        if(state == HAS_QUARTER) {
            System.out.println("您当前已经投入过25分钱了,无需重复投入!");
        } else if(state == NO_QUARTER) {
            state = HAS_QUARTER;
            System.out.println("投入25分钱成功!");
        } else if(state == SOLD_OUT) {
            System.out.println("当前糖果已经售罄,请不要再投钱!");
        } else if(state == SOLD) {
            System.out.println("您的糖果正在出来见您的路上,请您稍等片刻,请不要再重复投钱!");
        }
    }
    
    //“退回25分钱”动作
    public void ejectQuarter() {
        if(state == HAS_QUARTER) {
            state = NO_QUARTER;
            System.out.println("已成功退回您25分钱!");
        } else if(state == NO_QUARTER) {
            System.out.println("您还没有投入钱,无法退回!");
        } else if(state == SOLD_OUT) {
            System.out.println("您还没有投入钱,无法退回!");
        } else if(state == SOLD) {
            System.out.println("您的糖果正在出来见您的路上,当前无法帮您退钱!");
        }
    }
    
    //“转动曲柄”动作
    public void turnCrank() {
        if(state == HAS_QUARTER) {
            state = SOLD;
            System.out.println("您的糖果正在出来见您的路上,请稍等!");
            //因为分发糖果是糖果机内部的动作,并不是顾客直接在外部触发的动作,因此分发糖果这个动作要在“转动曲柄”动作里被调用
            dispense();
        } else if(state == NO_QUARTER) {
            System.out.println("您还没有投入钱,请投入钱之后再转动曲柄!");
        } else if(state == SOLD_OUT) {
            System.out.println("您还没有投入钱,请投入钱之后再转动曲柄!");
        } else if(state == SOLD) {
            System.out.println("您的糖果正在出来见您的路上,请不要重复转动曲柄!");
        }
    }
    
    //“分发糖果”动作
    public void dispense() {
        if(state == HAS_QUARTER) {
            System.out.println("转动曲柄之后才能分发糖果,请先转动曲柄!");
        } else if(state == NO_QUARTER) {
            System.out.println("您还没有投入钱,请先投入钱!");
        } else if(state == SOLD_OUT) {
            System.out.println("您还没有投入钱,请先投入钱!");
        } else if(state == SOLD) {
            System.out.println("一个糖果正在出来见您的路上");
            count = count - 1;
            if(count == 0) {
                state == SOLD_OUT;
                System.out.println("出来一个糖果!");
            } else {
                state == NO_QUARTER;
                System.out.println("出来一个糖果!");
            }
        }
    }
}

像上面的代码,我们几乎已经完全实现了我们目前的需求,但是我们目前是把状态机的状态和状态机相关的动作全部都写在了糖果状态机里面,这样写会不会出现什么问题呢?目前确实是没有问题,但是假如我们后面我们有一个需求,会在原有状态的基础上多出一个赢家状态,就是说现在顾客转动曲柄之后,是有概率切换到赢家状态的。如果转动曲柄之后,切换到了赢家状态,可以一次性的活动两颗糖果,也就是说可以免费获取一颗糖果,目前的状态切换流程图如下:

那这个时候,因为多了一种状态,所以我们原来的糖果状态机在进行if判断状态的时候就需要多加一个状态,所以你就要修改原有的代码逻辑,这工作量是非常大的,而且特别容易出bug,最重要的是这违反了我们软件设计的一个规则“对修改关闭,对扩展打开”。因此我们现在需要怎么办呢?我们不得不把原来的代码重构一下,现在我们需要使用状态模式重新重构一下。之前所有的状态和所有的动作我们都放到了糖果状态机里面,现在我们变一下,我们把所有的状态放到糖果状态机里面,但是我们把状态改成是对象类型,然后把所有的动作放到状态对象里面,然后用糖果状态机组合我们的状态对象,糖果状态机委托状态对象去处理我们的动作。具体重构代码如下:

//完整的糖果状态机类
public class GumballMachine {
    State soldOutState;
    State noQuarterState;
    State hasQuarterState;
    State soldState;
    
    //初始状态设置为售罄状态
    State state = soldOutState;
    //糖果机内的默认糖果个数设置为0
    int count = 0;
    
    public GumballMachine(int numberGumballs) {
        soldOutState = new SoldOutState(this);
        noQuarterState = new NoQuarterState(this);
        hasQuarterState = new HasQuarterState(this);
        soldState = new SoldState(this);
        this.count = numberGumballs;
        if(numberGumballs > 0) {
            state = noQuarterState;
        }
    }
    
    public void insertQuarter() {
        state.insertQuarter();
    }
    
    public void ejectQuarter() {
        state.ejectQuarter();
    }
    
    public void turnCrank() {
        state.turnCrank();
        //因为分发糖果不属于顾客可以操作的动作,它是糖果机内部的动作,因此必须要把这个动作放到转动曲柄这个用户能在外部操作的动作里面调用
        state.dispense();
    }
    
    void releaseBall() {
        System.out.println("一个糖果正在出来见您的路上!");
        if(count != 0) {
            count = count - 1;
        }
    }
    
    //下面是get,toString等方法
}

//写一个State状态接口,后面所有的状态对象都需要实现这个接口
public interface State {
    void insertQuarter();
    void ejectQuarter();
    void tumCrank();
    void dispense();
}

//“没有25分钱”状态类
public class NoQuarterState implements State {
    GumballMachine gumballMachine;
    
    public NoQuarterState(GumbalMachine gumbalMachine) {
        this.gumbalMachine = gumbalMachine;
    }
    
    public void insertQuarter() {
        System.out.println("成功插入25分钱!");
        gumballMachine.setState(gumballMachine.getHasQuarterState);
    }
    
    public void ejectQuarter() {
        System.out.println("您当前还没有投入钱,无法退回!");
    }
    
    public void turnCrank() {
        System.out.println("请先投入钱,再转动曲柄");
    }
    
    public void dispense() {
        System.out.println("请先投入钱!");
    }
}

//“有25分钱”状态类
public class HasQuarterState implements State {
    GumballMachine gumballMachine;
    
    public HasQuarterState(GumballMachine gumballMachine) {
        this.gumballMachine = gumballMachine;
    }
    
    public void insertQuarter() {
        System.out.println("您已经投入过25分钱了,不能重复投入!");
    }
    
    public void ejectQuarter() {
        System.out.println("成功退回您25分钱!");
        gumballMachine.setState(gumballMachine.getNoQuarterState);
    }
    
    public void turnCrank() {
        System.out.println("请等待糖果出来!");
        gumballMachine.setState(gumballMachine.getSoldState);
    }
    
    public void dispense() {
        System.out.println("请先转动曲柄!");
    }
}

//“售出”状态类
public class SoldState implements State {
    GumballMachine gumballMachine;
    
    public SoldState(GumballMachine gumballMachine) {
        this.gumballMachine = gumballMachine;
    }
    
    public void insertQuarter() {
        System.out.println("您的糖果正在来见您的路上,此时不能投入钱!");
    }
    
    public void ejectQuarter() {
        System.out.println("您的糖果正在来见您的路上,此时不能退回钱!");
    }
    
    public void turnCrank() {
        System.out.println("您的糖果正在来见您的路上,此时不能转动曲柄!");
    }
    
    public void dispense() {
        gumballMachine.releaseBall();
        if(gumbalMachine.getCount() > 0) {
            gumbalMachine.setState(gumballMachine.getNoQuarterState());
        } else {
            gumballMachine.setState(gumbalMachine.getSoldOutState());
        }
    }
}

重构了代码之后,到目前为止我们做了什么事情:

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

现在我们就可以写新增加的需求了,我们只需要在原有代码的基础上进行扩展就行了,而无需修改原有的代码接口,代码如下:

public class GumballMachine {
    State soldOutState;
    State noQuarterState;
    State hasQuarterState;
    State soldState;
    
    //新增赢家状态
    State winnerState;
    
    State state = soldOutState;
    int count = 0;
    
    //这里有一些方法
}

//现在让我们实现WinnerState类本身,其实它很像SoldState类:
public class WinnerState implements State {
    //实例变量和构造器
    
    //insertQuarter错误信息
    
    //ejectQuarter错误信息
    
    //turnCrank错误信息
    
    public void dispense() {
        System.out.println("你是一个赢家,你的25分钱可以获取两颗糖!");
        gumballMachine.releaseBall();
        if(gumbalMachine.getCount() == 0) {
            gumballMachine.setState(gumballMachine.getSoldOutState());
        } else {
            gumballMachine.releaseBall();
            if(gumballMachine.getCount() > 0) {
                gumballMachine.setState(gumballMachine.getNoQuarterState());
            } else {
                gumballMachine.setState(gumballMachine.getSoldOutState());
            }
        }
    }
}

//我们还要再做一个改变:我们需要实现机会随机数,这里顾客有10%的机会成为赢家。还要增加一个进入WinnerState状态的转换。这两件事情都要加进HasQuarterState,因为顾客会从这个状态中转动曲柄;
public class HasQuarterState implements State {
    Random randomWinner = new Random(System.currentTimeMillis());
    GumballMachine gumballMachine;
    
    public HasQuarterState(GumballMachine gumballMachine) {
        this.gumballMachine = gumballMachine;
    }
    
    public void insertQuarter() {
        System.out.println("您已经投入过25分钱了,不能重复投入!");
    }
    
    public void ejectQuarter() {
        System.out.println("成功退回您25分钱!");
        gumballMachine.setState(gumballMachine.getNoQuarterState);
    }
    
    public void turnCrank() {
        System.out.println("请等待糖果出来!");
        
        int winner = randomWinner.nextInt(10);
        if((winner == 0) && (gumballMachine.getCount() > 1)) {
            gumballMachine.setState(gumballMachine.getWinnerState());
        } else {
            gumballMachine.setState(gumballMachine.getSoldState());
        }
    }
    
    public void dispense() {
        System.out.println("请先转动曲柄!");
    }
}

//还有一点需要特别提醒一下,既然我们的赢家状态类WinnerState和我们的售出状态类SoldState的功能几乎一样,那么我什么不把我们的赢家状态类里面的逻辑也写到售出状态类里面呢?因为这也是我们的一个软件设计原则,即一个类只给它一个责任,让它只做一件事,为什么呢?假如你现在把赢家状态类的逻辑写到了售出状态类里面了,那如果后面我们的赢家概率改变了,你是不是不得不修改售出状态类里面的逻辑,这是不合理的,因为我们的售出状态类的逻辑根本没有变化,因此我们需要把赢家状态类的逻辑单独放到一个类里面,这样我们后期修改代码的时候不会伤及无辜。并且如果不分开写两个状态类,对于阅读你代码的人也会造成误解,不易读。

责任链设计模式

主要的思想,设计原则:当你想要让一个以上的对象执行请求的时候,就是用责任链设计模式。因为像我们的普通的请求,一般都是给一个对象的某个方法,但是责任链设计模式会把一个请求给好多个对象执行

优点:可以将请求的发送者和接收者解耦,如果不使用责任链设计模式的话,那么请求发送者和请求接受者的耦合度会非常的高,因为如果请求改变的话,我的接收请求的方法要变的东西就会很多;但是如果使用了责任链设计模式的话,请求改变的话我可能仅仅改变某一个处理请求的链中的某一个handler处理器就可以。

场景:假设我们有一个请假系统,员工可以提交请假申请,然后由各级主管审批。每个主管都有自己的审批权限和职责范围。我们可以使用责任链模式来实现这个场景。

//首先,我们定义一个抽象的处理器类Handler,它包含一个指向下一个处理器的引用和一个处理请求的方法handleRequest
public abstract class Handler {
    protected Handler nextHandler;

    public void setNextHandler(Handler nextHandler) {
        this.nextHandler = nextHandler;
    }

    public abstract void handleRequest(LeaveRequest request);
}

//接下来,我们定义具体的处理器类,例如DirectorHandler、ManagerHandler和CEOHandler,它们分别表示不同级别的主管。
public class DirectorHandler extends Handler {
    @Override
    public void handleRequest(LeaveRequest request) {
        if (request.getDays() <= 3) {
            System.out.println("主任批准了" + request.getName() + "的请假申请");
        } else {
            if (nextHandler != null) {
                nextHandler.handleRequest(request);
            } else {
                System.out.println("请假申请未被批准");
            }
        }
    }
}

public class ManagerHandler extends Handler {
    @Override
    public void handleRequest(LeaveRequest request) {
        if (request.getDays() <= 7) {
            System.out.println("经理批准了" + request.getName() + "的请假申请");
        } else {
            if (nextHandler != null) {
                nextHandler.handleRequest(request);
            } else {
                System.out.println("请假申请未被批准");
            }
        }
    }
}

public class CEOHandler extends Handler {
    @Override
    public void handleRequest(LeaveRequest request) {
        if (request.getDays() <= 15) {
            System.out.println("CEO批准了" + request.getName() + "的请假申请");
        } else {
            System.out.println("请假申请未被批准");
        }
    }
}

//最后,我们创建一个LeaveRequest类来表示请假申请,并在客户端代码中组装责任链。
public class LeaveRequest {
    private String name;
    private int days;

    public LeaveRequest(String name, int days) {
        this.name = name;
        this.days = days;
    }

    public String getName() {
        return name;
    }

    public int getDays() {
        return days;
    }
}

public class Client {
    public static void main(String[] args) {
        Handler directorHandler = new DirectorHandler();
        Handler managerHandler = new ManagerHandler();
        Handler ceoHandler = new CEOHandler();

        directorHandler.setNextHandler(managerHandler);
        managerHandler.setNextHandler(ceoHandler);

        LeaveRequest request1 = new LeaveRequest("张三", 2);
        LeaveRequest request2 = new LeaveRequest("李四", 5);
        LeaveRequest request3 = new LeaveRequest("王五", 10);
        LeaveRequest request4 = new LeaveRequest("赵六", 20);

        directorHandler.handleRequest(request1);
        directorHandler.handleRequest(request2);
        directorHandler.handleRequest(request3);
        directorHandler.handleRequest(request4);
    }
}

运行客户端代码,可以看到如下输出:

主任批准了张三的请假申请
经理批准了李四的请假申请
CEO批准了王五的请假申请
请假申请未被批准

上面使用责任链设计模式的应用场景主要是请假审批场景。
还有一个使用责任链设计模式的场景是Netty的ChannelPipeline里面的各个ChannelHandler处理器其实也是使用的责任链设计模式。

我们学习了这么多种设计模式,那么我们什么时候使用它们呢?

首先千万不要认为:如果没有使用模式解决某个问题,就不是经验丰富的开发人员。

当你设计的时候,尽可能地用最简单的方式解决问题。你的目标应该是简单,而不是“如何在这个问题中应用模式”。

将你的思绪集中在设计本身,而不是在模式上。只有在真正需要时才使用模式。有些时候,简单的方式就行得通,那么就别用模式。

那么到底什么时候使用模式呢?当你确信你的设计中有一个问题需要解决的时候,或者当你确信未来的需求可能会改变时,都可以采用模式。如果你已经确定未来的需求不会改变,那么你就不用使用模式了,怎样简单怎样来。因为,模式会带来复杂性,设计模式常常产生一些额外的类和对象,所以会增加设计的复杂度。设计模式也会在你的设计中加入更多层,这不但增加复杂性,而且效率下降。

本文标签: 二十三种模式