万字解析设计模式之策略模式、命令模式

一、策略模式

1.1概述

先看下面的图片,我们去旅游选择出行模式有很多种,可以骑自行车、可以坐汽车、可以坐火车、可以坐飞机。

 策略模式(Strategy Pattern)是一个行为型设计模式,它定义了一组算法家族,分别封装起来,让它们之间可以互相替换,且算法的变化不会影响使用算法的客户。通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。

1.2结构

策略模式的主要角色如下:

  • 抽象策略(Strategy)类:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。定义了所有支持的算法的共同接口,可以是抽象类、接口或抽象类和接口的组合。
  • 具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现或行为。
  • 环境(Context)类:持有一个策略类的引用,将具体的算法委托给这些策略对象来处理,最终给客户端调用。

1.3实现

【例】促销活动

一家百货公司在定年度的促销活动。针对不同的节日(春节、中秋节、圣诞节)推出不同的促销活动,由促销员将促销活动展示给客户。类图如下:

 抽象策略(Strategy)类

package com.yanyu.Strategyer;


public interface Strategy {
    void show();
}

具体策略角色(Concrete Strategy)

package com.yanyu.Strategyer;

//为春节准备的促销活动A
public class StrategyA implements Strategy {

    public void show() {
        System.out.println("买一送一");
    }
}
package com.yanyu.Strategyer;

//为中秋准备的促销活动B
public class StrategyB implements Strategy {
    public void show() {
        System.out.println("满200元减50元");
    }
}
package com.yanyu.Strategyer;

//为圣诞准备的促销活动C
public class StrategyC implements Strategy {
    public void show() {
        System.out.println("满1000元加一元换购任意200元以下商品");
    }
}

环境(Context)类

package com.yanyu.Strategyer;

public class SalesMan {
    //持有抽象策略角色的引用
    private Strategy strategy;

    public SalesMan(Strategy strategy) {
        this.strategy = strategy;
    }

    public Strategy getStrategy() {
        return strategy;
    }

    public void setStrategy(Strategy strategy) {
        this.strategy = strategy;
    }

    //向客户展示促销活动
    public void salesManShow(){
        strategy.show();
    }
}

客户端类

package com.yanyu.Strategyer;

public class Client {
    public static void main(String[] args) {
        //春节来了, 使用春节促销活动
        SalesMan salesMan = new SalesMan(new StrategyA());
       // 展示促销活动
        salesMan.salesManShow();
        System.out.println("=============");
       // 中秋节到了, 使用中秋节的促销活动
        salesMan.setStrategy (new StrategyB());
        // 展示促销活动
        salesMan.salesManShow();
    }
}

1.4优缺点

1,优点:

  • 策略类之间可以自由切换

    由于策略类都实现同一个接口,所以使它们之间可以自由切换。

  • 易于扩展

    增加一个新的策略只需要添加一个具体的策略类即可,基本不需要改变原有的代码,符合“开闭原则“

  • 避免使用多重条件选择语句(if else),充分体现面向对象设计思想。

2,缺点:

  • 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。
  • 策略模式将造成产生很多策略类,可以通过使用享元模式在一定程度上减少对象的数量

1.5使用场景

  • 当你想使用对象中各种不同的算法变体, 并希望能在运行时切换算法时, 可使用策略模式;

  • 当你有许多仅在执行某些行为时略有不同的相似类时, 可使用策略模式;

  • 如果算法在上下文的逻辑中不是特别重要, 使用该模式能将类的业务逻辑与其算法实现细节隔离开来;

  • 当类中使用了复杂条件运算符以在同一算法的不同变体中切换时, 可使用该模式。

  • 系统要求使用算法的客户不应该知道其操作的数据时,可使用策略模式来隐藏与算法相关的数据结构。
  • 多个类只区别在表现行为不同,可以使用策略模式,在运行时动态选择具体要执行的行为

二、 命令模式

2.1概述

日常生活中,我们出去吃饭都会遇到下面的场景。

命令模式是一种行为设计模式,将请求或操作封装成对象,从而使请求者和接收者松耦合,使发出请求的责任和执行请求的责任分割开。在命令模式中,命令就像是一个对象,可以被存储、重复、撤销、恢复等操作,因此它可以通过队列、日志等方式来管理和操作。

为什么需要命令模式

假如你正在开发一款新的文字编辑器, 当前的任务是创建一个包含多个按钮的工具栏, 并让每个按钮对应编辑器的不同操作。 你创建了一个非常简洁的 按钮类, 它不仅可用于生成工具栏上的按钮, 还可用于生成各种对话框的通用按钮。

最简单的解决方案是在使用按钮的每个地方都创建大量的子类。 这些子类中包含按钮点击后必须执行的代码。

zeren

你很快就意识到这种方式有严重缺陷。 首先, 你创建了大量的子类, 当每次修改基类 按钮时, 你都有可能需要修改所有子类的代码。 简单来说, GUI 代码以一种拙劣的方式依赖于业务逻辑中的不稳定代码。 还有一个部分最难办。 复制/粘贴文字等操作可能会在多个地方被调用。 例如用户可以点击工具栏上小小的 “复制” 按钮, 或者通过上下文菜单复制一些内容, 又或者直接使用键盘上的 Ctrl+C 。

最简单的解决方案是在使用按钮的每个地方都创建大量的子类。 这些子类中包含按钮点击后必须执行的代码。

zeren

你很快就意识到这种方式有严重缺陷。 首先, 你创建了大量的子类, 当每次修改基类 按钮时, 你都有可能需要修改所有子类的代码。 简单来说, GUI 代码以一种拙劣的方式依赖于业务逻辑中的不稳定代码。 还有一个部分最难办。 复制/粘贴文字等操作可能会在多个地方被调用。 例如用户可以点击工具栏上小小的 “复制” 按钮, 或者通过上下文菜单复制一些内容, 又或者直接使用键盘上的 Ctrl+C 。

用命令模式解决问题

软件设计通常会将关注点进行分离。 最常见的例子: 一层负责用户图像界面; 另一层负责业务逻辑。 GUI 层负责在屏幕上渲染美观的图形, 捕获所有输入并显示用户和程序工作的结果。 当需要完成一些重要内容时 (比如计算月球轨道或撰写年度报告), GUI 层则会将工作委派给业务逻辑底层。

这在代码中看上去就像这样: 一个 GUI 对象传递一些参数来调用一个业务逻辑对象。 这个过程通常被描述为一个对象发送请求给另一个对象。

 2.2结构

命令模式包含以下主要角色:

  • 抽象命令类(Command)角色: 定义命令的接口,声明执行的方法。
  • 具体命令(Concrete Command)角色:具体的命令,实现命令接口;通常会持有接收者,并调用接收者的功能来完成命令要执行的操作。
  • 实现者/接收者(Receiver)角色: 负责执行具体的命令操作。任何类都可能成为一个接收者,只要它能够实现命令要求实现的相应功能。
  • 调用者/请求者(Invoker)角色: 负责调用命令并执行。这个是客户端真正触发命令并要求命令执行相应操作的地方,也就是说相当于使用命令对象的入口。

2.3实现

将上面的案例用代码实现,那我们就需要分析命令模式的角色在该案例中由谁来充当。

服务员: 就是调用者角色,由她来发起命令。

资深大厨: 就是接收者角色,真正命令执行的对象。

订单: 命令中包含订单。

类图如下:

抽象命令类

package com.yanyu.Commander;

public interface Command {
    void execute();//只需要定义一个统一的执行方法
}

具体命令(Concrete Command)角色

package com.yanyu.Commander;

import java.util.Set;

public class OrderCommand implements Command {

    //持有接受者对象
    private SeniorChef receiver; // 命令模式中的接收者对象
    private Order order; // 命令的参数

    public OrderCommand(SeniorChef receiver, Order order){
        this.receiver = receiver; // 初始化接收者对象
        this.order = order; // 初始化命令的参数
    }

    public void execute()  {
        System.out.println(order.getDiningTable() + "桌的订单:"); // 输出订单信息
        Set<String> keys = order.getFoodDic().keySet(); // 获取订单中食物的键集合
        for (String key : keys) {
            receiver.makeFood(order.getFoodDic().get(key),key); // 调用接收者对象的方法执行制作食物的操作
        }

        try {
            Thread.sleep(100); // 停顿一下 模拟做饭的过程
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(order.getDiningTable() + "桌的饭弄好了"); // 输出制作完成的信息
    }
}

接收者(Receiver)角色

package com.yanyu.Commander;

// 资深大厨类 是命令的Receiver
public class SeniorChef {
    public void makeFood(int num,String foodName) {
        System.out.println(num + "份" + foodName);
    }
}

调用者

package com.yanyu.Commander;

import java.util.ArrayList;

public class Waitor {

    private ArrayList<Command> commands;//可以持有很多的命令对象

    public Waitor() {
        commands = new ArrayList();
    }

    public void setCommand(Command cmd){
        commands.add(cmd);
    }

    // 发出命令 喊 订单来了,厨师开始执行
    public void orderUp() {
        System.out.println("美女服务员:叮咚,大厨,新订单来了.......");
        for (int i = 0; i < commands.size(); i++) {
            Command cmd = commands.get(i);
            if (cmd != null) {
                cmd.execute();
            }
        }
    }
}

命令的请求

package com.yanyu.Commander;

import java.util.HashMap;
import java.util.Map;

public class Order {
    // 餐桌号码
    private int diningTable;

    // 用来存储餐名并记录份数
    private Map<String, Integer> foodDic = new HashMap<String, Integer>();

    public int getDiningTable() {
        return diningTable; // 获取餐桌号码
    }

    public void setDiningTable(int diningTable) {
        this.diningTable = diningTable; // 设置餐桌号码
    }

    public Map<String, Integer> getFoodDic() {
        return foodDic; // 获取食物名称和数量的映射
    }

    public void setFoodDic(String name, int num) {
        foodDic.put(name,num); // 添加食物名称和数量的映射
    }
}

客户端类

package com.yanyu.Commander;

public class Client {
    public static void main(String[] args) {
        //创建2个order
        Order order1 = new Order();
        order1.setDiningTable(1);
        order1.getFoodDic().put("西红柿鸡蛋面",1);
        order1.getFoodDic().put("小杯可乐",2);

        Order order2 = new Order();
        order2.setDiningTable(3);
        order2.getFoodDic().put("尖椒肉丝盖饭",1);
        order2.getFoodDic().put("小杯雪碧",1);

        //创建接收者
        SeniorChef receiver=new SeniorChef();
        //将订单和接收者封装成命令对象
        OrderCommand cmd1 = new OrderCommand(receiver, order1);
        OrderCommand cmd2 = new OrderCommand(receiver, order2);
        //创建调用者 waitor
        Waitor invoker = new Waitor();
        invoker.setCommand(cmd1);
        invoker.setCommand(cmd2);

        //将订单带到柜台 并向厨师喊 订单来了
        invoker.orderUp();
    }
}

2.4 优缺点

1,优点:

  • 降低系统的耦合度。命令模式能将调用操作的对象与实现该操作的对象解耦。
  • 增加或删除命令非常方便。采用命令模式增加与删除命令不会影响其他类,它满足“开闭原则”,对扩展比较灵活。
  • 可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令。
  • 方便实现 Undo 和 Redo 操作。命令模式可以与后面介绍的备忘录模式结合,实现命令的撤销与恢复。

2,缺点:

  • 使用命令模式可能会导致某些系统有过多的具体命令类。
  • 系统结构更加复杂。

2.5使用场景

  • 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。
  • 系统需要在不同的时间指定请求、将请求排队和执行请求。
  • 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。

2.6源码分析

在JDK中,命令模式的经典实现是Runnable接口和java.util.concurrent包中的Executor框架。Runnable接口定义了一个执行动作的协议,而Executor框架则提供了一些实现Runnable接口的类(如ThreadPoolExecutor),以及执行Runnable对象的方法。

以下是一个简单的示例,我们将使用Runnable接口和Executor框架来实现命令模式:

public class CommandPatternJDKExample {

    public static void main(String[] args) {
        // 创建一个命令对象
        Runnable command = new ConcreteCommand();

        // 创建一个执行器对象
        Executor executor = Executors.newSingleThreadExecutor();

        // 执行命令
        executor.execute(command);
    }
}

// 具体命令类
class ConcreteCommand implements Runnable {

    @Override
    public void run() {
        System.out.println("命令被执行了");
    }
}

在上面的示例中,我们创建了一个ConcreteCommand类,它实现了Runnable接口,并且在run()方法中执行了一个简单的命令。然后,我们使用Executors类创建了一个执行器对象,并调用了它的execute()方法来执行这个命令。

三、策略模式实验

任务描述

在多个裁判负责打分的单人跳水比赛中,每位裁判给选手一个得分,选手的最后得分是根据全体裁判的得分计算出来的。现有5名裁判和7人裁判两种评分计算方案,以后可能会有更多计算方案。

  • 5名裁判员评分规则:5名裁判员打出分数以后,先删去最高和最低的无效分,余下3名裁判员的分数之和乘以运动员所跳动作的难度系数,便得出该动作的实得分;
  • 7名裁判员评分规则:方法与5名裁判员评分方法相同,但7人裁判员算出的得分最后还应除以5,再乘以3。

本关任务:请设计一种能兼容多种评分策略的架构,且用户可以自主选择其中一种策略作为比赛的评分方案。

实现方式

  1. 从上下文类中找出修改频率较高的算法 (也可能是用于在运行时选择某个算法变体的复杂条件运算符);

  2. 声明该算法所有变体的通用策略接口;

  3. 将算法逐一抽取到各自的类中, 它们都必须实现策略接口;

  4. 在上下文类中添加一个成员变量用于保存对于策略对象的引用。 然后提供设置器以修改该成员变量。 上下文仅可通过策略接口同策略对象进行交互, 如有需要还可定义一个接口来让策略访问其数据;

  5. 客户端必须将上下文类与相应策略进行关联, 使上下文可以预期的方式完成其主要工作。

编程要求

根据提示,在右侧编辑器 Begin-End 内补充代码:

  • PlayerScore:上下文环境类;
  • Codepoints:抽象策略;
  • Codepoints5:5人裁判评分策略类;
  • Codepoints7:7人裁判评分策略类;
  • Client:客户端类。

测试说明

平台会对你编写的代码进行测试,第一行给出一个正整数 n(表示裁判人数)和一个实数 k(表示跳水难度系数)。第二行给出 n 个实数(评分成绩),中间以空格进行分隔。输出最后结果,格式为 The final score is XX.XX,保留小数点后两位。

测试输入: 5 2.0 5 5.5 5 5 4.5 预期输出: The final score is 30.00

测试输入: 7 2.0 5 5.5 5 5 4.5 5 5 预期输出: The final score is 30.00

抽象策略(Strategy)类

package step1;

/********** Begin *********/
public interface Codepoints{
    double computerScore(double []a,double difficulty);

}
/********** End *********/

 具体策略(Concrete Strategy)类

package step1;

/********** Begin *********/
import java.util.Arrays;

public class Codepoints5 implements Codepoints{
    
    public double computerScore(double[]a,double difficulty){
        if(a.length<=2)
            return 0;
        double score,sum=0;
        Arrays.sort(a);//排序数组
        for (int i=1;i<a.length-1;i++){
            sum=sum+a[i];
        }
        score=sum*difficulty;
        return score;
    }
}

/********** End *********/
package step1;

/********** Begin *********/
import java.util.Arrays;

public class Codepoints7 implements Codepoints{
    
    public double computerScore(double[]a,double difficulty){
        if(a.length<=2)
            return 0;
        double score,sum=0;
        Arrays.sort(a);//排序数组
        for (int i=1;i<a.length-1;i++){
            sum=sum+a[i];
        }
        score=sum/5.0*3*difficulty;
        return score;
    }
}

/********** End *********/

环境(Context)类

package step1;

/********** Begin *********/

public class PlayerScore{
    private Codepoints codepoints;
    public void setStrategy(Codepoints codepoints){
    this.codepoints=codepoints;
    }
    public double Calculate(double []a,double difficulty){
        return codepoints.computerScore(a,difficulty);
    }
}
/********** End *********/

客户端类


package step1;

import java.util.Scanner;

public class Client {
    public static void main(String[] args) {
        /********** Begin *********/
        PlayerScore player=new PlayerScore ();
        Scanner scanner = new Scanner(System.in);
        int n =scanner.nextInt();// 裁判人数
        double diff=scanner.nextDouble();//动作的难度系数
        double[] array=new double [n];// 裁判的评分
        for (int i=0 ; i<n ; i++){
            array[i] = scanner.nextDouble();
        }
        if (n==5){
            player.setStrategy(new Codepoints5());
        }
        else if (n==7) {
            player.setStrategy(new Codepoints7());
        }
       
        System.out.println("The final score is "+ String.format("%.2f", player.Calculate(array,diff)));
        /********** End *********/
    }
}

四、命令模式实验

任务描述

zeren

在植物大战僵尸的游戏设计中,植物战士是兵营生产的,玩家通过点击界面上“图片按钮”向兵营发出生产命令,兵营接受命令后每生产一个植物战士需要冷却一定的时间才能继续生产。

本关任务:本模拟程序中只需要实现一种植物战士的生产,请你用命令模式实现。

实现方式

  1. 声明仅有一个执行方法的命令接口;

  2. 抽取请求并使之成为实现命令接口的具体命令类。 每个类都必须有一组成员变量来保存请求参数和对于实际接收者对象的引用。 所有这些变量的数值都必须通过命令构造函数进行初始化;

  3. 找到担任发送者职责的类。 在这些类中添加保存命令的成员变量。 发送者只能通过命令接口与其命令进行交互。 发送者自身通常并不创建命令对象, 而是通过客户端代码获取;

  4. 修改发送者使其执行命令, 而非直接将请求发送给接收者;

  5. 客户端必须按照以下顺序来初始化对象:

    •  创建接收者;
    •  创建命令, 如有需要可将其关联至接收者;
    •  创建发送者并将其与特定命令关联。

编程要求

根据提示,在右侧编辑器 Begin-End 内补充 “CampInvokers.java”和 “ProduceCommand.java” 文件代码,其它地方不要修改。

测试说明

平台会对你编写的代码进行测试,请输入发出生产命令的个数和兵营冷却时间(毫秒)。 输入结束后,为了模拟界面操作,系统会在 1000 毫秒内持续监控兵营接到的命令。 测试输入:6 200 预期输出: 豌豆射手生产出来了 豌豆射手生产出来了 豌豆射手生产出来了 豌豆射手生产出来了 豌豆射手生产出来了

测试输入:6 500 预期输出: 豌豆射手生产出来了 豌豆射手生产出来了

抽象命令类

package step1;

public abstract class Commands{
    public abstract void Execute();
}

具体命令

package step1;

public class ProduceCommand extends Commands{
    @Override
    public void Execute() {
        /********** Begin *********/
        //生产植物战士
        new Peashooter();
       
        /********** End *********/
    }
}
package step1;
//所有战士的基类
public interface IBotany {
}
package step1;
//植物战士
public class Peashooter implements IBotany {
    public Peashooter()
    {
        System.out.println("豌豆射手生产出来了");
    }
}

接收者(Receiver)角色

调用者

package step1;

import java.util.ArrayList;
import java.util.List;

// 兵营就是命令的管理者
public class CampInvokers {
    private final List<Commands> commands = new ArrayList<>(); // 命令队列
    private final long TrainTimer; // 训练冷却时间
    private long timer; // 当前时间
    
    // 构造函数,初始化训练冷却时间和当前时间
    public CampInvokers(long trainTime){
        TrainTimer=trainTime;
        timer=0;
    }
    
    // 添加训练命令到命令队列
    public void Train()
    {
        // 将生产命令加入到命令队列
        commands.add(new ProduceCommand());
    }
    
    // 执行命令队列中的命令
    public void ExecuteCommand()
    {
        if (commands.size() <= 0) return;
        if (timer+TrainTimer <= System.currentTimeMillis())
        {
            // 执行队列里最上面的命令
            commands.get(0).Execute(); 
            // 移除最上面的命令
            commands.remove(0); 
            // 重置冷却时间
            timer = System.currentTimeMillis(); 
        }
    }
    
    // 取消训练命令
    public void CancelTrainCommand()
    {
        if (commands.size() > 0)
        {
            commands.remove(commands.size() - 1);
            if (commands.size() == 0)
            {
                timer = System.currentTimeMillis();
            }
        }
    }
}

客户端类 

package step1;

import java.util.Scanner;

// 客户端类,用于演示命令模式
public class Client {
    public static void main(String[] args){
        Scanner scanner = new Scanner(System.in);
        
        // 从用户输入获取训练次数和训练冷却时间
        int number = scanner.nextInt();
        long traintime = scanner.nextLong();
        long currtime= System.currentTimeMillis();

        // 创建兵营管理者对象
        CampInvokers camp = new CampInvokers(traintime);
        
        // 循环训练指定次数
        for (int i = 0; i < number; i++) {
            camp.Train();
        }
        
        // 循环执行命令直至时间到达1秒后
        while (true)
        {
            camp.ExecuteCommand();
            if(currtime+1000<System.currentTimeMillis())
                break;
        }
    }
}

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>