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


    前言

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

    小明是这样写的:

    /**
     * 循环播放的音乐播放器实例
     */
    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();
        }
    
    }
    
    • 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
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    输出结果

    开始播放:1.mp3
    暂停播放:1.mp3
    播放下一首==============================>
    开始播放:2.mp3
    播放下一首==============================>
    开始播放:3.mp3
    播放下一首==============================>
    开始播放:1.mp3
    播放下一首==============================>
    开始播放:2.mp3
    播放上一首==============================>
    开始播放:1.mp3
    播放上一首==============================>
    开始播放:3.mp3
    播放上一首==============================>
    开始播放:2.mp3
    播放上一首==============================>
    开始播放:1.mp3
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    小明的领导一看,说这样写不行,如果业务逻辑更复杂了,比如增加顺序播放、随机播放、单首循环播放,或者在播放前要判断是否会员,是否购买该歌曲,岂不是要修改一直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);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    抽象出命令类

    public interface Command {
        /**
         * 命令执行方法
         */
        void execute();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    具体命令

    public class StartCommand implements Command {
        private Player mPlayer;
    
        public StartCommand(Player player) {
            mPlayer = player;
        }
    
        @Override
        public void execute(String url) {
            mPlayer.start(url);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    public class StopCommand implements Command {
        private Player mPlayer;
    
        public StopCommand(Player player) {
            mPlayer = player;
        }
    
        @Override
        public void execute(String url) {
            mPlayer.stop(url);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    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;
        }
    
    }
    
    • 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
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70

    开始测试

     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
    • 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
    • 28

    输出结果

    开始播放:1.mp3
    暂停播放:1.mp3
    开始播放:2.mp3
    没有“3.mp3”的播放权限,将播放下一首歌
    开始播放:1.mp3
    开始播放:2.mp3
    开始播放:1.mp3
    没有“3.mp3”的播放权限,将播放下一首歌
    开始播放:1.mp3
    没有“3.mp3”的播放权限,将播放下一首歌
    开始播放:1.mp3
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    可以看到,播放器类只专注于自己的播放和暂停功能即可。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();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
            // 控制条可以执行单挑命令也可以批量执行多条命令
            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();
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

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

    ICommand命令类

    public interface ICommand {
    
        void write();
    
        void undo();
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    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();
        }
        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    接收者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());
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    调用者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();
            }
        }
    
    }
    
    • 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

    测试

      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();//撤销
    
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    输出结果

    显示:[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]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

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

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

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

  • 相关阅读:
    Java中的SPI原理浅谈
    大学生第一款浏览器怎么选,这款浏览器适合学生用
    Java的动态代理Proxy.newProxyInstance
    企业微信hook接口协议,标签变动回调
    “Linux免除系统交互操作方法、expect自动化交互工具” 及 “SSH批量修改主机密码脚本”
    minikube部署K8s命令
    【基于Python加django的学生综合成绩管理系统-哔哩哔哩】 https://b23.tv/6zjxc4Z
    如何使用springcloud LoadBalancer代替ribbon
    js详细讲解放大镜的实现
    2023最新PS(photoshop)Win+Mac免费下载安装包及教程内置AI绘画-网盘下载
  • 原文地址:https://blog.csdn.net/iromkoear/article/details/126368319