1. 背景

1.1. 策略模式回顾

Strategy Pattern in UML

策略模式是行为型设计模式的一种,UML图如上所示,Context 持有 Strategy 接口引用,实际调用时可以是 ConcreteStrategyA 也可以是 ConcreteStrategyB

策略模式与Spring Framework中的依赖注入类似,都是面向接口编程,实际使用时可以注入接口的任意实现,这种方法符合面向对象设计中的开闭原则、里氏替换原则和依赖倒转原则

1.2. 策略模式的问题

Strategy 有多个实现,那么 Context 到底该使用哪个,策略模式中一般是使用一个配置文件配置一个具体实现的全类名,这意味着将决定权交给开发者,也意味着多个 Strategy 的实现无法同时使用

然而实际业务场景中我们期望根据上下文参数或者请求参数动态选择 Strategy 的实现处理相关业务逻辑(伪代码如下),因此考虑对策略模式进行增强,下面结合一个实例描述整个优化过程

if(conditionA) {
    concreteStrategyA.execute();
} else (conditionB) {
    concreteStrategyB.execute();
}

2. 实例与分析

需求: 某厂商计算开发一个二元计算器,厂商默认支持加减乘除运算,同时允许用户扩展其它运算

实际业务场景中业务逻辑会比加减乘除复杂的多,但不影响整体模式设计

2.1. 枚举实现

加减乘除运算通常用来作为枚举抽象方法的示例如下

public enum BinaryOperation {

    PLUS("+") {
        @Override
        public double apply(double x, double y) {
            return x + y;
        }
    };
    // 省略减、乘、除

    private String symbol;

    BinaryOperation(String symbol) {
        this.symbol = symbol;
    }

    public abstract double apply(double x, double y);
}

使用枚举能够满足默认的加减乘除运算,但显然用户无法扩展其它运算

2.2. 接口代替枚举

为了允许用户扩展,定义一个二元运算接口以及默认的加减乘除实现

public interface BinaryOperation {

    double apply(double x, double y);
}

/**
* 加法实现,减乘除省略
*/
public class PlusOperation implements BinaryOperation {
    @Override
    public double apply(double x, double y) {
        return x + y;
    }
}

xy 和要执行的运算封装在一个实体类 CalculateRequest 中并在计算器 Calculator 中根据不同计算类型选择 BinaryOperation 的不同实现

/**
* 计算请求
*/
public class CalculateRequest {

    private String symbol;

    private double x;

    private double y;

    // getter, setter

}

/**
* 计算器
*/
public class Calculator {

    private final BinaryOperation PLUS = new PlusOperation();
    private final BinaryOperation MINUS = new MinusOperation();
    private final BinaryOperation MULTIPLY = new MultiplyOperation();
    private final BinaryOperation DIVIDE = new DivideOperation();

    public double calculate(CalculateRequest calculateRequest) {
        String symbol = calculateRequest.getSymbol();
        double x = calculateRequest.getX();
        double y = calculateRequest.getY();
        if(symbol.equals("+")) {
            return PLUS.apply(x, y);
        } else if(symbol.equals("-")) {
            return MINUS.apply(x, y);
        } else if(symbol.equals("*")) {
            return MULTIPLY.apply(x, y);
        } else if(symbol.equals("/")) {
            return DIVIDE.apply(x, y);
        } else {
            throw new IllegalArgumentException(symbol);
        }
    }
}

将整个判断过程都在 Calculator 中实现显然不是好的实践,每次新增一类运算都需要修改 Calculator ,这违背了面向对象设计的开闭原则, 而且该业务场景中运算是允许用户自行实现的,Calculator 中根本无法了解用户自行实现的运算

2.3. 提取判断逻辑到接口

针对上一节的问题可以将运算符判断的过程提取到 BinaryOperation 中,使用 supports() 方法判断是否支持特定计算请求,并重构原本的 apply() 方法,使用 CalculateRequest 作为方法参数

public interface BinaryOperation {

    boolean supports(CalculateRequest calculateRequest);

    double apply(CalculateRequest calculateRequest);
}

/**
* 加法实现,减乘除省略
*/
public class PlusOperation implements BinaryOperation {

    @Override
    public boolean supports(CalculateRequest calculateRequest) {
        return "+".equals(calculateRequest.getSymbol());
    }

    @Override
    public double apply(CalculateRequest calculateRequest) {
        return calculateRequest.getX() + calculateRequest.getY();
    }

}

Calculator 中使用一个集合保存多个 BinaryOperation 的实现,计算时遍历选择 supports() 方法返回true的实现,并提供一个 addOperation() 方法允许向集合中添加新的操作

public class Calculator {

    private final List<BinaryOperation> binaryOperations = new ArrayList<>();

    public Calculator() {
        binaryOperations.add(new PlusOperation());
        binaryOperations.add(new MinusOperation());
        binaryOperations.add(new MultiplyOperation());
        binaryOperations.add(new DivideOperation());
    }

    public void addOperation(BinaryOperation binaryOperation) {
        binaryOperations.add(binaryOperation);
    }

    public double calculate(CalculateRequest calculateRequest) {
        for(BinaryOperation binaryOperation : binaryOperations) {
            if(binaryOperation.supports(calculateRequest)) {
                return binaryOperation.apply(calculateRequest);
            }
        }
        throw new IllegalArgumentException(calculateRequest.getSymbol());
    }
}

到此为止,我们的业务需求实际上已经实现了,并且 BinaryOperation 也已经展现了本文希望说明的高级策略模式,然而还存在优化空间

2.4. 缩小访问范围

上一节 BinaryOperation 的多个实现 PlusOperationMinusOperation 都声明了 public ,允许所有类直接访问,实际上这是没必要的, 不符合权限最小化的原则,jdk 1.8提供的接口静态方法可以对此进行优化,将 PlusOperationMinusOperation 都改为默认包级别的访问, 并在 BinaryOperation 接口中提供静态方法返回对应运算的实例

class PlusOperation implements BinaryOperation {

    //...
}

public interface BinaryOperation {

    boolean supports(CalculateRequest calculateRequest);

    double apply(CalculateRequest calculateRequest);

    static BinaryOperation plusOperation() {
        return new PlusOperation();
    }

    // 减乘除省略
}

public class Calculator {

    private final List<BinaryOperation> binaryOperations = new ArrayList<>();

    public Calculator() {
        binaryOperations.add(BinaryOperation.plusOperation());
        binaryOperations.add(BinaryOperation.minusOperation());
        binaryOperations.add(BinaryOperation.miltiplyOperation());
        binaryOperations.add(BinaryOperation.divideOperation());
    }

    //...
}

2.5. 使用组合模式减少重复代码

本示例使用 Calculator 封装了多个 BinaryOperator 实现各类型运算,那么如果有另一个客户端类也希望使用 BinaryOperator 及其实现呢,它也需要使用一个集合属性添加所有 BinaryOperator 的实现,使用时不断遍历选择一个实现

显然,添加默认实现以及遍历选择的代码都属于重复代码可以再次进行封装,一种方式是将它们封装在一个工具类中,然后更好的是使用组合模式

public class CompositeBinaryOperation implements BinaryOperation {

    private Collection<BinaryOperation> binaryOperations;

    public CompositeBinaryOperation() {
        binaryOperations = new ArrayList<>();
        binaryOperations.add(BinaryOperation.plusOperation());
        binaryOperations.add(BinaryOperation.minusOperation());
        binaryOperations.add(BinaryOperation.multiplyOperation());
        binaryOperations.add(BinaryOperation.divideOperation());
    }

    public void addOperation(BinaryOperation binaryOperation) {
        binaryOperations.add(binaryOperation);
    }

    @Override
    public boolean supports(CalculateRequest calculateRequest) {
        return binaryOperations.stream().anyMatch(op -> op.supports(calculateRequest));
    }

    @Override
    public double apply(CalculateRequest calculateRequest) {
        return binaryOperations.stream()
                            .filter(op -> op.supports(calculateRequest))
                            .findFirst()
                            .orElseThrow(IllegalArgumentException::new)
                            .apply(calculateRequest);
    }
}

客户端代码中可以直接使用 new 创建 CompositeBinaryOperation ,不过更好的方式是结合Spring Framework使用, 将 CompositeBinaryOperation 声明为一个Bean注入到客户端代码中,这样做的好处是客户端代码仍然可以面向接口 BinaryOperation 开发,遵循里氏代换原则和依赖倒转原则

@Configuration
public class OperationConfig {

    @Bean
    public BinaryOperation compositeBinaryOperation() {
        CompositeBinaryOperation compositeBinaryOperation = new CompositeBinaryOperation();
        //添加其它运算符
        compositeBinaryOperation.addOperation(new CustomOperation());
        return compositeBinaryOperation;
    }
}

@Service
class Calculator {

    private final BinaryOperation binaryOperation;

    public Calculator(BinaryOperation binaryOperation) {
        this.binaryOperation = binaryOperation;
    }

    public double calculate(CalculateRequest calculateRequest) throws OperationNotSupportedException {
        return binaryOperation.apply(calculateRequest);
    }
}

2.6. 自动组合

上一节使用组合模式时手动为每一个实现类创建实例并添加到 CompositeBinaryOperation 中,更好的方式是使用Spring的自动注入功能自动将所有实现类的实例添加到 CompositeBinaryOperation 中,后续新增其它实现时只需要添加其对应的Bean即可,这样更加符合 面向新增开放面向修改关闭 的原则,要注意的是此时Spring容器中存在多个 BinaryOperation 类型的Bean,因此在 compositeBinaryOperation 上添加 @Primary 注解表示自动装配时优先使用它

@Configuration
public class OperationConfig {

    @Bean
    public BinaryOperation plusOperation(){
        return new PlusOperation();
    }

    @Bean
    public BinaryOperation minusOperation(){
        return new MinusOperation();
    }

    @Bean
    @Primary
    public BinaryOperation compositeBinaryOperation(List<BinaryOperation> binaryOperations) {
        return new CompositeBinaryOperation(binaryOperations);
    }
}

2.7. 遍历顺序

上面几节使用集合保存了多个 BinaryOperation 的实现使用时进行遍历,某些场景下还需要控制遍历顺序,此时考虑 BinaryOperation 继承Spring Framework提供的 Ordered 接口,遍历时按照不同实现的顺序进行遍历

public interface BinaryOperation extends Ordered {

    boolean supports(CalculateRequest calculateRequest);

    double apply(CalculateRequest calculateRequest);

}

3. 模式结构

通过上面的示例可以总结高级策略模式主要是在原有策略模式接口上新增一个 supports() 方法,接口的实现类在 supports() 方法中添加判断逻辑,supports() 方法返回true时表示此实现可以处理当前请求

上下文类 Context 以集合或者组合模式的方式持有多个策略接口的实现,执行业务操作时选择 supports() 方法返回true的实现

advanced strategy pattern

4. Spring中的高级策略模式

Spring中也大量使用了高级策略模式,例如

Example 1. org.springframework.validation.Validator
public interface Validator {

	boolean supports(Class<?> clazz);

	void validate(Object target, Errors errors);

}
Example 2. org.springframework.web.method.support.HandlerMethodArgumentResolver
public interface HandlerMethodArgumentResolver {

	boolean supportsParameter(MethodParameter parameter);

	@Nullable
	Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;

}

5. Spring中简化的高级策略模式

execute() 方法有返回值时,可以使用它的返回值判断来代替 supports() 方法,典型的示例是SpringMVC中的 org.springframework.web.servlet.HandlerMapping

package org.springframework.web.servlet;

public interface HandlerMapping {

    /**
    * Returns null if no match was found. This is not an error. The DispatcherServlet will query all registered HandlerMapping beans to find a match, and only decide there is an error if none can find a handler.
    * @param request - current HTTP request
    * @return a HandlerExecutionChain instance containing handler object and any interceptors, or null if no mapping found
    * @throws Exception - if there is an internal error
    */
    @Nullable
	HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;     (1)

}
  1. 注意方法添加了 @Nullable 注解表示它的返回值可能是null,并且在注释中说明了null返回值的意义,这些不是必须的,但却是最佳实践

HandlerMapping 的调用者需要判断 getHandler 的返回值,非空则返回

package org.springframework.web.servlet;

public class DispatcherServlet extends FrameworkServlet {

	@Nullable
	private List<HandlerMapping> handlerMappings;

	@Nullable
	protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
		if (this.handlerMappings != null) {
			for (HandlerMapping mapping : this.handlerMappings) {
				HandlerExecutionChain handler = mapping.getHandler(request);
				if (handler != null) {
					return handler;
				}
			}
		}
		return null;
	}
}