【设计模式实战】命令模式:原理篇

前言

小明在公司负责开发音乐应用,用户听音乐的时候可以点击播放、暂停、上一首、下一首,可以循环播放。

小明是这样写的:

/**
 * 循环播放的音乐播放器实例
 */
public class PlayerManager {

    private List<String> mList = new ArrayList();
    private int pos = 0;

    public PlayerManager() {

    }

    public int getPos() {
        return pos;
    }

    public List<String> getList() {
        return mList;
    }

    public void setList(List<String> list) {
        mList = list;
    }

    public void start() {
        System.out.println("开始播放:" + mList.get(pos));
    }

    public void stop() {
        System.out.println("暂停播放:" + mList.get(pos));
    }

    public void next() {
        System.out.println("播放下一首==============================>");
        if (mList.size() == (pos + 1)) {
            pos = 0;
        } else {
            pos++;
        }
        start();
    }

    public void pre() {
        System.out.println("播放上一首==============================>");
        if (pos == 0) {
            pos = mList.size() - 1;
        } else {
            pos--;
        }
        start();
    }

}
public void test() {
        PlayerManager player = new PlayerManager();
        List<String> mList = new ArrayList();
        mList.add("1.mp3");
        mList.add("2.mp3");
        mList.add("3.mp3");
        player.setList(mList);

        player.start();
        player.stop();

        player.next();
        player.next();
        player.next();
        player.next();

        player.pre();
        player.pre();
        player.pre();
        player.pre();
    }

输出结果

开始播放:1.mp3
暂停播放:1.mp3
播放下一首==============================>
开始播放:2.mp3
播放下一首==============================>
开始播放:3.mp3
播放下一首==============================>
开始播放:1.mp3
播放下一首==============================>
开始播放:2.mp3
播放上一首==============================>
开始播放:1.mp3
播放上一首==============================>
开始播放:3.mp3
播放上一首==============================>
开始播放:2.mp3
播放上一首==============================>
开始播放:1.mp3

小明的领导一看,说这样写不行,如果业务逻辑更复杂了,比如增加顺序播放、随机播放、单首循环播放,或者在播放前要判断是否会员,是否购买该歌曲,岂不是要修改一直Player类?Player类越来越臃肿。Player应该只负责播放器的功能,不应该掺杂业务逻辑,不符合单一职责。Player的扩展性也很低,如果要增加新的播放器命令,比如音效、倍速播放、定时关闭等等功能,只能在原来的基础上去增加。

领导建议使用命令模式,播放器负责播放逻辑,用户负责请求,命令负责执行逻辑。


使用命令模式改造

改造时,增加一个业务逻辑:当播放某首没有权限的歌时,自动切换到下一首歌。

播放器只负责播放器的功能,比如播放和暂停。

/**
 * 音乐播放器实例
 */
public class Player {

    public Player() {

    }

    public void start(String url) {
        System.out.println("开始播放:" + url);
    }

    public void stop(String url ) {
        System.out.println("暂停播放:" + url);
    }

}

抽象出命令类

public interface Command {
    /**
     * 命令执行方法
     */
    void execute();
}

具体命令

public class StartCommand implements Command {
    private Player mPlayer;

    public StartCommand(Player player) {
        mPlayer = player;
    }

    @Override
    public void execute(String url) {
        mPlayer.start(url);
    }

}
public class StopCommand implements Command {
    private Player mPlayer;

    public StopCommand(Player player) {
        mPlayer = player;
    }

    @Override
    public void execute(String url) {
        mPlayer.stop(url);
    }

}

Buttons通过命令对播放器进行控制,写一些业务逻辑

public class Buttons {
    private List<String> mList = new ArrayList();
    private int pos = 0;
    private StartCommand mStartCommand;
    private StopCommand mStopCommand;

    public void setStartCommand(StartCommand startCommand) {
        mStartCommand = startCommand;
    }

    public void setStopCommand(StopCommand stopCommand) {
        mStopCommand = stopCommand;
    }

    public void start() {

        String url = mList.get(pos);
        //假设3.map没有权限播放
        if (url.equals("3.mp3")) {
            System.out.println("没有“3.mp3”的播放权限,将播放下一首歌");
            //发送播放下一首歌的指令
            next();
            return;
        }
        mStartCommand.execute(mList.get(pos));
    }

    public void stop() {
        mStopCommand.execute(mList.get(pos));
    }

    public void next() {
        pos = getNextUrlPos();
        start();
    }

    public void pre() {
        pos = getPreUrlPos();
        start();
    }

    public List<String> getList() {
        return mList;
    }

    public void setList(List<String> list) {
        mList = list;
    }

    public int getNextUrlPos() {
        int next;
        if (mList.size() == (pos + 1)) {
            next = 0;
        } else {
            next = pos + 1;
        }
        return next;
    }

    public int getPreUrlPos() {
        int next;
        if (pos == 0) {
            next = mList.size() - 1;
        } else {
            next = pos - 1;
        }
        return next;
    }

}

开始测试

 public void test() {
         Player player = new Player();

        StartCommand startCommand = new StartCommand(player);
        StopCommand stopCommand = new StopCommand(player);

        Buttons buttons = new Buttons();
        buttons.setStartCommand(startCommand);
        buttons.setStopCommand(stopCommand);

        List<String> mList = new ArrayList();
        mList.add("1.mp3");
        mList.add("2.mp3");
        mList.add("3.mp3");
        buttons.setList(mList);

        buttons.start();
        buttons.stop();

        buttons.next();
        buttons.next();
        buttons.next();

        buttons.pre();
        buttons.pre();
        buttons.pre();

    }

输出结果

开始播放:1.mp3
暂停播放:1.mp3
开始播放:2.mp3
没有“3.mp3”的播放权限,将播放下一首歌
开始播放:1.mp3
开始播放:2.mp3
开始播放:1.mp3
没有“3.mp3”的播放权限,将播放下一首歌
开始播放:1.mp3
没有“3.mp3”的播放权限,将播放下一首歌
开始播放:1.mp3

可以看到,播放器类只专注于自己的播放和暂停功能即可。Buttons不直接调用播放器的方法,而是通过具体命令去调用,这样无论播放器怎么改动,只会影响命令,不会影响Buttons。Buttons负责具体的业务逻辑,如果有一天,需要另外做一套有不同逻辑的播放界面,那么只需要再另外创建一个Buttons,再写一套业务逻辑即可。如果不使用命令模式,如果需要改动业务逻辑,岂不是要在Player上修改?Player将逻辑越来越复杂,让人难以理解。

命令模式讲解

看一下命令模式的UML图
在这里插入图片描述
Receiver,接收者:执行具体逻辑的底层代码,任何一个类都可以成为一个接收者,封装了具体操作逻辑的方法称为行动方法。

Command,抽象命令:定义所有具体命令类的抽象接口。

ConcreteCommand,具体命令:实现了Command接口,负责执行接收者的方法。

Invoker,请求者:负责调用具体命令

意图:
将一个请求封装成一个对象,从而使您可以用不同的请求对客户进行参数化。

主要解决:
在软件系统中,行为请求者与行为实现者通常是一种紧耦合的关系,但某些场合,比如需要对行为进行记录、撤销或重做、事务等处理时,这种无法抵御变化的紧耦合的设计就不太合适。

命令模式进阶

假设我们想一次性执行多个命令呢?

public class Controller {
    private List<Command> Commands = new ArrayList<>();

    public void addCommand(Command Command) {
        Commands.add(Command);
    }

    public void execute(Command Command) {
        Command.execute();
    }

    public void executes() {
        for (Command Command:
             Commands) {
            Command.execute();
        }
        Commands.clear();
    }
}

        // 控制条可以执行单挑命令也可以批量执行多条命令
        VideoPlayer player = new VideoPlayer();
        Controller controller = new Controller();
        controller.execute(new PlayCommand(player));

        controller.addCommand(new PauseCommand(player));
        controller.addCommand(new PlayCommand(player));
        controller.addCommand(new StopCommand(player));
        controller.addCommand(new SpeedCommand(player));
        controller.executes();

假设我们想对命令做一个撤销功能呢?比如键盘打字后,按“Ctrl+Z”可以返回上一个操作

ICommand命令类

public interface ICommand {

    void write();

    void undo();

}

WriteCommand具体命令类

public class WriteCommand implements ICommand {

    public String word;
    public Tablet tablet;

    public WriteCommand( Tablet tablet,String word) {
        this.word = word;
        this.tablet = tablet;
    }

    @Override
    public void write() {
        tablet.write(word);
    }

    @Override
    public void undo() {
        tablet.undo();
    }
    
}

接收者Tablet ,负责具体的写字和撤销逻辑


/**
 * 写字板实例
 */
public class Tablet {
    private List<String> mList = new ArrayList<>();

    public void write(String word) {
        mList.add(word);
        System.out.println("显示:" + mList.toString());
    }

    public void undo() {
        if (mList.size() > 0) {
            mList.remove(mList.size() - 1);
        }
        System.out.println("显示:" + mList.toString());
    }

}

调用者Invoker类,将执行过的命令的都保存起来,方便撤销


public class Invoker {

    private List<WriteCommand> mCommands = new ArrayList<>();

    public void execute(WriteCommand command) {
        mCommands.add(command);
        command.write();
    }

    public void executes(List<WriteCommand> commands) {
        mCommands.addAll(commands);
        for (ICommand command :
                commands) {
            command.write();
        }
    }

    public void undo() {
        if (mCommands.size() > 0) {
            WriteCommand writeCommand = mCommands.get(mCommands.size() - 1);
            writeCommand.undo();
        }
    }

}

测试

  public void test5() {
			 Tablet tablet = new Tablet();
        
        Invoker invoker = new Invoker();
        invoker.execute(new WriteCommand(tablet,"a"));//执行一个命令
        invoker.execute(new WriteCommand(tablet,"b"));//执行一个命令
        invoker.execute(new WriteCommand(tablet,"c"));//执行一个命令

        List<WriteCommand> mCommands = new ArrayList<>();
        mCommands.add(new WriteCommand(tablet,"d"));
        mCommands.add(new WriteCommand(tablet,"e"));
        mCommands.add(new WriteCommand(tablet,"f"));

        invoker.executes(mCommands);//执行一系列命令
        invoker.undo();//撤销
        invoker.undo();//撤销

    }

输出结果

显示:[a]
显示:[a, b]
显示:[a, b, c]
显示:[a, b, c, d]
显示:[a, b, c, d, e]
显示:[a, b, c, d, e, f]
显示:[a, b, c, d, e]
显示:[a, b, c, d]

在一些需要有撤销功能的应用中,是很适合用命令模式的,比如画图、图片编辑、视频编辑、富文本编辑器。

又比如电商产品,要求程序员将用户在购买完成后,把购买之前经历过的流程,都反馈到后台,方便进行行为分析。比如说“进入首页->搜索商品->打开详情->查看第一页评论->查看第二页评论->查看第三页评论->点击购买->付款成功”,可以知道该用户是很在意评论的。

其实用户的这些行为,是不是就像一个个命令,我们完全可以在购买之前将这些命令记录下来,然后在购买完成时发送到后台,最后清理命令。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值