在软件开发中,我们经常会遇到一些场景,其中业务流程大致相同,但具体的操作步骤或算法却可能因为某些条件的不同而有所变化。为了应对这种情况,设计模式中的“策略模式”提供了一种优雅的解决方案。 本文将探讨策略模式的概念、应用场景、以及不同的实现方式,希望这个分享能节省大家的开发时间,这样可以有更多的时间来做更多想做的事,譬如陪陪家人。
最近在阅读其它项目的代码时,发现策略模式出现的频次高,且各种写法都有,有的阅读起来费劲想骂人,有的如沐春风。
代码实现1: 角色1:抽象得到的策略接口
角色2:具体策略
角色3:上下文
角色4:客户端
代码实现2: 角色1:抽象得到的策略接口
角色2:具体策略
角色3:上下文
角色4:客户端
代码实现3:
角色1:抽象得到的策略接口
角色2:具体策略
角色3:上下文
角色4:客户端
这类代码看多了,就想抽时间总结总结落地策略模式的最佳实践。
中午收到一篇关于策略模式的推文,看过后,觉得写的挺好,不同地方可以借鉴,赞叹的同时又产生了一些新的想法和感悟。本着择日不如撞日,就今天写了。
策略模式(Strategy Pattern)是一种行为型(Behavioral Pattern)的设计模式,实现了算法独立于使用它的客户端而独立变化,解耦了算法的具体实现与客户端的调用。
策略模式定义了一系列算法,并将每一个算法封装起来,允许在运行时根据需要选择不同的算法行为。 这种模式是为了实现同一个目标,有多种解决方案或算法的情况下使用的。
在策略模式中,定义了一个抽象策略类,然后创建具体策略类来实现不同的算法,并将这些算法封装起来。
这些具体策略类通常继承自抽象策略类或实现相同的接口,以确保它们的行为是一致的,这样它们就可以互换使用。 这种设计符合里氏替换原则(Liskov Substitution Principle,LSP),可以确保程序的行为在替换对象时保持一致。
在客户端,创建一个环境类(或上下文),该类持有一个对抽象策略类的引用,通过这个引用调用相应的策略方法。 这样,客户端可以在运行时选择不同的策略,而不需要修改上下文类。
在策略模式中,主要涉及四类角色:
a. 策略接口(Strategy Interface):这是一个接口,定义了所有支持的算法的公共操作。不同的算法以不同的方式实现这个接口。
b. 具体策略(Concrete Strategies):实现策略接口的类,提供具体的算法实现。每一个类封装了一种具体的算法或行为。
c. 上下文环境(Context):上下文是使用策略的类。它包含一个策略接口的引用,用于运行时切换策略。上下文不知道具体策略的实现细节,它通过策略接口与策略交互
d. 客户端(Client): 通过上下文来执行指定的策略。
策略模式的优势在于它提供了一种灵活的方式来切换算法,使得算法的变化独立于客户端,同时也支持新算法的引入和旧算法的退役,而不会对系统的其他部分产生影响。 使用策略模式的代码实现符合开闭原则,扩展性和维护性会更好。
策略接口是否可以是抽象类? 可以的。 策略接口的真正的意思是协议或“超类型(supertype)”,并不特指java语言中interface类型的对象。 唐成,公众号:的数字化之路面向接口就是依赖倒置?
策略模式的优点包括: 1、算法的解耦,使得算法可以独立于客户端变化。 2、提高代码的可维护性和扩展性,因为新的策略可以很容易地添加到系统中。 3、提供了一系列的可供重用的算法族,避免使用多重条件判断代码,并支持开闭原则。
策略模式的缺点: 算法选择由客户端来决定,策略模式并不决定在何时使用何种算法,增加了耦合。 虽然这在一定程度上提高了系统的灵活性,但客户端需要理解所有具体策略类之间的区别,以便选择合适的算法,增加了客户端的使用难度。也相当于增加了客户端和具体算法的耦合。不符合迪米特原则(law of demeter LOD),最少知道原则(LeastKnowledge Principle 简写LKP)。
在经典的策略模式中,我们首先定义一个策略接口,然后创建一系列实现了该接口的具体策略类。上下文环境(Context)负责接收客户端的请求,并根据具体情况选择并执行相应的策略。客户端则负责创建上下文环境和提供所需的策略。
角色1: 抽象得到的策略接口
// 策略接口
public interface Strategy {
public int doOperation(int num1, int num2);
}
角色2:具体策略
// 具体策略类A:加法
public class OperationAdd implements Strategy {
@Override
public int doOperation(int num1, int num2) {
return num1 + num2;
}
}
// 具体策略类B:减法
public class OperationSubtract implements Strategy {
@Override
public int doOperation(int num1, int num2) {
return num1 - num2;
}
}
角色3:上下文
// 上下文环境
public class Context {
private Strategy strategy;
public Context(Strategy strategy) {
this.strategy = strategy;
}
public int executeStrategy(int num1, int num2) {
return strategy.doOperation(num1, num2);
}
}
角色4:客户端
// 客户端
public class Client {
public static void main(String[] args) {
// 创建上下文环境并设置策略A
Context context = new Context(new OperationAdd());
System.out.println("10 + 5 = " + context.executeStrategy(10, 5));
// 创建上下文环境并设置策略B
context = new Context(new OperationSubtract());
System.out.println("10 - 5 = " + context.executeStrategy(10, 5));
}
}
基于工厂的策略模式:当策略数量较多,或者需要在运行时动态添加或删除策略时,我们可以使用工厂模式来管理策略的创建。在这种情况下,上下文环境会与一个策略工厂合作,由工厂负责根据需要创建和提供具体的策略实例。
角色1: 抽象得到的策略接口
public interface Strategy {
public int doOperation(int num1, int num2);
}
角色2:具体策略
// 加法策略
public class OperationAdd implements Strategy {
@Override
public int doOperation(int num1, int num2) {
return num1 + num2;
}
}
// 减法策略
public class OperationSubtract implements Strategy {
@Override
public int doOperation(int num1, int num2) {
return num1 - num2;
}
}
角色3:简单工厂模式+上下文
// 策略工厂
public class StrategyFactory {
// 根据输入参数创建并返回策略对象
public static Strategy getStrategy(String type) {
if ("add".equalsIgnoreCase(type)) {
return new OperationAdd();
} else if ("subtract".equalsIgnoreCase(type)) {
return new OperationSubtract();
}
// 可以添加更多的策略类型
return null; // 或者抛出异常,表示无效的策略类型
}
}
// 上下文类
public class Context {
private Strategy strategy;
// 使用策略工厂初始化策略对象
public Context(String strategyType) {
this.strategy = StrategyFactory.getStrategy(strategyType);
}
// 执行策略操作
public int executeStrategy(int num1, int num2) {
if (strategy != null) {
return strategy.doOperation(num1, num2);
} else {
throw new IllegalStateException("Strategy is not initialized");
}
}
}
角色4:客户端
// 客户端
public class Client {
public static void main(String[] args) {
// 使用加法策略
Context context = new Context("add");
System.out.println("10 + 5 = " + context.executeStrategy(10, 5));
// 使用减法策略
context = new Context("subtract");
System.out.println("10 - 5 = " + context.executeStrategy(10, 5));
// 尝试使用不存在的策略(这将导致StrategyFactory返回null,并在Context中抛出异常)
// context = new Context("divide"); // 这将失败,因为divide策略尚未实现
// System.out.println("10 / 5 = " + context.executeStrategy(10, 5));
}
}
在这个示例中,StrategyFactory 类负责根据传入的字符串参数("add" 或 "subtract")来创建并返回相应的策略对象。Context 类使用策略工厂来获取策略对象,并在 executeStrategy 方法中执行该策略。测试类 StrategyPatternDemo 展示了如何根据输入动态切换策略。
请注意,如果尝试使用尚未实现的策略(例如 "divide"),StrategyFactory 将返回 null,然后在 Context 的 executeStrategy 方法中抛出异常,因为策略没有被正确初始化。在实际应用中,你可以根据需要添加更多的策略类型和相应的实现,并在策略工厂中扩展对它们的支持。
在实际的业务代码中算法类型也是一个请求参数。 具体的代码改动,粉丝朋友可以自己尝试写一写。
但是,大家有没有发现,工厂模式剥离了具体策略的创建过程,但是复杂度又上升了。加之我们有更好的选择,所以此处不再推荐经典策略模式。
这里对这种简单的策略,推荐用枚举进行优化。
在Java中,枚举类型是一种特殊的类,枚举的本质是创建了一些静态类的集合,用于表示固定数量的常量。在某些情况下,如果策略的数量有限且相对固定,我们可以使用枚举来简化策略的管理。在这种情况下,我们可以利用枚举来实现策略模式,上下文环境可以通过枚举值来选择并执行相应的策略,使得策略的选择更加清晰和类型安全。 以下是一个基于枚举的策略模式示例:
角色1: 抽象得到的策略接口
// 策略接口
public interface Strategy {
int doOperation(int num1, int num2);
}
角色2:具体策略
// 策略枚举,每个枚举项都实现了Strategy接口
public enum OperationStrategy implements Strategy {
ADD {
@Override
public int doOperation(int num1, int num2) {
return num1 + num2;
}
},
SUBTRACT {
@Override
public int doOperation(int num1, int num2) {
return num1 - num2;
}
};
// 这里可以添加更多的策略实现
}
角色3:上下文
// 上下文类
public class Context {
private final Strategy strategy;
public Context(Strategy strategy) {
this.strategy = strategy;
}
public int executeStrategy(int num1, int num2) {
return strategy.doOperation(num1, num2);
}
}
角色4:客户端
// 客户端
public class Client {
public static void main(String[] args) {
// 使用加法策略
Context context = new Context(OperationStrategy.ADD);
System.out.println("10 + 5 = " + context.executeStrategy(10, 5));
// 使用减法策略
context = new Context(OperationStrategy.SUBTRACT);
System.out.println("10 - 5 = " + context.executeStrategy(10, 5));
}
}
在这个示例中,我们定义了一个OperationStrategy枚举,它实现了Strategy接口。每个枚举项(如ADD和SUBTRACT)都重写了doOperation方法,以提供不同的行为。Context类接收一个Strategy类型的参数(在这里是OperationStrategy枚举项),并在executeStrategy方法中调用该策略的方法。
这种方法的好处是类型安全和清晰的意图表达。因为策略是作为枚举类型的一部分,所以策略的选择只能是预定义的那些选项,不能是任意的对象。这使得代码更加健壮和易于维护。同时,由于枚举类型的清晰性,代码的可读性也得到了提高。
可以看到,如果策略简单的话,基于枚举的策略模式优雅许多,调用方也做到了0修改,但正确地使用枚举策略模式需要额外考虑以下几点。
其实,大部分企业应用都没有使用core java,而是使用了开发框架,譬如Spring。 那么Spring下如何优雅的使用策略模式呢?
在Spring框架中实现策略模式通常涉及到使用Spring的依赖注入功能来动态地选择并注入不同的策略实现。以下是一个简单的示例,展示了如何在Spring框架中结合使用策略模式和依赖注入。
首先,定义策略接口和具体的策略实现类:
角色1: 抽象得到的策略接口
// 策略接口
public interface PaymentStrategy {
void processPayment(Order order);
}
角色2:具体策略
// 信用卡支付策略
@Component("creditCardPaymentStrategy")
public class CreditCardPaymentStrategy implements PaymentStrategy {
@Override
public void processPayment(Order order) {
System.out.println("Processing credit card payment for order: " + order);
// 实现信用卡支付逻辑
}
}
// 现金支付策略
@Component("cashPaymentStrategy")
public class CashPaymentStrategy implements PaymentStrategy {
@Override
public void processPayment(Order order) {
System.out.println("Processing cash payment for order: " + order);
// 实现现金支付逻辑
}
}
注意,我们使用@Component注解来标记这些类,这样Spring就能自动扫描并管理这些bean。同时,我们还为每个策略实现指定了一个唯一的bean名称,如creditCardPaymentStrategy和cashPaymentStrategy。
角色3:上下文 接下来,创建上下文类,它依赖于Spring注入的策略对象:
@Component
public class PaymentContext {
private final Map<String, PaymentStrategy> strategyMap;
@Autowired
public PaymentContext(List<PaymentStrategy> strategies) {
this.strategyMap = new ConcurrentHashMap<>();
for (PaymentStrategy strategy : strategies) {
// 假设每个策略实现都有一个独特的bean名称,作为键存储在map中
String beanName = strategy.getClass().getSimpleName();
strategyMap.put(beanName, strategy);
}
}
public PaymentStrategy getStrategy(String key) {
return strategyMap.get(key);
}
}
在上面的代码中,我们注入了一个PaymentStrategy的列表,并遍历这个列表来填充ConcurrentHashMap。我们假设每个策略bean的名称是唯一的,并且可以作为键来存储策略。在实际应用中,你可能需要一个更稳健的方式来确定每个策略的键。
接下来,在需要的地方注入PaymentContext并使用它来获取策略:
角色4:客户端
@Service
public class Client {
private final PaymentContext paymentContext;
@Autowired
public Client(PaymentContext paymentContext) {
this.paymentContext = paymentContext;
}
public void processOrder(Order order, String strategyKey) {
PaymentStrategy strategy = paymentContext.getStrategy(strategyKey);
if (strategy != null) {
strategy.processPayment(order);
} else {
// 处理策略未找到的情况
}
}
}
现在,PaymentContext是单例的,但是由于它内部使用了ConcurrentHashMap,因此可以安全地在并发环境下使用。每次调用getStrategy(key)方法时,都会从线程安全的map中检索策略。
确保你的Spring配置正确,并且所有的组件都被扫描和创建。如果你的策略bean有特定的命名规则或者你想使用自定义的键来标识策略,你可能需要在注入策略列表之前进行一些额外的配置或处理。
最后,需要注意的是,虽然ConcurrentHashMap确保了线程安全地访问策略映射,但每个策略实现本身也必须是线程安全的。如果策略实现包含共享的可变状态,那么你需要确保这些状态在并发访问时也是安全的。
Tips:
//直接把策略注入会有什么弊端
@Autowired
private Map<String, PaymentStrategy> strategyMap;
在Spring框架中,使用@Autowired注解直接注入一个Map<String, PaymentStrategy>类型的strategyMap,如上所示。
这样做确实可以自动装配所有实现了PaymentStrategy接口的bean到一个Map中,但这样的做法可能会带来一些潜在的风险和问题:
为了避免这些问题,你可以考虑以下替代方案:
服务定位器模式(Service Locator Pattern)是一种设计模式,它允许应用程序中的客户端代码通过单一的接口来访问服务或依赖项,而不是直接依赖于具体的服务实现。 Show the code:
import java.util.HashMap;
import java.util.Map;
public class PaymentStrategyLocator {
private static final Map<String, PaymentStrategy> strategyMap = new HashMap<>();
static {
strategyMap.put("default", new DefaultPaymentStrategy());
strategyMap.put("alternative", new AlternativePaymentStrategy());
// 可以在这里添加更多的策略实现
}
public static PaymentStrategy getStrategy(String key) {
return strategyMap.get(key);
}
}
总的来说,虽然自动装配在某些情况下可以简化代码,但也需要谨慎使用,确保它不会引入不必要的复杂性和风险。
一个优化项:
策略模式是一种强大的设计模式,它帮助我们在面对不同情况时能够灵活地切换算法或操作。通过了解和掌握不同的策略模式实现方式,你可以根据实际需求选择最合适的方案,从而提升你的设计能力,从白银段位进阶到黄金段位。