headfirst设计模式(7)—命令模式

一、前言

什么是命令模式?

在软件系统中,“行为请求者”与“行为实现者”通常呈现一种“紧耦合”。但在某些场合,比如要对行为进行“记录、撤销/重做、事务”等处理,这种无法抵御变化的紧耦合是不合适的。在这种情况下,如何将“行为请求者”与“行为实现者”解耦?将一组行为抽象为对象,实现二者之间的松耦合,这就是命令模式(Command Pattern)——摘自百度百科

它解决了什么问题?

解除行为请求者和行为实现者的高耦合。

比如,有一个业务,最开始的时候是:A->B

然后,有一天产品说,B这个资源比较敏感,每次调用B之前要先记录一下日志,假如说记录日志的业务逻辑是C,然后业务逻辑就会变成下面这个样子:

A -> C, A-> B

再然后,产品又说,B这个资源啊,有点不稳定,如果调用失败的时候,能不能重试3次?然后业务逻辑就变成这个样子:

A->C, retry 3 :A->B

后来,产品觉得B资源...,不这次不是B资源了,出现了一个新需要,处理的逻辑和B有点像,能不能$%^&?

结果,那块代码就变得无法维护了。。。如果有一天,需要去重构它,或者碰到一个类似的需求的时候,如何不让自己难受,不给别人挖坑?

命令模式买不了吃亏,买不了上当!

 

二、以餐厅点餐,了解命令模式

举个例子:假如你去一个餐厅吃烤鱼,从叫服务员点菜,到服务员通知师傅做菜,到最后服务员上菜,这个流程就可以描述成一个命令模式,类图如下:

 

 

每个角色和命令模式的几个核心要素对应关系如上图所示。

Customer(Client)

/**
 * 顾客(Client)
 */
public class Customer {

  public static void main(String[] args) {
      
    Order order = new BakeFishOrder(new Chef("李"));

    Waiter waiter = new Waiter();
    waiter.takeOrder(order);
    waiter.orderUp();
  }
}

顾客需要做3个事情:

1,点一个烤鱼

2,把菜单交给服务员

3,服务员上菜

顾客不关心这个烤鱼的制作流程,只希望,我点菜以后,能够按时把菜上上来就OK了,在实际的业务中,甚至可以把 new Chef() 这个操作也放在其他地方实现

比如具体的Command里面,或者Invoker里面,让用户感知不到,这样业务的逻辑会变得更加平滑,总的来说就是:只关注自己需要的东西

Waiter(Invoker)

/**
 * 服务员(Invoker)
 */
public class Waiter {

  private Order order;

  public void takeOrder(Order order){
    System.out.println("服务员接到订单!");
    this.order = order;
  }

  public void orderUp(){
    System.out.println("服务员呼叫后台准备烹饪订单中的菜品");
    order.execute();
    System.out.println("订单菜品已经烹饪完成,服务员上菜!");
  }
}

服务员这里需要做两件事情:

1,接单

2,通知厨房做菜并上菜

对于服务员来说,他也不需要关心这道菜谁做的,怎么做的,只需要给厨房说了以后,过段时间把做好的菜送到客户的手上就OK了

Order(Command)

/**
 * 订单(Command)
 */
public interface Order {
  void execute();
}

订单是一个抽象的东西,具体干活的是订单接口实现类,引入订单的作用是面向抽象编程,以后每加一种菜,只需要增加一个实现类就可以了

BakeFishOrder(ConncereteCommand)

/**
 * 烤鱼订单(具体的订单对象)
 */
public class BakeFishOrder implements Order{

  private Chef chef;

  public BakeFishOrder(Chef chef){
    this.chef = chef;
  }

  public void execute() {
    chef.bakeFish();
  }
}

烤鱼订单这里就是一个具体的订单,在它的execute方法中就需要去调用真正的实现者的一个或一些方法来实现制作烤鱼这样的一个需求。

Chef(Receiver)

/**
 * 厨师(Receiver)
 */
public class Chef {

  private String name;

  public Chef(String name){
    this.name = name;
  }

  public void bakeFish(){
    System.out.println(name + "师傅在制作烤鱼...");
  }
}

在这个例子中,厨师就是最后干活的对应命令模式中的Receiver,绕这么大一圈,也就是为了顾客(行为请求者)和 厨师(行为实现者)的解耦。

运行结果:

 

顾客和厨师解耦的好处是什么?

1,在点餐的流程不变的情况下,可以随意增加新菜(Order的实现类)

2,在点餐的流程不变的情况下,原菜品的制作方式变化,上层是无需做任何修改的

3,在Waiter中,可以做各种日志记录,订单排队,之类的各种操作,非耦合的情况下,可操作性很大,并且不会影响到顾客和厨师相关类

三、遥控器需求

HeadFirst上,需要实现的是一个遥控器,遥控器用来遥控房间内的各种家电,并且可以实现undo功能。

但是有几个问题:

1,每个厂商的驱动标准是不一样的,比如电灯,开关的方法是:on(), off(),吊扇的方法是:high(), medium(),low(),off()

2,遥控器直接调用具体的家电驱动的话,一旦新的厂商加载到遥控器上面的时候,就必须要改代码,比如:电灯的开关方法变成了:dim(level),通过level来调节灯泡的明暗程度,调到0就是关,调到100就是开。

如何使用命令模式来实现呢?具体的流程如下:

0,驱动类

/**
 * 描述:灯
 */
public class Light {

    public static final int LIGHT_ON = 100;//打开灯时,灯的亮度
    public static final int LIGHT_OFF = 0;//关闭灯时,灯的亮度

    private String location;//灯的位置
    private int level;//亮度

    public Light(String location) {
        this.location = location;
    }

    //开灯
    public void on() {
        level = LIGHT_ON;
        System.out.println(location + ", 灯已经打开!");
    }

    //关灯
    public void off() {
        level = LIGHT_OFF;
        System.out.println(location + ", 灯已经关闭!");
    }

    /**
     * 调节灯的亮度
     * @param level 亮度
     */
    public void dim(int level) {
        this.level = level;

        if (level == LIGHT_OFF) {
            off();
            return;
        }

        if(level == LIGHT_ON){
            on();
            return;
        }

        throw new RuntimeException("无法调节到指定的亮度, level: " + level);
    }

    public int getLevel() {
        return level;
    }
}
View Code
/**
 * 描述:吊扇
 */
public class CeilingFan {

    public static final int HIGH = 3;//3档
    public static final int MEDIUM = 2;//2档
    public static final int LOW = 1;//1档
    public static final int OFF = 0;//0档
    String location;//吊扇位置
    private int speed;//当前速度
 
    public CeilingFan(String location) {
        this.location = location;
        speed = OFF;
    }
  
    public void high() {
        speed = HIGH;
        System.out.println(location + ",吊扇档位:" + HIGH);
    } 
 
    public void medium() {
        speed = MEDIUM;
        System.out.println(location + ",吊扇档位:" + MEDIUM);
    }
 
    public void low() {
        speed = LOW;
        System.out.println(location + ",吊扇档位:" + LOW);
    }
  
    public void off() {
        speed = OFF;
        System.out.println(location + ",吊扇档位:" + OFF);
    }
  
    public int getSpeed() {
        return speed;
    }
}
View Code

1,制定标准,每个家电都必须依照这个规范来进行开发,接入遥控器系统(Command接口)

/**
 * 描述:命令接口
 */
public interface Command {
    /**
     * 描述:执行命令
     */
    void execute();

    /**
     * 描述:撤销命令
     */
    void undo();
}

2,根据需求先实现遥控器,再实现具体的命令(由上而下开发)

/**
 * 遥控器
 */
public class RemoteControl {
    Command[] onCommands;//启动命令
    Command[] offCommands;//关闭命令
    Command undoCommand;//撤销命令
 
    public RemoteControl() {
        //初始化遥控器,命令设置为默认实现类NoCommand,减少判断逻辑,减少NLP概率
        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;
    }

    /**
     * 设置命令
     * @param slot 按钮位置
     * @param onCommand 启动命令
     * @param offCommand 关闭命令
     */
    public void setCommand(int slot, Command onCommand, Command offCommand) {
        onCommands[slot] = onCommand;
        offCommands[slot] = offCommand;
    }

    /**
     * 启动按钮按下时的操作
     * @param slot 按钮位置
     */
    public void onButtonWasPushed(int slot) {
        onCommands[slot].execute();
        undoCommand = onCommands[slot];
    }

    /**
     * 关闭按钮按下时的操作
     * @param slot 按钮位置
     */
    public void offButtonWasPushed(int slot) {
        offCommands[slot].execute();
        undoCommand = offCommands[slot];
    }

    /**
     * 撤销按钮按下时的操作
     */
    public void undoButtonWasPushed() {
        undoCommand.undo();
    }
  
    public String toString() {
        StringBuffer stringBuff = new StringBuffer();
        stringBuff.append("\n------ 遥控器 -------\n");
        for (int i = 0; i < onCommands.length; i++) {
            stringBuff.append("[位置 " + i + "] " + onCommands[i].getClass().getName()
                + "    " + offCommands[i].getClass().getName() + "\n");
        }
        stringBuff.append("[撤销] " + undoCommand.getClass().getName() + "\n");
        return stringBuff.toString();
    }
}
/**
 * 描述:命令的默认的空实现
 */
public class NoCommand implements Command {
    public void execute() { }
    public void undo() { }
}

3,实现电灯的开关

/**
 * 描述:开灯命令
 */
public class LightOnCommand implements Command {
    private Light light;
    private int lastLightLevel;
    public LightOnCommand(Light light) {
        this.light = light;
    }
    public void execute() {
        lastLightLevel = light.getLevel();
        light.on();
    }

    public void undo() {
        light.dim(lastLightLevel);
    }
}
View Code
/**
 * 描述:关灯命令
 */
public class LightOffCommand implements Command {

    private Light light;
    private int lastLightLevel;

    public LightOffCommand(Light light) {
        this.light = light;
    }

    public void execute() {
        lastLightLevel = light.getLevel();
        light.off();
    }

    public void undo() {
        light.dim(lastLightLevel);
    }
}
View Code

4,实现风扇的各个档位

public class CeilingFanHighCommand implements Command {
    CeilingFan ceilingFan;
    int prevSpeed;
  
    public CeilingFanHighCommand(CeilingFan ceilingFan) {
        this.ceilingFan = ceilingFan;
    }
 
    public void execute() {
        prevSpeed = ceilingFan.getSpeed();
        ceilingFan.high();
    }
 
    public 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();
        }
    }
}
View Code
public class CeilingFanMediumCommand implements Command {
    CeilingFan ceilingFan;
    int prevSpeed;
  
    public CeilingFanMediumCommand(CeilingFan ceilingFan) {
        this.ceilingFan = ceilingFan;
    }
 
    public void execute() {
        prevSpeed = ceilingFan.getSpeed();
        ceilingFan.medium();
    }
 
    public 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();
        }
    }
}
View Code
public class CeilingFanLowCommand implements Command {
    CeilingFan ceilingFan;
    int prevSpeed;
  
    public CeilingFanLowCommand(CeilingFan ceilingFan) {
        this.ceilingFan = ceilingFan;
    }
 
    public void execute() {
        prevSpeed = ceilingFan.getSpeed();
        ceilingFan.low();
    }
 
    public 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();
        }
    }
}
View Code
public class CeilingFanOffCommand implements Command {
    CeilingFan ceilingFan;
    int prevSpeed;
  
    public CeilingFanOffCommand(CeilingFan ceilingFan) {
        this.ceilingFan = ceilingFan;
    }
 
    public void execute() {
        prevSpeed = ceilingFan.getSpeed();
        ceilingFan.off();
    }
 
    public 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();
        }
    }
}
View Code

当然这种并不是最好的方式,undo中的代码被写了几次,这样,哪里有重复,哪里就有抽象(实际业务中,请根据具体业务选择是否抽象,如果抽象了发现业务更难做了,就要考虑是不是抽象的方向有问题了)

public abstract class AbstractCeilingFanCommand implements Command{
  private CeilingFan ceilingFan;
  private int prevSpeed;

  public AbstractCeilingFanCommand(CeilingFan ceilingFan) {
    this.ceilingFan = ceilingFan;
  }

  public void execute() {
    prevSpeed = ceilingFan.getSpeed();
    doExecute();
  }

  protected abstract void doExecute();

  public 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();
    }
  }

  protected CeilingFan getCeilingFan() {
    return ceilingFan;
  }
}

重新实现各个档位

public class CeilingFanHighCommand extends AbstractCeilingFanCommand{

    public CeilingFanHighCommand(CeilingFan ceilingFan) {
        super(ceilingFan);
    }

    protected void doExecute() {
        getCeilingFan().high();
    }
}
View Code
public class CeilingFanMediumCommand extends AbstractCeilingFanCommand{

    public CeilingFanMediumCommand(CeilingFan ceilingFan) {
        super(ceilingFan);
    }

    protected void doExecute() {
        getCeilingFan().medium();
    }
}
View Code
public class CeilingFanLowCommand extends AbstractCeilingFanCommand {
    public CeilingFanLowCommand(CeilingFan ceilingFan) {
        super(ceilingFan);
    }
    protected void doExecute() {
        getCeilingFan().low();
    }
}
View Code
public class CeilingFanOffCommand extends AbstractCeilingFanCommand {

    public CeilingFanOffCommand(CeilingFan ceilingFan) {
        super(ceilingFan);
    }

    protected void doExecute() {
        getCeilingFan().off();
    }
}
View Code

最后是测试类:

public class TestClient {

    private static RemoteControl REMOTE_CONTROL = new RemoteControl();
    static{

        //初始化灯也可以是抽象的,因为,Light的命令,并不关心是什么灯
        Light livingRoomLight = new Light("卧室");
        //设置开灯,关灯命令
        REMOTE_CONTROL.setCommand(0, new LightOnCommand(livingRoomLight), new LightOffCommand(livingRoomLight));

        //初始化吊扇
        CeilingFan ceilingFan = new CeilingFan("卧室");
        //初始化吊扇每个档位的命令
        CeilingFanOffCommand ceilingFanOff = new CeilingFanOffCommand(ceilingFan);
        REMOTE_CONTROL.setCommand(1, new CeilingFanLowCommand(ceilingFan), ceilingFanOff);
        REMOTE_CONTROL.setCommand(2, new CeilingFanMediumCommand(ceilingFan), ceilingFanOff);
        REMOTE_CONTROL.setCommand(3, new CeilingFanHighCommand(ceilingFan), ceilingFanOff);
    }
 
    public static void main(String[] args) {

        System.out.println(REMOTE_CONTROL);

        REMOTE_CONTROL.onButtonWasPushed(0);
        REMOTE_CONTROL.offButtonWasPushed(0);

        System.out.print("撤销命令执行:");
        REMOTE_CONTROL.undoButtonWasPushed();

        REMOTE_CONTROL.onButtonWasPushed(1);
        REMOTE_CONTROL.onButtonWasPushed(2);
        System.out.print("撤销命令执行:");
        REMOTE_CONTROL.undoButtonWasPushed();

        REMOTE_CONTROL.onButtonWasPushed(3);
        System.out.print("撤销命令执行:");
        REMOTE_CONTROL.undoButtonWasPushed();
  
        REMOTE_CONTROL.offButtonWasPushed(3);
        System.out.print("撤销命令执行:");
        REMOTE_CONTROL.undoButtonWasPushed();
    }
}

测试结果:

梳理一下,命令模式和这里的对应关系:

Invoker:RemoteControl(遥控器)

Command:Command接口

ConcereteCommand:LightOnCommand,LightOffCommand,CeilingFanLowCommand....

Receiver:Light,CeilingFan

Client:TestClient

对TestClient和具体的驱动类(Light,CeilingFan)实现解耦,Client不直接调用驱动类,而是基于遥控器(Invoker)制定的标准,来调用具体的命令实现类。

这样,在新增新的家电驱动的时候,只需要新增命令实现类和Client的初始化代码即可,也可以做成配置文件,这样就不用去改TestClient代码了

四、命令模式优缺点

优点:

1,降低了请求者和执行者之间的耦合性

2,新命令的扩展性很强,需要新的命令,只需要加一个实现类即可,对现有代码影响很小

3,命令之间还可以相互组合形成新的命令,比如在遥控器里面要实现一个闪光效果,可以用LightOnCommand,LightOffCommand,结合起来做一个宏命令

缺点:

命令的数量可能会很多,每个指令都必须封装成一个类,如果指令过多,那么开发的成本会很高

所以,只有在请求者和执行者真的需要解耦的时候才使用:

比如:A->B的调用中新增了很多操作,日志,排队,各种逻辑,需要引入Invoker

比如:有A->C逻辑和B相似,则可以引入Command接口来抽象这一流程

比如:B,C逻辑需要撤销,或者重做的时候,也可以引入命令模式

总之,根据需要来抽象,没有最好的,只有当下最合适的

或许,spring aop就很合适呢? 

posted @ 2019-02-25 21:51  纷飞丶  阅读(339)  评论(0编辑  收藏  举报