Bootstrap

【设计模式】【行为型模式】策略模式(Strategy)

👋hi,我不是一名外包公司的员工,也不会偷吃茶水间的零食,我的梦想是能写高端CRUD
🔥 2025本人正在沉淀中… 博客更新速度++
📫 欢迎+V: flzjcsg2,我们共同讨论Java深渊的奥秘
🎵 当你的天空突然下了大雨,那是我在为你炸乌云

一、入门

什么是策略模式?

策略模式是一种行为设计模式,允许在运行时选择算法或行为。它将算法封装在独立的类中,使得它们可以互换,而不影响客户端代码。

为什么需要策略模式?

策略模式的主要目的是解决算法或行为在代码中硬编码的问题,使得系统更加灵活、可扩展和易于维护。可以优化大量的if-else。

怎样实现策略模式?

策略模式的主要角色如下:
抽象策略(Strategy)类:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略所需的接口。
具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现或行为
环境(Context)类:持有一个策略类的引用,最终给客户端调用。

【案例】促销活动
百货公司,在不同的节日,会有不同的促销
在这里插入图片描述

定义百货公司所有促销活动的共同接口

public interface Strategy {
  void show();
}

定义具体策略角色(Concrete Strategy):每个节日具体的促销活动

//为春节准备的促销活动A
public class StrategyA implements Strategy {
  public void show() {
    System.out.println("买一送一");
 }
}
//为中秋准备的促销活动B
public class StrategyB implements Strategy {
  public void show() {
    System.out.println("满200元减50元");
 }
}
//为圣诞准备的促销活动C
public class StrategyC implements Strategy {
  public void show() {
    System.out.println("满1000元加一元换购任意200元以下商品");
 }
}
定义环境角色(Context):用于连接上下文,即把促销活动推销给客户,这里可以理解为销售员
public class SalesMan {            
  //持有抽象策略角色的引用               
  private Strategy strategy;        
                      
  public SalesMan(Strategy strategy) {   
    this.strategy = strategy;       
 }                     
                      
  //向客户展示促销活动                
  public void salesManShow(){        
    strategy.show();           
 }                     
} 

二、策略模式在源码中的运用

2.1、Java Collections 中的排序策略

Java的Collections.sort()方法使用了策略模式来实现排序功能。它允许通过传递不同的Comparator实现来定义不同的排序策略。

List<Integer> numbers = Arrays.asList(3, 1, 4, 1, 5, 9);

// 策略1:升序排序
Collections.sort(numbers, new Comparator<Integer>() {
    @Override
    public int compare(Integer a, Integer b) {
        return a.compareTo(b);
    }
});
System.out.println("升序排序: " + numbers);

// 策略2:降序排序
Collections.sort(numbers, new Comparator<Integer>() {
    @Override
    public int compare(Integer a, Integer b) {
        return b.compareTo(a);
    }
});
System.out.println("降序排序: " + numbers);

在上面的代码中Comparator是策略接口。具体的排序逻辑(升序、降序)是策略实现。Collections.sort()是上下文,负责调用策略。
源码中,Collections类的sort方法,里面调用了Arrays类的sort方法。ArraysTimSort类的sort方法

// Collections类
public static <T> void sort(List<T> list, Comparator<? super T> c) {
    list.sort(c);
}

default void sort(Comparator<? super E> c) {
    Object[] a = this.toArray();
    Arrays.sort(a, (Comparator) c);    // 调用Arrays类的sort方法
    ListIterator<E> i = this.listIterator();
    for (Object e : a) {
        i.next();
        i.set((E) e);
    }
}

// Arrays类
public static <T> void sort(T[] a, Comparator<? super T> c) {
    if (c == null) {
        sort(a);
    } else {
        if (LegacyMergeSort.userRequested)
            legacyMergeSort(a, c);
        else
            TimSort.sort(a, 0, a.length, c, null, 0, 0); // 调用TimSort类的sort方法
    }
}

TimSort类中,这里我们只要关注,我们传的策略,入参c的使用地方就好了。这里会调用countRunAndMakeAscending方法,我们关注这个方法,我们传的排序策略,也就是入参c,会被使用。

static <T> void sort(T[] a, int lo, int hi, Comparator<? super T> c,
                     T[] work, int workBase, int workLen) {
    assert c != null && a != null && lo >= 0 && lo <= hi && hi <= a.length;

    int nRemaining  = hi - lo;
    if (nRemaining < 2)
        return;  // Arrays of size 0 and 1 are always sorted

    // If array is small, do a "mini-TimSort" with no merges
    if (nRemaining < MIN_MERGE) {
        int initRunLen = countRunAndMakeAscending(a, lo, hi, c);  // 关注调用这个方法
...


// countRunAndMakeAscending 方法
private static <T> int countRunAndMakeAscending(T[] a, int lo, int hi,
                                                Comparator<? super T> c) {
    assert lo < hi;
    int runHi = lo + 1;
    if (runHi == hi)
        return 1;

    if (c.compare(a[runHi++], a[lo]) < 0) { // 排序策略被使用
        while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) < 0)
            runHi++;
        reverseRange(a, lo, runHi);
    } else {                          
        while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) >= 0)
            runHi++;
    }

    return runHi - lo;
}

2.2、Spring 中的资源加载策略

Spring框架中的ResourceLoader接口和其实现类(如ClassPathResourceLoaderFileSystemResourceLoader等)也使用了策略模式。

ResourceLoader resourceLoader = new DefaultResourceLoader();

// 策略1:从类路径加载资源
Resource classPathResource = resourceLoader.getResource("classpath:application.properties");
System.out.println("类路径资源: " + classPathResource.exists());

// 策略2:从文件系统加载资源
Resource fileSystemResource = resourceLoader.getResource("file:/path/to/file.txt");
System.out.println("文件系统资源: " + fileSystemResource.exists());

ResourceLoader是策略接口。具体的资源加载逻辑(类路径、文件系统等)是策略实现。DefaultResourceLoader是上下文,负责调用策略。
下面是结合源码说明
策略接口:ResourceLoader 接口

public interface ResourceLoader {
    Resource getResource(String location);
}

具体策略:ClassPathResource、FileSystemResource等是具体的策略实现。

// ClassPathResource实现
public class ClassPathResource extends AbstractFileResolvingResource {
    private final String path;
    public ClassPathResource(String path) {
        this.path = path;
    }
    @Override
    public InputStream getInputStream() throws IOException {
        InputStream is = getClassLoader().getResourceAsStream(path);
        if (is == null) {
            throw new FileNotFoundException("Resource not found: " + path);
        }
        return is;
    }
}

// FileSystemResource
public class FileSystemResource extends AbstractResource {
    private final File file;
    public FileSystemResource(String path) {
        this.file = new File(path);
    }
    @Override
    public InputStream getInputStream() throws IOException {
        return new FileInputStream(file);
    }
}

上下文:DefaultResourceLoader 是上下文,负责根据路径选择合适的策略。

public class DefaultResourceLoader implements ResourceLoader {
    @Override
    public Resource getResource(String location) {
        // 根据路径前缀选择策略
        if (location.startsWith(CLASSPATH_URL_PREFIX)) {
            return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()));
        }
        else if (location.startsWith(FILE_URL_PREFIX)) {
            return new FileSystemResource(location.substring(FILE_URL_PREFIX.length()));
        }
        else {
            try {
                // 尝试作为URL加载
                URL url = new URL(location);
                return new UrlResource(url);
            }
            catch (MalformedURLException ex) {
                // 如果都不是,默认为文件系统资源
                return new FileSystemResource(location);
            }
        }
    }
}

这里应该有UU们会好奇,诶,为什么具体策略没有实现策略接口?Spring 的资源加载策略是策略模式的一种变体。它与经典策略模式的区别在于:

  • 经典策略模式:
    • 策略接口和具体策略是直接实现的。
    • 例如,排序策略中,Comparator 是策略接口,具体的排序类是策略实现。
  • Spring 的资源加载策略:
    • 策略接口(ResourceLoader)和具体策略(ClassPathResource 等)之间通过上下文(DefaultResourceLoader)连接。
    • 具体策略实现的是 Resource 接口,而不是 ResourceLoader 接口。

三、总结

策略模式通过将算法或行为封装到独立的类中,提供了一种灵活、可扩展的方式来管理代码中的变化部分。它的核心优势是解耦动态切换,但也会带来类的数量增加和客户端使用成本的问题。适用于需要动态切换行为、避免重复代码或隔离算法实现细节的场景。

优点
灵活性:允许在运行时动态切换算法或行为,无需修改客户端代码。
可扩展性:新增策略时只需添加新的策略类,符合开闭原则(对扩展开放,对修改关闭)。
解耦:将算法或行为与使用它的上下文分离,降低了代码的耦合度。
避免重复代码:将相似的算法提取到独立的策略类中,减少代码重复。
易于测试:每个策略类可以独立测试,简化了测试过程。

缺点
增加类的数量:每个策略都需要一个独立的类,可能会导致类的数量增多,增加系统复杂性。
客户端需要了解策略:客户端必须知道有哪些策略,并选择合适的策略,增加了使用成本。
性能开销:在运行时切换策略可能会引入额外的性能开销(如对象创建和销毁)

适用场景
需要动态切换算法或行为:例如,支付方式、排序算法、资源加载策略等。
有多个相似的类,只有行为不同:例如,不同类型的折扣计算、不同的日志记录方式等。
避免使用复杂的条件语句:当代码中有大量if-else或switch-case语句时,可以用策略模式替代。
需要隔离算法的实现细节:当不希望暴露算法的实现细节,或者希望算法可以独立变化时。
需要对算法进行扩展:当系统需要支持新的算法,且不希望修改现有代码时。

参考

黑马程序员Java设计模式详解, 23种Java设计模式(图解+框架源码分析+实战)_哔哩哔哩_bilibili

;