概述
在软件工程领域,变化是唯一不变的主题。当业务逻辑中充斥着大量if-else或switch-case分支,而每个分支又代表着一组特定的算法或行为时,代码往往会变得臃肿、难以维护且违反开闭原则。策略模式(Strategy Pattern)正是应对此类场景的经典良方。其核心意图由GoF清晰定义:定义一系列算法,把它们一个个封装起来,并使它们可以相互替换。本模式使得算法的变化可独立于使用它的客户端。 简而言之,策略模式将“做什么”与“怎么做”解耦,赋予系统在运行时动态选择算法的能力。
策略模式解决的核心问题集中体现在三个方面:第一,消除复杂的条件分支语句,提升代码可读性和可维护性;第二,实现算法的动态替换,无需重启应用即可切换业务逻辑;第三,严格遵循开闭原则,新增一种算法只需增加一个具体策略类,而无需修改上下文和已有策略。本文将引领你从最原始的if-else硬编码算法出发,逐步重构至经典的策略模式实现,进而深入剖析JDK、Spring、MyBatis、Dubbo等主流框架源码中对策略模式的精妙运用,最终延伸到分布式环境下的负载均衡、ID生成、灰度发布等高级场景。通过五个典型业务场景的完整Demo与图示,辅以十余道专家级面试题解析,本文旨在为Java开发者构建一幅从基础到架构的策略模式全景图谱。
一、模式定义与结构
1.1 GoF标准定义
The Strategy Pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
策略模式属于对象行为型模式,它强调的是算法的封装与替换。
1.2 UML类图
classDiagram
class Context {
- strategy: Strategy
+ Context(Strategy strategy)
+ setStrategy(Strategy strategy)
+ executeStrategy(): void
}
class Strategy {
<<interface>>
+ algorithmInterface(): void
}
class ConcreteStrategyA {
+ algorithmInterface(): void
}
class ConcreteStrategyB {
+ algorithmInterface(): void
}
class ConcreteStrategyC {
+ algorithmInterface(): void
}
Context --> Strategy : holds
ConcreteStrategyA ..|> Strategy
ConcreteStrategyB ..|> Strategy
ConcreteStrategyC ..|> Strategy
1.3 角色职责与结构详解
上述类图清晰地勾勒出策略模式的三大核心角色:Strategy(抽象策略)、ConcreteStrategy(具体策略) 以及Context(上下文)。
-
Strategy接口:它声明了所有支持的算法的公共接口。在图中,
Strategy定义了一个algorithmInterface()方法。该接口将算法的调用规范统一化,使得Context可以无差别地对待任何具体策略。它是实现算法可替换性的基石。 -
ConcreteStrategy具体策略类:图中
ConcreteStrategyA、ConcreteStrategyB、ConcreteStrategyC分别实现了Strategy接口。每个类封装了一个具体的算法实现。例如,在会员折扣系统中,它们可以分别代表“普通会员折扣”、“黄金会员折扣”和“钻石会员折扣”。这些类互相独立,新增一种折扣算法不会影响其他已有策略。 -
Context上下文类:
Context扮演着承上启下的角色。它持有一个对Strategy对象的引用(图中-strategy: Strategy),并在构造时或通过setStrategy()方法接收一个具体策略实例。Context本身并不实现算法逻辑,它的核心方法是executeStrategy(),该方法内部仅仅是对所持有策略对象的algorithmInterface()方法的委托调用。这种设计巧妙地将算法的选择与使用分离:客户端决定使用哪个策略,而Context只负责执行被选中的策略。
这种结构使得客户端可以灵活地在运行时替换Context内部的策略对象。例如,用户从普通会员升级为黄金会员时,客户端只需调用context.setStrategy(new GoldMemberStrategy()),后续的价格计算行为便会自动切换。整个过程无需修改Context类的任何代码,完美诠释了“对扩展开放,对修改关闭”的设计原则。
二、代码演进与实现
本节将通过一个“会员折扣计算”的业务场景,演示代码从反模式到优雅设计模式的演进过程。
2.1 不使用模式的原始代码(反模式案例)
首先,我们来看一段典型的、未经设计的代码。它通过if-else判断会员类型来执行不同的折扣计算。
/**
* 反模式:不使用策略模式的折扣计算器
* 缺点:
* 1. 违反开闭原则:每增加一种会员等级(如新增"钻石会员"),都需要修改calculatePrice方法,增加新的if-else分支。
* 2. 算法与客户端紧耦合:折扣算法与PriceCalculator类紧密绑定,无法在不修改代码的情况下替换算法。
* 3. 可维护性差:随着条件分支增多,方法体会变得臃肿不堪,可读性和可测试性急剧下降。
*/
public class PriceCalculatorV1 {
public static final int TYPE_NORMAL = 1;
public static final int TYPE_GOLD = 2;
public static final int TYPE_DIAMOND = 3;
/**
* 根据会员类型计算折扣后价格
* @param originalPrice 原价
* @param memberType 会员类型
* @return 折扣后价格
*/
public double calculatePrice(double originalPrice, int memberType) {
if (memberType == TYPE_NORMAL) {
// 普通会员无折扣
System.out.println("普通会员:无折扣");
return originalPrice;
} else if (memberType == TYPE_GOLD) {
// 黄金会员8折
System.out.println("黄金会员:享受8折优惠");
return originalPrice * 0.8;
} else if (memberType == TYPE_DIAMOND) {
// 钻石会员6折
System.out.println("钻石会员:享受6折优惠");
return originalPrice * 0.6;
} else {
throw new IllegalArgumentException("不支持的会员类型");
}
}
public static void main(String[] args) {
PriceCalculatorV1 calculator = new PriceCalculatorV1();
double price = 1000.0;
System.out.println("原价:" + price);
System.out.println("折后价:" + calculator.calculatePrice(price, TYPE_NORMAL));
System.out.println("折后价:" + calculator.calculatePrice(price, TYPE_GOLD));
System.out.println("折后价:" + calculator.calculatePrice(price, TYPE_DIAMOND));
}
}
2.2 经典策略模式重构
现在我们运用策略模式对上述代码进行重构,彻底解决条件分支和扩展性问题。
Step 1: 定义Strategy接口
/**
* 策略接口:定义折扣算法的规范
* 所有具体折扣策略都必须实现此接口
*/
public interface DiscountStrategy {
/**
* 计算折扣后的价格
* @param originalPrice 商品原价
* @return 折扣后价格
*/
double calculateDiscount(double originalPrice);
}
Step 2: 实现多个具体策略类
/**
* 具体策略:普通会员折扣策略
*/
public class NormalMemberStrategy implements DiscountStrategy {
@Override
public double calculateDiscount(double originalPrice) {
System.out.println("普通会员:无折扣");
return originalPrice; // 无折扣
}
}
/**
* 具体策略:黄金会员折扣策略
*/
public class GoldMemberStrategy implements DiscountStrategy {
@Override
public double calculateDiscount(double originalPrice) {
System.out.println("黄金会员:享受8折优惠");
return originalPrice * 0.8;
}
}
/**
* 具体策略:钻石会员折扣策略
*/
public class DiamondMemberStrategy implements DiscountStrategy {
@Override
public double calculateDiscount(double originalPrice) {
System.out.println("钻石会员:享受6折优惠");
return originalPrice * 0.6;
}
}
Step 3: 定义Context上下文类
/**
* 上下文类:价格计算器,负责维护当前使用的折扣策略,并将计算请求委托给它
*/
public class PriceContext {
// 持有策略对象的引用
private DiscountStrategy discountStrategy;
/**
* 构造方法:允许在创建上下文时直接注入策略
* @param discountStrategy 具体的折扣策略对象
*/
public PriceContext(DiscountStrategy discountStrategy) {
this.discountStrategy = discountStrategy;
}
/**
* 设置策略方法:允许在运行时动态切换算法
* @param discountStrategy 新的折扣策略对象
*/
public void setDiscountStrategy(DiscountStrategy discountStrategy) {
this.discountStrategy = discountStrategy;
}
/**
* 执行计算:将计算请求委托给当前持有的策略对象
* 这是策略模式的核心:Context本身不包含任何计算逻辑,它只是一个"壳"或"代理"
* @param originalPrice 原价
* @return 折扣后价格
*/
public double executeCalculation(double originalPrice) {
if (discountStrategy == null) {
throw new IllegalStateException("尚未设置折扣策略");
}
// 关键点:调用策略接口方法,而非自己实现算法
return discountStrategy.calculateDiscount(originalPrice);
}
}
Step 4: 客户端调用演示
/**
* 客户端:演示策略模式的动态替换特性
*/
public class StrategyPatternDemo {
public static void main(String[] args) {
double originalPrice = 1000.0;
System.out.println("商品原价:" + originalPrice);
// 1. 创建上下文对象,并初始注入普通会员策略
PriceContext context = new PriceContext(new NormalMemberStrategy());
System.out.println("普通会员折后价:" + context.executeCalculation(originalPrice));
// 2. 运行时切换到黄金会员策略
context.setDiscountStrategy(new GoldMemberStrategy());
System.out.println("黄金会员折后价:" + context.executeCalculation(originalPrice));
// 3. 运行时切换到钻石会员策略
context.setDiscountStrategy(new DiamondMemberStrategy());
System.out.println("钻石会员折后价:" + context.executeCalculation(originalPrice));
}
}
2.3 策略模式的进阶特性
a) 策略枚举:将策略实例限定为有限集合
当策略数量固定且较少时,可以使用枚举来优雅地实现策略模式。枚举天然是单例的,且类型安全。
/**
* 使用枚举实现策略模式
*/
public enum MemberDiscountEnum {
// 枚举实例分别实现calculate方法
NORMAL {
@Override
double calculate(double price) {
System.out.println("普通会员:无折扣");
return price;
}
},
GOLD {
@Override
double calculate(double price) {
System.out.println("黄金会员:8折");
return price * 0.8;
}
},
DIAMOND {
@Override
double calculate(double price) {
System.out.println("钻石会员:6折");
return price * 0.6;
}
};
// 抽象方法,每个枚举实例必须实现
abstract double calculate(double price);
public static void main(String[] args) {
double price = 1000.0;
System.out.println(MemberDiscountEnum.NORMAL.calculate(price));
System.out.println(MemberDiscountEnum.GOLD.calculate(price));
System.out.println(MemberDiscountEnum.DIAMOND.calculate(price));
}
}
b) 函数式策略:利用Java 8 Lambda简化策略实现
对于只有一个抽象方法的策略接口(函数式接口),我们可以直接使用Lambda表达式替代具体的策略类,极大地减少样板代码。
import java.util.function.DoubleUnaryOperator;
/**
* 函数式策略模式:使用Java 8内置的函数式接口或自定义@FunctionalInterface
*/
public class FunctionalStrategyDemo {
// 策略上下文,接受一个函数式接口 DoubleUnaryOperator
static class PriceContext {
private DoubleUnaryOperator discountFunction;
public PriceContext(DoubleUnaryOperator discountFunction) {
this.discountFunction = discountFunction;
}
public void setDiscountFunction(DoubleUnaryOperator discountFunction) {
this.discountFunction = discountFunction;
}
public double execute(double price) {
return discountFunction.applyAsDouble(price);
}
}
public static void main(String[] args) {
double price = 1000.0;
// 直接使用Lambda表达式定义具体策略,无需编写NormalMemberStrategy等类
PriceContext context = new PriceContext(p -> {
System.out.println("普通会员无折扣");
return p;
});
System.out.println(context.execute(price));
// 动态切换为黄金会员策略
context.setDiscountFunction(p -> {
System.out.println("黄金会员8折");
return p * 0.8;
});
System.out.println(context.execute(price));
// 结合方法引用
context.setDiscountFunction(FunctionalStrategyDemo::diamondDiscount);
System.out.println(context.execute(price));
}
private static double diamondDiscount(double price) {
System.out.println("钻石会员6折");
return price * 0.6;
}
}
c) 策略工厂:根据条件自动选择并创建策略对象
在实际项目中,我们往往需要根据输入参数(如会员等级字符串)自动获取对应的策略对象。这时可以将策略模式与工厂模式结合。
import java.util.HashMap;
import java.util.Map;
/**
* 策略工厂:负责根据条件创建/获取策略实例
*/
public class DiscountStrategyFactory {
// 缓存策略实例,避免重复创建
private static final Map<String, DiscountStrategy> STRATEGY_MAP = new HashMap<>();
static {
STRATEGY_MAP.put("normal", new NormalMemberStrategy());
STRATEGY_MAP.put("gold", new GoldMemberStrategy());
STRATEGY_MAP.put("diamond", new DiamondMemberStrategy());
}
/**
* 根据会员等级获取对应的折扣策略
* @param level 会员等级字符串
* @return 对应的策略对象,若不存在则返回普通会员策略作为默认
*/
public static DiscountStrategy getStrategy(String level) {
return STRATEGY_MAP.getOrDefault(level.toLowerCase(), new NormalMemberStrategy());
}
// 或者,如果策略是无状态的,也可以直接返回单例对象
public static DiscountStrategy getSingletonStrategy(String level) {
return STRATEGY_MAP.get(level.toLowerCase());
}
public static void main(String[] args) {
PriceContext context = new PriceContext(null);
// 根据外部输入动态获取策略
String userLevel = "gold";
context.setDiscountStrategy(DiscountStrategyFactory.getStrategy(userLevel));
System.out.println(context.executeCalculation(1500));
}
}
2.4 策略模式执行时序图
sequenceDiagram
participant Client
participant Context
participant Strategy
participant ConcreteStrategyA
Client->>ConcreteStrategyA: new ConcreteStrategyA()
activate ConcreteStrategyA
Client->>Context: new Context(strategy)
activate Context
Context->>Context: set strategy attribute
deactivate Context
Client->>Context: executeCalculation()
activate Context
Context->>Strategy: calculateDiscount(price)
activate Strategy
note right of Strategy: 多态调用实际执行的是<br/>ConcreteStrategyA的方法
Strategy->>ConcreteStrategyA: calculateDiscount(price)
activate ConcreteStrategyA
ConcreteStrategyA-->>Strategy: return discountedPrice
deactivate ConcreteStrategyA
Strategy-->>Context: return discountedPrice
deactivate Strategy
Context-->>Client: return discountedPrice
deactivate Context
时序图解读:
该时序图精确描绘了策略模式的执行流程。首先,Client(客户端)主动创建具体的策略对象ConcreteStrategyA。随后,Client将创建好的策略对象作为参数传递给Context的构造函数,Context内部将保存该策略对象的引用。当Client调用Context的executeCalculation()方法时,Context并没有自行计算,而是立即将请求委托给所持有的Strategy接口引用。由于Java的多态特性,实际执行的是ConcreteStrategyA中重写的calculateDiscount()方法。最终,折扣后的价格原路返回给Client。此流程展示了算法选择权在客户端,算法执行权在策略对象,而上下文仅作为委托代理的核心思想。通过setStrategy()方法,客户端可以在任意时刻替换Context内部的策略引用,从而实现运行时算法的无缝切换。
三、源码级应用分析
策略模式在Java生态及主流开源框架中有着极其广泛的应用。理解这些源码中的设计,有助于我们掌握该模式的精髓。
3.1 JDK中的策略模式
1. java.util.Comparator
Comparator接口是策略模式的教科书级范例。它定义了一个比较算法compare(T o1, T o2)。Collections.sort()或Arrays.sort()方法作为Context,接收一个Comparator策略,并在排序过程中委托它来决定元素的顺序。
// Strategy接口
List<String> names = Arrays.asList("John", "Alice", "Bob");
// 使用不同的Comparator策略实现不同的排序算法
Collections.sort(names, (s1, s2) -> s1.compareTo(s2)); // 字典序策略
Collections.sort(names, (s1, s2) -> s1.length() - s2.length()); // 长度排序策略
2. java.util.concurrent.ThreadPoolExecutor的拒绝策略
RejectedExecutionHandler是线程池的拒绝策略接口。当线程池无法处理新提交的任务时,将根据配置的具体策略(AbortPolicy抛出异常、CallerRunsPolicy调用者运行、DiscardPolicy丢弃、DiscardOldestPolicy丢弃最旧任务)来处理。
ThreadPoolExecutor executor = new ThreadPoolExecutor(
1, 1, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(1),
new ThreadPoolExecutor.CallerRunsPolicy() // 注入具体的拒绝策略
);
3. java.awt.LayoutManager
在Swing/AWT图形界面编程中,LayoutManager接口定义了容器内组件的布局算法。BorderLayout、FlowLayout、GridLayout等都是具体的布局策略。Container作为上下文,持有一个LayoutManager引用,并在doLayout()时委托给它。
4. java.nio.charset.Charset
Charset类本身虽然不是典型的接口,但其内部的CharsetEncoder和CharsetDecoder实现了编码与解码策略的抽象。Charset.forName("UTF-8")获取的是一个具体的字符集编码策略对象。
3.2 Spring框架中的策略模式
Spring框架将策略模式运用到了极致,尤其在资源管理、事务处理、视图解析等方面。
1. Resource与ResourceLoader
Resource接口是Spring对底层资源访问的统一抽象,ClassPathResource、FileSystemResource、UrlResource分别代表不同的资源访问策略。ResourceLoader(上下文)提供了getResource(String location)方法,根据location前缀(如classpath:、file:、http:)自动选择并返回对应的Resource策略实现。
2. PlatformTransactionManager
这是Spring事务管理的核心策略接口。DataSourceTransactionManager(针对JDBC)、JtaTransactionManager(针对分布式JTA)、HibernateTransactionManager(针对Hibernate)都是其具体策略实现。TransactionTemplate或@Transactional注解背后的拦截器充当上下文,在运行时调用PlatformTransactionManager的提交、回滚等策略方法。
3. ViewResolver
Spring MVC中的视图解析器是策略模式的又一经典应用。InternalResourceViewResolver(解析JSP)、FreeMarkerViewResolver(解析FreeMarker模板)、ThymeleafViewResolver(解析Thymeleaf模板)等实现了ViewResolver接口。DispatcherServlet作为上下文,遍历所有的ViewResolver策略,直到有一个能够成功解析逻辑视图名为止。
4. CacheManager与Cache
Spring Cache抽象中,CacheManager是管理缓存策略的接口,EhCacheCacheManager、RedisCacheManager、CaffeineCacheManager是其具体实现。开发者通过配置注入不同的CacheManager策略,即可在运行时切换底层缓存技术(从Ehcache切换到Redis),而业务代码完全无感知。
3.3 MyBatis框架中的策略模式
1. Executor执行器策略
Executor是MyBatis执行SQL语句的核心接口,包含SimpleExecutor(默认,每次执行都开启新的Statement)、ReuseExecutor(重用Statement)、BatchExecutor(批量执行)。在创建SqlSession时,通过configuration.newExecutor(...)根据配置选择具体的执行器策略。
2. Cache缓存策略
MyBatis的二级缓存采用了装饰器模式,但其基础的缓存策略接口Cache下也有多种实现:PerpetualCache(基于HashMap的永久缓存)、LruCache(LRU淘汰策略)、FifoCache(FIFO淘汰策略)、BlockingCache(阻塞式缓存)。这些策略可自由组合。
3. ObjectWrapperFactory
ObjectWrapperFactory允许自定义对象包装策略,用于处理对象属性的设置与读取。
3.4 Dubbo框架中的策略模式
Dubbo作为高性能RPC框架,其扩展点几乎全由策略模式驱动(通过SPI机制)。
1. LoadBalance负载均衡策略
LoadBalance接口定义了从多个服务提供者中选择一个的策略。RandomLoadBalance(随机)、RoundRobinLoadBalance(轮询)、LeastActiveLoadBalance(最少活跃调用)、ConsistentHashLoadBalance(一致性Hash)是其具体实现。消费者端在发起调用前,会根据配置的负载均衡策略动态选择提供者地址。
2. Cluster容错策略
Cluster接口负责将Directory中的多个Invoker伪装成一个Invoker,并提供容错逻辑。FailoverCluster(失败自动切换)、FailfastCluster(快速失败)、FailsafeCluster(失败安全)、ForkingCluster(并行调用)等都是不同的容错策略。
四、分布式环境下的策略模式
在分布式系统架构中,策略模式同样扮演着至关重要的角色,它帮助我们在面对复杂的网络环境、流量洪峰和业务定制需求时,能够灵活地切换算法和行为。
4.1 分布式负载均衡策略
Nginx作为反向代理服务器,其核心的负载均衡算法(轮询、加权轮询、IP Hash、最少连接)是策略模式的生动体现。Nginx内部维护了针对每个上游(upstream)的负载均衡上下文,并持有具体的负载均衡策略对象。当一个请求到达时,上下文将选择后端服务器的任务委托给该策略。
4.2 分布式ID生成策略
在分库分表场景下,生成全局唯一ID是基础设施层的核心问题。常见的解决方案有:雪花算法(Snowflake)(本地生成,高性能但依赖时钟)、数据库号段模式(如美团Leaf,强依赖DB)、UUID(简单但无序)、Redis自增(依赖Redis)。我们可以定义一个IdGenerator策略接口,并根据不同业务场景(如订单ID、用户ID)注入不同的生成策略。
4.3 消息队列消费策略
Kafka Consumer的分区分配策略决定了Consumer Group中的每个消费者各自负责哪些分区。RangeAssignor(按范围分配)、RoundRobinAssignor(轮询分配)、StickyAssignor(粘性分配,尽量保持原有分配)是三种内置的PartitionAssignor策略实现。这些策略直接影响了消息消费的负载均衡性和稳定性。
4.4 分布式配置中心的灰度策略
在Apollo、Nacos等配置中心中,灰度发布功能允许管理员只对部分客户端生效新配置。灰度规则可以是:按IP灰度、按用户ID哈希灰度、按请求标签灰度。GrayReleaseStrategy接口定义了boolean isGrayTarget(RequestContext ctx)方法。配置中心客户端在拉取配置时,会执行注入的灰度策略来决定获取正式配置还是灰度配置。
4.5 微服务路由策略
Spring Cloud Gateway提供了灵活的路由断言(RoutePredicateFactory)和过滤器(GatewayFilterFactory)机制。例如,PathRoutePredicateFactory、HeaderRoutePredicateFactory、WeightRoutePredicateFactory都是具体的路由匹配策略。RouteDefinitionRouteLocator作为上下文,持有这些策略并应用它们以决定请求的最终路由。
4.6 分布式限流算法示例:可插拔的限流策略
限流是保障系统稳定性的关键手段。我们可以设计一个可插拔的限流框架,支持固定窗口、滑动窗口、令牌桶、漏桶等策略。
// 抽象策略接口
public interface RateLimiterStrategy {
boolean tryAcquire(String key, int permits);
}
// 具体策略:令牌桶算法(使用Guava RateLimiter模拟)
public class TokenBucketStrategy implements RateLimiterStrategy {
private final Map<String, RateLimiter> limiters = new ConcurrentHashMap<>();
@Override
public boolean tryAcquire(String key, int permits) {
RateLimiter limiter = limiters.computeIfAbsent(key, k -> RateLimiter.create(10.0)); // 10 QPS
return limiter.tryAcquire(permits);
}
}
// 具体策略:滑动窗口计数(基于Redis + Lua)
public class SlidingWindowStrategy implements RateLimiterStrategy {
@Override
public boolean tryAcquire(String key, int permits) {
// 省略Redis Lua脚本执行细节...
return true;
}
}
// 上下文:限流入口
public class RateLimiterContext {
private RateLimiterStrategy strategy;
public RateLimiterContext(RateLimiterStrategy strategy) {
this.strategy = strategy;
}
public boolean limit(String resourceKey) {
return strategy.tryAcquire(resourceKey, 1);
}
}
4.7 分布式负载均衡策略架构流程图
flowchart TD
A["客户端请求"] --> B{"负载均衡器入口"}
B --> C["获取可用服务实例列表"]
C --> D["策略选择器<br>(根据配置读取负载均衡策略)"]
D --> E{"策略类型"}
E -->|"随机策略"| F["RandomLoadBalancer<br>随机选择一个实例"]
E -->|"轮询策略"| G["RoundRobinLoadBalancer<br>按顺序轮询实例"]
E -->|"一致性哈希策略"| H["ConsistentHashLoadBalancer<br>根据请求参数哈希选择实例"]
E -->|"最少连接策略"| I["LeastConnectionsLoadBalancer<br>选择连接数最少的实例"]
F --> J["获取目标服务实例IP:Port"]
G --> J
H --> J
I --> J
J --> K["转发请求至目标服务"]
K --> L["目标服务处理并响应"]
L --> M["返回客户端"]
架构流程图解读:
该流程图清晰地展示了在分布式负载均衡场景下策略模式的工作机制。当客户端请求到达负载均衡器时,首先会从服务注册中心获取一份健康的服务实例列表。随后,策略选择器(通常是一个配置读取组件)根据预先设定的配置项(如load-balance=round_robin)决定本次应当采用哪一种具体的负载均衡策略。在E节点,系统根据策略类型分发到不同的策略实现模块:RandomLoadBalancer(随机策略)利用伪随机算法挑选实例;RoundRobinLoadBalancer(轮询策略)通过原子计数器顺序返回实例;ConsistentHashLoadBalancer(一致性哈希策略)对请求的关键信息(如用户ID)进行哈希运算,将请求粘滞到特定实例,适用于有状态服务;LeastConnectionsLoadBalancer(最少连接策略)动态评估各实例的活跃连接数,将流量导向负载最轻的节点。最终,选定的目标服务地址被传递给转发模块,完成请求代理。此设计使得负载均衡算法可以独立于请求转发的核心流程,无论是替换现有算法还是增加新的算法(如基于响应时间的自适应策略),都无需修改负载均衡器的核心调度代码,体现了策略模式在大型分布式系统中的高度灵活性。
五、对比辨析
深入理解策略模式还需要将其与其他行为型或结构型模式进行对比,以避免误用。
1. 策略模式 vs 状态模式
这是最容易混淆的一对模式,因为它们的UML类图结构完全相同。区别在于意图(Intent)。
- 策略模式:算法由外部客户端选择并注入,一旦选定,在单次业务流程中通常保持不变。例如,客户端选择了支付宝支付策略,在整个支付流程中不会自动切换成微信支付。策略之间的切换是外部主动触发的。
- 状态模式:行为由内部状态驱动,状态对象通常包含对上下文的引用,并能在执行过程中自行切换上下文的状态。例如,TCP连接从
Established状态收到Close请求后,状态机自动转换为Closing状态。切换是内部自动发生的。
2. 策略模式 vs 命令模式
- 策略模式:关注算法的可替换性。多个策略对象是平等的,它们解决同一个问题(如计算折扣),只是实现方式不同。
- 命令模式:关注请求的封装与参数化。它将一个请求(包含接收者和操作)封装为一个对象,从而支持请求的排队、记录日志、撤销/重做等操作。命令模式中的不同命令通常解决不同的问题(如
OpenCommand、CloseCommand)。
3. 策略模式 vs 工厂模式
- 工厂模式(包括简单工厂、工厂方法、抽象工厂):关注对象的创建。它隐藏了对象创建的细节,客户端通过工厂获取产品实例。
- 策略模式:关注行为的封装与替换。客户端通常已经持有策略对象(可能是通过工厂创建的),并通过上下文使用它。
- 结合使用:常常使用工厂模式来生产策略对象,实现根据条件自动选择策略的功能(如前述的策略工厂示例)。
4. 策略模式 vs 模板方法模式
- 策略模式:使用对象组合(委托)。它将整个算法替换掉。上下文通过委托给策略接口来完成工作。
- 模板方法模式:使用继承。它在父类中定义了一个算法的骨架(固定不变的部分),并将某些特定步骤延迟到子类中实现。它替换的是算法的部分步骤,而非整个算法。
5. 策略模式 vs 函数式编程
Java 8引入的Lambda表达式极大地简化了策略模式的实现。一个策略接口如果只有一个抽象方法(函数式接口),就可以直接用Lambda表达式替代具体的策略类。例如:
Comparator<T>对应(T o1, T o2) -> intRunnable对应() -> void- 自定义的
DiscountStrategy对应double price -> double这使得策略模式从“面向类的设计”转向“面向函数的设计”,消除了大量样板代码,使代码更加简洁。
六、适用场景分析(重点强化)
以下将通过五个典型的业务场景,提供完整的可运行Demo、Mermaid图示及深入分析。
场景一:电商促销折扣系统
Demo代码:
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
// 1. 策略接口
interface PromotionStrategy {
BigDecimal calculateDiscount(BigDecimal originalPrice);
}
// 2. 具体策略:无折扣
class NoDiscountStrategy implements PromotionStrategy {
@Override
public BigDecimal calculateDiscount(BigDecimal price) { return price; }
}
// 具体策略:满减策略
class FullReductionStrategy implements PromotionStrategy {
private final BigDecimal threshold;
private final BigDecimal reduction;
public FullReductionStrategy(BigDecimal threshold, BigDecimal reduction) {
this.threshold = threshold;
this.reduction = reduction;
}
@Override
public BigDecimal calculateDiscount(BigDecimal price) {
if (price.compareTo(threshold) >= 0) {
return price.subtract(reduction);
}
return price;
}
}
// 具体策略:打折策略
class PercentageDiscountStrategy implements PromotionStrategy {
private final BigDecimal discountRate; // 0.8 表示8折
public PercentageDiscountStrategy(BigDecimal discountRate) {
this.discountRate = discountRate;
}
@Override
public BigDecimal calculateDiscount(BigDecimal price) {
return price.multiply(discountRate);
}
}
// 3. 策略工厂:根据活动类型自动选择策略
class PromotionStrategyFactory {
private static final Map<String, PromotionStrategy> STRATEGY_CACHE = new HashMap<>();
static {
STRATEGY_CACHE.put("NORMAL", new NoDiscountStrategy());
STRATEGY_CACHE.put("FULL_100_MINUS_10", new FullReductionStrategy(new BigDecimal("100"), new BigDecimal("10")));
STRATEGY_CACHE.put("PERCENT_80", new PercentageDiscountStrategy(new BigDecimal("0.8")));
}
public static PromotionStrategy getStrategy(String promotionCode) {
return STRATEGY_CACHE.getOrDefault(promotionCode, new NoDiscountStrategy());
}
}
// 4. 上下文:价格计算器
class PriceCalculatorContext {
private PromotionStrategy strategy;
public PriceCalculatorContext(PromotionStrategy strategy) { this.strategy = strategy; }
public void setStrategy(PromotionStrategy strategy) { this.strategy = strategy; }
public BigDecimal calculate(BigDecimal price) {
System.out.println("原价: " + price);
BigDecimal finalPrice = strategy.calculateDiscount(price);
System.out.println("折后价: " + finalPrice);
return finalPrice;
}
}
// 5. 策略叠加演示:组合模式结合策略模式
class CompositePromotionStrategy implements PromotionStrategy {
private final List<PromotionStrategy> strategies;
public CompositePromotionStrategy(List<PromotionStrategy> strategies) { this.strategies = strategies; }
@Override
public BigDecimal calculateDiscount(BigDecimal price) {
BigDecimal result = price;
for (PromotionStrategy s : strategies) {
result = s.calculateDiscount(result);
}
return result;
}
}
public class EcommerceDemo {
public static void main(String[] args) {
PriceCalculatorContext context = new PriceCalculatorContext(null);
BigDecimal orderPrice = new BigDecimal("150");
// 根据用户领取的优惠券码自动匹配策略
String userCouponCode = "FULL_100_MINUS_10";
context.setStrategy(PromotionStrategyFactory.getStrategy(userCouponCode));
context.calculate(orderPrice);
// 策略叠加:先满减再打折
List<PromotionStrategy> strategies = Arrays.asList(
new FullReductionStrategy(new BigDecimal("100"), new BigDecimal("10")),
new PercentageDiscountStrategy(new BigDecimal("0.9"))
);
context.setStrategy(new CompositePromotionStrategy(strategies));
context.calculate(orderPrice);
}
}
Mermaid类图:
classDiagram
class PromotionStrategy {
<<interface>>
+calculateDiscount(BigDecimal price) BigDecimal
}
class NoDiscountStrategy {
+calculateDiscount(BigDecimal price) BigDecimal
}
class FullReductionStrategy {
-threshold: BigDecimal
-reduction: BigDecimal
+calculateDiscount(BigDecimal price) BigDecimal
}
class PercentageDiscountStrategy {
-discountRate: BigDecimal
+calculateDiscount(BigDecimal price) BigDecimal
}
class CompositePromotionStrategy {
-strategies: List~PromotionStrategy~
+calculateDiscount(BigDecimal price) BigDecimal
}
class PriceCalculatorContext {
-strategy: PromotionStrategy
+calculate(BigDecimal price) BigDecimal
}
class PromotionStrategyFactory {
+getStrategy(String code) PromotionStrategy
}
PromotionStrategy <|.. NoDiscountStrategy
PromotionStrategy <|.. FullReductionStrategy
PromotionStrategy <|.. PercentageDiscountStrategy
PromotionStrategy <|.. CompositePromotionStrategy
PriceCalculatorContext --> PromotionStrategy
PromotionStrategyFactory ..> PromotionStrategy : creates
文字说明:
在电商促销折扣系统中,促销规则变化频繁且多样。通过引入策略模式,我们将“满减”、“打折”、“无折扣”等具体算法封装为独立的PromotionStrategy实现。PriceCalculatorContext作为价格计算上下文,负责执行委托来的折扣计算。PromotionStrategyFactory策略工厂屏蔽了策略对象的创建细节,客户端只需传入一个活动编码即可获取对应的策略实例。此外,本Demo还展示了策略模式的组合特性——CompositePromotionStrategy实现了策略接口并持有一组策略,从而支持了促销策略的叠加(例如先满减再打折)。这种设计使得营销团队新增一种促销方式(如“买二赠一”)时,开发人员只需新增一个具体策略类并注册到工厂中,完全遵循开闭原则,且不会对核心订单计算流程产生任何侵入性影响。
场景二:支付方式选择
Demo代码:
// 1. 支付策略接口
interface PaymentStrategy {
void pay(BigDecimal amount);
}
// 2. 具体策略:支付宝
class AlipayStrategy implements PaymentStrategy {
@Override
public void pay(BigDecimal amount) {
System.out.println("调用支付宝API,支付金额:" + amount + "元");
// 模拟API调用逻辑
}
}
// 具体策略:微信支付
class WechatPayStrategy implements PaymentStrategy {
@Override
public void pay(BigDecimal amount) {
System.out.println("调用微信支付API,支付金额:" + amount + "元");
}
}
// 具体策略:银行卡支付
class BankCardStrategy implements PaymentStrategy {
private String cardNumber;
public BankCardStrategy(String cardNumber) { this.cardNumber = cardNumber; }
@Override
public void pay(BigDecimal amount) {
System.out.println("调用银行接口,卡号:" + cardNumber + ",支付金额:" + amount + "元");
}
}
// 3. 支付上下文
class PaymentContext {
private PaymentStrategy paymentStrategy;
public PaymentContext(PaymentStrategy strategy) { this.paymentStrategy = strategy; }
public void setPaymentStrategy(PaymentStrategy strategy) { this.paymentStrategy = strategy; }
public void executePayment(BigDecimal amount) {
if (paymentStrategy == null) throw new IllegalStateException("未选择支付方式");
paymentStrategy.pay(amount);
}
}
public class PaymentDemo {
public static void main(String[] args) {
PaymentContext context = new PaymentContext(null);
Scanner scanner = new Scanner(System.in);
System.out.println("请选择支付方式: 1.支付宝 2.微信 3.银行卡");
int choice = scanner.nextInt();
// 根据用户选择动态注入策略
switch (choice) {
case 1: context.setPaymentStrategy(new AlipayStrategy()); break;
case 2: context.setPaymentStrategy(new WechatPayStrategy()); break;
case 3: context.setPaymentStrategy(new BankCardStrategy("6222****1234")); break;
}
context.executePayment(new BigDecimal("99.99"));
}
}
Mermaid时序图:
sequenceDiagram
participant User
participant PaymentController as 支付前端控制器
participant PaymentContext as 支付上下文
participant StrategyFactory as 策略工厂(可选)
participant AlipayStrategy as 支付宝策略
participant AliPayAPI as 支付宝API网关
User->>PaymentController: 选择支付宝支付并确认订单
PaymentController->>StrategyFactory: getStrategy("ALIPAY")
StrategyFactory-->>PaymentController: 返回 AlipayStrategy 实例
PaymentController->>PaymentContext: setPaymentStrategy(alipayStrategy)
PaymentController->>PaymentContext: executePayment(amount)
PaymentContext->>AlipayStrategy: pay(amount)
activate AlipayStrategy
AlipayStrategy->>AliPayAPI: 发起支付请求(参数签名)
AliPayAPI-->>AlipayStrategy: 返回支付结果(成功/失败)
AlipayStrategy-->>PaymentContext: 支付处理完成
deactivate AlipayStrategy
PaymentContext-->>PaymentController: 返回支付响应
PaymentController-->>User: 显示支付结果
文字说明:
该时序图清晰呈现了支付方式选择场景中策略模式的运行时交互。用户在前端触发支付请求并选定支付宝后,后端控制器(或Service层)调用策略工厂(或直接实例化)获取对应的AlipayStrategy对象,并通过setPaymentStrategy方法将其注入到PaymentContext中。随后控制器调用PaymentContext的统一支付接口executePayment。此时,上下文对象毫不关心具体的支付渠道逻辑,它只是单纯地将支付请求委托给当前持有的AlipayStrategy。在策略对象内部,它负责组装特定于支付宝的请求参数(如签名、格式化金额),并调用支付宝的远程API。这种设计带来的最大好处是支付渠道的横向扩展能力。假设未来需要接入“云闪付”或“PayPal”,我们只需新增一个CloudQuickPayStrategy类并实现PaymentStrategy接口,其余代码(包括上下文、前端调用逻辑)均无需任何修改。这使得支付系统具备了高度的灵活性和可维护性。
场景三:日志输出策略
Demo代码:
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.Socket;
// 1. 日志策略接口
interface LogStrategy {
void log(String message);
}
// 2. 具体策略:控制台输出
class ConsoleLogStrategy implements LogStrategy {
@Override
public void log(String message) {
System.out.println("[CONSOLE] " + message);
}
}
// 具体策略:文件输出
class FileLogStrategy implements LogStrategy {
private final String filePath;
public FileLogStrategy(String filePath) { this.filePath = filePath; }
@Override
public void log(String message) {
try (FileWriter fw = new FileWriter(filePath, true);
PrintWriter pw = new PrintWriter(fw)) {
pw.println("[FILE] " + message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 具体策略:远程Socket输出
class SocketLogStrategy implements LogStrategy {
private final String host;
private final int port;
public SocketLogStrategy(String host, int port) { this.host = host; this.port = port; }
@Override
public void log(String message) {
try (Socket socket = new Socket(host, port);
PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) {
out.println("[SOCKET] " + message);
} catch (IOException e) {
System.err.println("Socket日志发送失败: " + e.getMessage());
}
}
}
// 3. 日志上下文
class LoggerContext {
private LogStrategy strategy;
public LoggerContext(LogStrategy strategy) { this.strategy = strategy; }
public void setStrategy(LogStrategy strategy) { this.strategy = strategy; }
public void log(String message) {
if (strategy != null) {
strategy.log(message);
}
}
}
public class LoggerDemo {
public static void main(String[] args) {
LoggerContext logger = new LoggerContext(new ConsoleLogStrategy());
logger.log("系统启动成功");
// 动态切换为文件策略
logger.setStrategy(new FileLogStrategy("app.log"));
logger.log("用户登录成功");
// 动态切换为Socket策略
logger.setStrategy(new SocketLogStrategy("localhost", 9999));
logger.log("关键业务操作");
}
}
Mermaid流程图:
flowchart TD
A[应用程序调用 LoggerContext.log] --> B{LoggerContext持有策略引用?}
B -->|是| C[调用 strategy.log]
B -->|否| D[忽略日志或抛出异常]
C --> E{策略类型判断}
E -->|ConsoleLogStrategy| F[System.out.println 输出到控制台]
E -->|FileLogStrategy| G[打开FileWriter, 追加写入文件]
E -->|SocketLogStrategy| H[建立Socket连接, 发送日志流]
F --> I[日志输出完成]
G --> I
H --> I
文字说明:
该流程图描述了策略模式在日志记录框架中的应用。业务代码仅与LoggerContext交互,调用其log()方法。在LoggerContext内部,它首先检查是否已注入有效的日志策略引用(无策略时可能默认不打印或抛出异常)。随后,它将日志内容原封不动地委托给该策略对象的log()方法。根据运行时注入的具体策略类型(控制台、文件、Socket),日志数据被导向不同的目的地。这种设计正是SLF4J(Simple Logging Facade for Java)等日志门面框架的核心思想。SLF4J提供统一的Logger接口作为上下文,而具体的实现(Logback、Log4j2、java.util.logging)则是可插拔的策略。通过策略模式,应用程序可以在完全不修改代码的情况下,仅通过替换classpath下的Jar包和配置文件,实现日志输出的热切换(例如从开发环境的控制台输出切换到生产环境的异步文件输出)。这种解耦极大地提升了系统的可移植性和配置灵活性。
场景四:数据压缩算法选择
Demo代码:
import java.io.*;
import java.util.zip.GZIPOutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
// 1. 压缩策略接口
interface CompressionStrategy {
void compress(File inputFile, File outputFile) throws IOException;
}
// 2. 具体策略:ZIP压缩
class ZipCompressionStrategy implements CompressionStrategy {
@Override
public void compress(File inputFile, File outputFile) throws IOException {
try (FileOutputStream fos = new FileOutputStream(outputFile);
ZipOutputStream zos = new ZipOutputStream(fos);
FileInputStream fis = new FileInputStream(inputFile)) {
ZipEntry entry = new ZipEntry(inputFile.getName());
zos.putNextEntry(entry);
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) > 0) {
zos.write(buffer, 0, len);
}
zos.closeEntry();
}
System.out.println("ZIP压缩完成: " + outputFile.getPath());
}
}
// 具体策略:GZIP压缩
class GzipCompressionStrategy implements CompressionStrategy {
@Override
public void compress(File inputFile, File outputFile) throws IOException {
try (FileInputStream fis = new FileInputStream(inputFile);
FileOutputStream fos = new FileOutputStream(outputFile);
GZIPOutputStream gzos = new GZIPOutputStream(fos)) {
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) > 0) {
gzos.write(buffer, 0, len);
}
}
System.out.println("GZIP压缩完成: " + outputFile.getPath());
}
}
// 3. 策略选择器(结合工厂模式自动判断)
class CompressionStrategySelector {
public static CompressionStrategy selectStrategy(File inputFile) {
String fileName = inputFile.getName().toLowerCase();
if (fileName.endsWith(".txt") || fileName.endsWith(".log")) {
return new GzipCompressionStrategy(); // 文本文件压缩率高
} else if (fileName.endsWith(".jpg") || fileName.endsWith(".png")) {
// 图片通常已经是压缩格式,直接存储或使用ZIP归档
return new ZipCompressionStrategy();
}
return new ZipCompressionStrategy(); // 默认
}
}
// 4. 上下文
class FileCompressor {
private CompressionStrategy strategy;
public FileCompressor(CompressionStrategy strategy) { this.strategy = strategy; }
public void compress(File input, File output) throws IOException {
strategy.compress(input, output);
}
}
public class CompressionDemo {
public static void main(String[] args) throws IOException {
File logFile = new File("application.log");
// 模拟文件存在,实际运行时请创建测试文件
if (!logFile.exists()) logFile.createNewFile();
CompressionStrategy strategy = CompressionStrategySelector.selectStrategy(logFile);
FileCompressor compressor = new FileCompressor(strategy);
compressor.compress(logFile, new File("application.log.gz"));
}
}
Mermaid流程图:
flowchart TD
A["输入待压缩文件"] --> B["策略选择器分析文件特征<br>(如扩展名)"]
B --> C{"文件类型判断"}
C -->|"文本文件 .txt .log"| D["选用 GzipCompressionStrategy<br>(压缩比高)"]
C -->|"图片文件 .jpg .png"| E["选用 ZipCompressionStrategy<br>(归档存储)"]
C -->|"其他类型"| F["选用默认 ZipCompressionStrategy"]
D --> G["创建 FileCompressor 上下文"]
E --> G
F --> G
G --> H["上下文执行 compress 方法"]
H --> I["具体策略内部调用 Java API<br>(GZIPOutputStream / ZipOutputStream)"]
I --> J["生成压缩后的文件"]
文字说明:
本场景展示了策略模式与工厂模式的深度融合。面对不同的文件类型,最合适的压缩算法往往不同:文本文件使用GZIP能获得极高的压缩率,而对于已经过编码压缩的图片文件,使用ZIP归档更为合适。CompressionStrategySelector(策略选择器)扮演了工厂的角色,它封装了策略选择的逻辑。主流程中,FileCompressor上下文无需知晓背后复杂的选型规则,它只负责执行被注入的CompressionStrategy的compress方法。这种架构的优势在于算法的智能匹配与透明替换。如果未来引入了新的压缩算法(如Zstd或Brotli),我们只需新增一个ZstdCompressionStrategy类,并在Selector中补充针对特定文件类型的判断逻辑即可。对于上层调用者而言,压缩过程始终是compressor.compress()这一个简单调用。此外,不同的压缩策略在性能(CPU消耗、压缩比、内存占用)上存在显著差异,通过策略模式,我们可以在运行时根据系统负载情况(例如在CPU空闲时使用高压缩比策略,繁忙时使用低消耗策略)动态切换,实现资源的精细化调度。
场景五:消息通知渠道切换
Demo代码:
import java.util.Properties;
// 1. 通知策略接口
interface NotificationStrategy {
void send(String recipient, String message);
}
// 2. 具体策略:邮件通知
class EmailNotificationStrategy implements NotificationStrategy {
@Override
public void send(String recipient, String message) {
System.out.println("发送邮件至 [" + recipient + "] : " + message);
// 调用JavaMail API...
}
}
// 具体策略:短信通知
class SmsNotificationStrategy implements NotificationStrategy {
@Override
public void send(String recipient, String message) {
System.out.println("发送短信至 [" + recipient + "] : " + message);
// 调用阿里云/腾讯云短信API...
}
}
// 具体策略:App推送
class AppPushNotificationStrategy implements NotificationStrategy {
@Override
public void send(String recipient, String message) {
System.out.println("推送App消息至设备 [" + recipient + "] : " + message);
// 调用极光推送/个推API...
}
}
// 3. 带降级功能的上下文
class NotificationContext {
private NotificationStrategy primaryStrategy;
private NotificationStrategy fallbackStrategy;
public NotificationContext(NotificationStrategy primary, NotificationStrategy fallback) {
this.primaryStrategy = primary;
this.fallbackStrategy = fallback;
}
public void notify(String recipient, String message) {
try {
primaryStrategy.send(recipient, message);
} catch (Exception e) {
System.err.println("主策略发送失败,启用降级策略: " + e.getMessage());
if (fallbackStrategy != null) {
fallbackStrategy.send(recipient, "[降级] " + message);
}
}
}
}
// 4. 策略配置加载器
class StrategyConfigLoader {
public static NotificationStrategy loadFromProperties() {
// 模拟从application.properties读取配置
String className = "EmailNotificationStrategy"; // 实际可动态读取
try {
return (NotificationStrategy) Class.forName(className).newInstance();
} catch (Exception e) {
return new EmailNotificationStrategy(); // 默认
}
}
}
public class NotificationDemo {
public static void main(String[] args) {
// 根据配置或用户偏好选择主策略和降级策略
NotificationStrategy primary = new SmsNotificationStrategy();
NotificationStrategy fallback = new EmailNotificationStrategy();
NotificationContext context = new NotificationContext(primary, fallback);
context.notify("user123", "您的订单已发货");
}
}
Mermaid时序图:
sequenceDiagram
participant BusinessService as 业务服务
participant NotificationContext as 通知上下文
participant PrimaryStrategy as 短信策略(主)
participant FallbackStrategy as 邮件策略(降级)
participant ExternalAPI as 第三方API
BusinessService->>NotificationContext: notify(user, message)
activate NotificationContext
NotificationContext->>PrimaryStrategy: send(recipient, message)
activate PrimaryStrategy
PrimaryStrategy->>ExternalAPI: 调用短信网关
ExternalAPI-->>PrimaryStrategy: 返回失败(网络超时)
PrimaryStrategy-->>NotificationContext: 抛出异常
deactivate PrimaryStrategy
Note over NotificationContext: 捕获异常,执行降级逻辑
NotificationContext->>FallbackStrategy: send(recipient, "[降级] " + message)
activate FallbackStrategy
FallbackStrategy->>ExternalAPI: 调用邮件SMTP服务
ExternalAPI-->>FallbackStrategy: 发送成功
FallbackStrategy-->>NotificationContext: 发送完成
deactivate FallbackStrategy
NotificationContext-->>BusinessService: 通知流程结束
deactivate NotificationContext
文字说明:
在分布式系统中,消息通知渠道的稳定性和可达性至关重要。该时序图展示了策略模式如何优雅地支持多策略优先级与降级机制。NotificationContext同时持有了主策略(PrimaryStrategy,如短信)和降级策略(FallbackStrategy,如邮件)。当业务服务触发通知时,上下文首先尝试调用主策略发送消息。如果主策略执行成功,则流程结束;若发生异常(如短信网关超时、余额不足),上下文捕获异常后自动切换到降级策略继续尝试。这种设计保证了消息的最终送达率,且主流程代码无需编写复杂的if-else判断。更进一步,结合StrategyConfigLoader,我们可以实现策略配置的外部化:将策略的实现类全限定名写入配置文件(如Apollo或Nacos),系统启动时通过反射动态加载。这意味着在不重启应用的情况下,运维人员只需修改配置中心的值,即可将全站的通知策略从“短信优先”切换为“App推送优先”或“仅邮件通知”,极大地提升了系统的运营弹性和容灾能力。
七、面试题精选与专家级解答
1. 策略模式和状态模式在UML类图上几乎一模一样,它们的本质区别是什么?如何在实际项目中区分使用?
答: 尽管UML类图结构高度相似(都有Context持有State/Strategy引用,并定义抽象行为接口),但二者的设计意图截然不同。
- 策略模式:关注算法的可替换性,行为的选择权在外部客户端。客户端明确知道有哪些策略,并根据条件主动将选定的策略注入给上下文。在单次业务流程中,策略通常保持不变。
- 状态模式:关注对象内部状态变迁驱动的行为变化。上下文的行为由其当前状态决定,并且状态对象内部通常持有对上下文的引用,能够在执行过程中自行修改上下文的状态,实现状态的自动流转(如TCP连接从Listening到Established再到Closed)。
项目区分原则:如果发现代码中是在外部根据type字段if-else来setStrategy,那这就是策略模式。如果发现Context的行为变化是由其内部某个字段的值自动触发的,并且该字段的修改是在State实现类内部完成的(例如context.setState(new ClosedState())),那这就是状态模式。
2. JDK中哪些地方使用了策略模式?请至少列举四个并分析其实现细节。
答:
java.util.Comparator:Comparator接口定义了比较算法,Collections.sort(List, Comparator)作为上下文,在执行排序时回调策略的方法。java.util.concurrent.RejectedExecutionHandler:线程池拒绝策略。ThreadPoolExecutor在无法处理任务时,委托给RejectedExecutionHandler的rejectedExecution方法。内置的AbortPolicy、CallerRunsPolicy等均是其具体策略。java.awt.LayoutManager:GUI布局管理器。Container持有LayoutManager引用,在doLayout()时将组件排列计算委托给具体的布局策略(如BorderLayout)。java.nio.charset.Charset:字符集编解码策略。虽然Charset是抽象类,但其子类封装了具体的编码转换算法,CharsetEncoder/Decoder也是策略的体现。
3. 策略模式如何与工厂模式结合使用?请画出类图并说明结合后的优势。
答: 结合后,工厂负责根据条件创建具体的策略对象,上下文只负责执行。这进一步解耦了客户端与具体策略类的依赖。
classDiagram
class Strategy {
<<interface>>
+algorithm()
}
class ConcreteStrategyA {
+algorithm()
}
class ConcreteStrategyB {
+algorithm()
}
class Context {
-strategy: Strategy
+execute()
}
class StrategyFactory {
+getStrategy(type): Strategy
}
Context --> Strategy
ConcreteStrategyA ..|> Strategy
ConcreteStrategyB ..|> Strategy
StrategyFactory ..> ConcreteStrategyA : creates
StrategyFactory ..> ConcreteStrategyB : creates
Client --> StrategyFactory : uses
Client --> Context : uses
优势:
- 单一职责:工厂类专职处理策略选择的逻辑(可能包含复杂的配置读取、缓存等),上下文类专注于调用算法。
- 降低耦合:客户端完全不需要知道具体策略类的名称,只需传入一个类型标识符(如String或Enum)即可获得正确的策略对象。
4. 使用策略模式后如何避免策略类的爆炸式增长?请列举几种优化手段。
答:
- 策略枚举:对于数量固定且行为相对简单的策略,使用枚举实现策略模式,每个枚举项作为一个策略单例。
- 函数式策略:利用Java 8 Lambda表达式或方法引用,对于简单的策略(如
p -> p * 0.8),无需编写独立的.java类文件。 - 参数化策略:不要为每种参数组合都创建一个新类。例如
DiscountStrategy不应有Discount80Strategy、Discount90Strategy,而应有一个PercentageDiscountStrategy,在构造时传入折扣率参数。 - 策略工厂 + 配置化:将策略实例缓存在Map中,避免重复创建对象。
5. Dubbo的负载均衡策略是如何体现策略模式的?如果让你扩展一种新的负载均衡策略,需要修改哪些代码?
答:
Dubbo通过SPI机制完美诠释了策略模式。LoadBalance接口是抽象策略,RandomLoadBalance等是具体策略。AbstractClusterInvoker在doSelect方法中,通过LoadBalance lb = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(loadbalanceName)获取策略实例,然后调用lb.select(invokers, invocation)完成负载均衡选择。
扩展新策略:完全无需修改Dubbo核心源码。只需在META-INF/dubbo/org.apache.dubbo.rpc.cluster.LoadBalance文件中添加一行myLB=com.xxx.MyLoadBalance,并实现LoadBalance接口即可。Dubbo的SPI会自动加载并实例化该策略。
6. 策略模式与函数式编程中的高阶函数有什么联系?Java 8 Lambda如何影响策略模式的实现方式?
答: 策略接口在本质上就是一个高阶函数(接收参数返回结果的函数对象)。在Java 8之前,必须通过匿名内部类或显式类实现策略。Java 8之后,只要策略接口符合函数式接口规范(即只有一个抽象方法),就可以直接使用Lambda表达式进行赋值。
- 策略接口
Comparator<T>对应高阶函数(T, T) -> int。 - 策略接口
Runnable对应高阶函数() -> void。 Lambda消除了策略模式的“类膨胀”问题,使得代码更加简洁和函数式,但设计思想依然是策略模式的委托机制。
7. 在分布式系统中,如何利用策略模式实现灰度发布和AB测试?
答:
定义GrayReleaseStrategy接口,方法boolean isGrayUser(RequestContext ctx)。具体策略包括:
WhiteListStrategy:白名单用户策略。PercentageRolloutStrategy:按百分比放量策略。HashBasedStrategy:按用户ID哈希取模策略。 在网关层或配置中心客户端,GrayReleaseContext持有该策略。每次请求到来时,调用isGrayUser判断是否走灰度链路。AB测试类似,定义TrafficSplittingStrategy,通过不同的分流算法(随机、哈希、地域)将流量导向不同版本的服务实例组。
8. 策略模式中的策略对象是否应该设计为无状态的?如果是,如何保证线程安全?
答: 强烈建议设计为无状态(Stateless)对象。因为策略对象通常会被上下文持有,并可能在多线程环境下并发调用(例如Spring单例Bean中的策略引用)。如果策略对象内部包含可变成员变量(如计数器、临时计算结果),就会引发线程安全问题。
保证线程安全的手段:
- 无状态设计:策略方法所需的全部数据均通过方法参数传入,不在类中定义实例变量。
- 使用
ThreadLocal:如果确实需要保存状态,且状态是线程绑定的,可以使用ThreadLocal包装。 - 原型作用域:在Spring中可以将策略Bean声明为
@Scope("prototype"),但无状态单例始终是性能最优解。
9. 如何实现一个支持运行时动态加载策略的插件化架构?请简述类加载器与SPI的应用。
答: 实现插件化架构的核心是自定义类加载器(ClassLoader)和SPI(Service Provider Interface)机制。
- 定义接口:发布一个标准API Jar包,内含
PluginStrategy接口。 - 插件实现:第三方开发者实现该接口,并在
META-INF/services/下配置实现类全名。 - 动态加载:主程序扫描指定目录(如
plugins/)下的Jar包,使用URLClassLoader加载这些Jar中的类,并结合ServiceLoader.load(PluginStrategy.class, customClassLoader)来发现并实例化插件策略。 - 策略注册:将加载到的策略对象注册到策略工厂的Map中,供运行时选择使用。这样即可在不停止主程序的情况下热插拔策略实现。
10. 策略模式在处理大量条件分支时比if-else更优雅,但什么情况下仍然应该使用if-else而不是策略模式?
答: 尽管策略模式很强大,但在以下情况中,使用简单的if-else反而更合适:
- 分支数量极少且确定不会扩展(如只有2-3个分支且未来5年不会变)。引入模式会带来额外的类和接口开销,得不偿失。
- 算法逻辑极其简单(如一行
return price * 0.8)。为了这么简单的逻辑单独建类是过度设计。 - 性能极度敏感的底层代码。策略模式涉及接口多态调用、对象创建,虽然JIT会优化,但在超高频调用的底层循环中,if-else的
tableswitch指令可能比虚方法调用略快。 - 快速原型验证阶段。在不确定业务模型稳定性的早期,直接写if-else迭代速度最快,待模型清晰后再重构为策略模式。
八、总结
策略模式作为GoF设计模式中使用频率最高的模式之一,其价值不仅在于消除了丑陋的if-else语句,更在于它构建了一种“面向接口编程”的思维范式。从JDK底层排序到Spring框架的IoC核心抽象,从Dubbo的微服务治理到分布式架构的灰度策略,策略模式无处不在。掌握它,意味着掌握了构建高内聚、低耦合、易扩展软件系统的关键钥匙。希望通过本篇逾万字的长文,你能彻底驾驭策略模式,并在你的架构师之路上更进一步。