设计模式-策略模式 Strategy

策略模式

该模式最常见的应用场景是,利用它来避免冗长的 if-else 或 switch 分支判断。不过,它的作用还不止如此。它也可以像模板模式那样,提供框架的扩展点等等。

1) 原理和实现

策略模式,英文全称是 Strategy Design Pattern。该模式是这样定义的:

Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it。

翻译成中文就是:定义一族算法类,将每个算法分别封装起来,让它们可以互相替换。策略模式可以使算法的变化独立于使用它们的客户端(这里的客户端代指使用算法的代码)。

策略模式主要包含以下角色:

  1. 策略接口(Strategy):定义所有支持的算法的公共接口。客户端使用这个接口与具体策略进行交互。
  2. 具体策略(Concrete Strategy):实现策略接口的具体策略类。这些类封装了实际的算法逻辑。
  3. 上下文(Context):持有一个策略对象,用于与客户端进行交互。上下文可以定义一些接口,让客户端不直接与策略接口交互,从而实现策略的封装。

让我们以一个简单的例子来说明策略模式:假设我们要实现一个计算器,支持加法、减法和乘法运算 。我们可以使用策略模式将各种运算独立为不同的策略,并让客户端根据需要选择和使用不同的策略。

首先,定义一个策略接口Operation

java 复制代码
public interface Operation {
    double execute(double num1, double num2);
}

接下来,创建具体策略类来实现加法、减法和乘法运算:

java 复制代码
public class Addition implements Operation {
    @Override
    public double execute(double num1, double num2) {
        return num1 + num2;
    }
}

public class Subtraction implements Operation {
    @Override
    public double execute(double num1, double num2) {
        return num1 - num2;
    }
}

public class Multiplication implements Operation {
    @Override
    public double execute(double num1, double num2) {
        return num1 * num2;
    }
}

然后,创建一个上下文类Calculator,让客户端可以使用这个类来执行不同的运算:

java 复制代码
public class Calculator {
    // 客户端可以持有一个算法
    private Operation operation;

    public void setOperation(Operation operation) {
        this.operation = operation;
    }

    public double calculate(double num1, double num2) {
        return operation.execute(num1, num2);
    }
}

现在,客户端可以使用Calculator类来执行不同的运算,例如:

java 复制代码
public class Client {
    public static void main(String[] args) {
        // 定义一个调用方
        Calculator calculator = new Calculator();

        // 计算加法
        Operation operation = new Addition();
        calculator.setOperation(operation);
        double calculate = calculator.calculate(10, 12.9);
        System.out.println("calculate = " + calculate);

        // 计算减法
        operation = new Subtraction();
        calculator.setOperation(operation);
        calculate = calculator.calculate(10, 12.9);
        System.out.println("calculate = " + calculate);
    }
}

在这个例子中,我们使用策略模式将加法、减法和乘法运算独立为不同的策略。客户端可以根据需要选择和使用不同的策略。Calculator上下文类持有一个Operation策略对象,并通过setOperation方法允许客户端设置所需的策略。这种方式使得算法的选择和执行更加灵活,易于扩展和维护。

策略模式的优点包括:

  1. 提高代码的可维护性和可扩展性。当需要添加新的算法时,我们只需要实现一个新的具体策略类,而无需修改客户端代码。
  2. 符合开闭原则。策略模式允许我们在不修改现有代码的情况下引入新的策略。
  3. 避免使用多重条件判断。使用策略模式可以消除一些复杂的条件判断语句,使代码更加清晰和易于理解。

策略模式的缺点包括:

  1. 客户端需要了解所有的策略。为了选择合适的策略,客户端需要了解不同策略之间的区别。
  2. 增加了类的数量。策略模式会导致程序中具体策略类的数量增加,这可能会导致代码的复杂性增加。

在实际开发中,我们可以根据业务需求和系统架构灵活地运用策略模式。例如,在电商系统中,我们可以使用策略模式处理不同的促销策略;在游戏系统中,我们可以使用策略模式处理不同的角色行为等。

1、策略的定义

策略类的定义比较简单,包含一个策略接口和一组实现这个接口的策略类。因为所有的策略类都实现相同的接口,所以,客户端代码基于接口而非实现编程,可以灵活地替换不同的策略。示例代码如下所示:

java 复制代码
public interface Strategy {
    void algorithmInterface();
}
public class ConcreteStrategyA implements Strategy {
    @Override
    public void  algorithmInterface() {
        //具体的算法...
    }
}
public class ConcreteStrategyB implements Strategy {
    @Override
    public void  algorithmInterface() {
        //具体的算法...
    }
}

2、策略的创建

因为策略模式会包含一组策略 ,在使用它们的时候,一般会通过类型(type)来判断创建哪个策略来使用。为了封装创建逻辑,我们需要对客户端代码屏蔽创建细节。

事实上我们可以做一定的优化,可以把根据 type 创建策略的逻辑抽离出来,放到工厂类中。示例代码如下所示:

java 复制代码
public class StrategyFactory {
    private static final Map<String, Strategy> strategies = new HashMap<>();
    static {
        strategies.put("A", new ConcreteStrategyA());
        strategies.put("B", new ConcreteStrategyB());
    }
    public static Strategy getStrategy(String type) {
        if (type == null || type.isEmpty()) {
            throw new IllegalArgumentException("type should not be empty.");
        }
        return strategies.get(type);
    }
}

一般来讲,如果策略类是无状态的,不包含成员变量,只是纯粹的算法实现 ,这样的策略对象是可以被共享使用的,不需要在每次调用 getStrategy() 的时候,都创建一个新的策略对象。针对这种情况,我们可以使用上面这种工厂类的实现方式,事先创建好每个策略对象,缓存到工厂类中,用的时候直接返回。

相反,如果策略类是有状态的,根据业务场景的需要,我们希望每次从工厂方法中,获得的都是新创建的策略对象,而不是缓存好可共享的策略对象,那我们就需要按照如下方式来实现策略工厂类。

java 复制代码
public class StrategyFactory {
    public static Strategy getStrategy(String type) {
        if (type == null || type.isEmpty()) {
            throw new IllegalArgumentException("type should not be empty.");
        }
        if (type.equals("A")) {
            return new ConcreteStrategyA();
        } else if (type.equals("B")) {
            return new ConcreteStrategyB();
        }
        return null;
    }
}

3、策略的使用

刚刚讲了策略的定义和创建,现在,我们再来看一下,策略的使用。

我们知道,策略模式包含一组可选策略,客户端代码一般如何确定使用哪个策略呢?最常见的是运行时动态确定使用哪种策略,这也是策略模式最典型的应用场景。

这里的"运行时动态"指的是,我们事先并不知道会使用哪个策略,而是在程序运行期间,根据配置、用户输入、计算结果等这些不确定因素,动态决定使用哪种策略。接下来,我们通过一个例子来解释一下。

java 复制代码
// 策略接口:EvictionStrategy
// 策略类:LruEvictionStrategy、FifoEvictionStrategy、LfuEvictionStrategy...
// 策略工厂:EvictionStrategyFactory
public class UserCache {
    private Map<String, User> cacheData = new HashMap<>();
    private EvictionStrategy eviction;
    public UserCache(EvictionStrategy eviction) {
        this.eviction = eviction;
    }
    //...
}
// 运行时动态确定,根据配置文件的配置决定使用哪种策略
public class Application {
    public static void main(String[] args) throws Exception {
        EvictionStrategy evictionStrategy = null;
        Properties props = new Properties();
        props.load(new FileInputStream("./config.properties"));
        String type = props.getProperty("eviction_type");
        evictionStrategy = EvictionStrategyFactory.getEvictionStrategy(type);
        UserCache userCache = new UserCache(evictionStrategy);
        //...
    }
}
// 非运行时动态确定,在代码中指定使用哪种策略
public class Application {
    public static void main(String[] args) {
        //...
        EvictionStrategy evictionStrategy = new LruEvictionStrategy();
        UserCache userCache = new UserCache(evictionStrategy);
        //...
    }
}

从上面的代码中,我们也可以看出,"非运行时动态确定",也就是第二个 Application 中的使用方式,并不能发挥策略模式的优势。在这种应用场景下,策略模式实际上退化成了"面向对象的多态特性"或"基于接口而非实现编程原则",这其实就是开篇时列举的例子的场景。

相关推荐
魔道不误砍柴功1 小时前
简单叙述 Spring Boot 启动过程
java·数据库·spring boot
失落的香蕉1 小时前
C语言串讲-2之指针和结构体
java·c语言·开发语言
枫叶_v1 小时前
【SpringBoot】22 Txt、Csv文件的读取和写入
java·spring boot·后端
wclass-zhengge1 小时前
SpringCloud篇(配置中心 - Nacos)
java·spring·spring cloud
路在脚下@1 小时前
Springboot 的Servlet Web 应用、响应式 Web 应用(Reactive)以及非 Web 应用(None)的特点和适用场景
java·spring boot·servlet
黑马师兄1 小时前
SpringBoot
java·spring
数据小小爬虫2 小时前
如何用Java爬虫“偷窥”淘宝商品类目API的返回值
java·爬虫·php
暮春二十四2 小时前
关于用postman调用接口成功但是使用Java代码调用却失败的问题
java·测试工具·postman
java小吕布2 小时前
Java中Properties的使用详解
java·开发语言·后端
爱吃土豆的程序员2 小时前
在oracle官网下载资源显示400 Bad Request Request Header Or Cookie Too Large 解决办法
java·数据库·oracle·cookie