hello,大家好,我是聪聪。
策略模式是一种行为设计模式, 它能让你定义一系列算法, 并将每种算法分别放入独立的类中, 以使算法的对象能够相互替换。
日常开发中,对于需要考虑各类场景、各类分支通用逻辑时,就需要考虑是否可以将if-else
、switch
逻辑替换成不同策略算法进行单独处理,提高代码的可读性、可维护性,避免代码混乱熵增。
这里简单介绍一下lambda替换策略模式的方式:
Collections#sort()
的排序方法,使用何种排序策略来自于 java.util.Comparator#compare()
中定义。javax.servlet.http.HttpServlet#service()
方法, 还有所有接受 HttpServletRequest
和 HttpServletResponse
对象作为参数的 doXXX()方法。根据HttpServletRequest.getMethod
获取请求方式(GET、POST、PUT …),用以路由处理各类请求策略。如何识别是否是策略模式:
现在我们举个日常开发示例:
让我们设计一个算费系统,当中有根据不同计费策略计算得到应收手续费。
以下伪代码是最常的if-else
switch
分支逻辑:
private static BigDecimal calculate(BigDecimal amount, String pricingModel) {
//单笔固定收费1元
if ("fixed".equals(pricingModel)) {
return new BigDecimal("1.00");
}
//百分比收费 2%
if ("percent".equals(pricingModel)) {
return amount.multiply(new BigDecimal("0.02"));
}
//固定值+百分比, 1+2%
if ("fixedAndPercentage".equals(pricingModel)) {
return new BigDecimal("1.00").add(amount.multiply(new BigDecimal("0.02")));
}
throw new IllegalArgumentException("暂不支持计费模式:" + pricingModel);
}
public static void main(String[] args) {
BigDecimal amount = new BigDecimal("100");
//计算单笔固定收费
System.out.println(calculate(amount,"fixed"));
//计算百分比收费
System.out.println(calculate(amount,"percent"));
//计算固定值+百分比收费
System.out.println(calculate(amount,"fixedAndPercentage"));
}
上述逻辑已经根据计费模式pricingModel
来判断所属计算分支。看是仍然有以下弊端:
calculate
方法,新增对应分支逻辑,导致整体方法持续增长、每次修改就绪全量回归。hard coding
下面将上述场景进行重构。
首先看看策略模式应该是一个怎样的架构方式。
在这里我们需要做以下事情:
if-else
逻辑进行拆分,抽象出一个同样的计费策略接口:PricingStrategy
Context
那直接上代码看看。
//通用的计费策略接口
interface PricingStrategy {
BigDecimal calculate(BigDecimal amount);
}
//固定计费模式
class FixedPricingStrategy implements PricingStrategy {
@Override
public BigDecimal calculate(BigDecimal amount) {
return new BigDecimal("1.00");
}
}
//固定百分比计费
class PercentPricingStrategy implements PricingStrategy {
@Override
public BigDecimal calculate(BigDecimal amount) {
if (amount == null) {
return BigDecimal.ZERO;
}
return amount.multiply(new BigDecimal("0.02"));
}
}
//固定值+百分比计费
class FixedAndPercentagePricingStrategy implements PricingStrategy {
@Override
public BigDecimal calculate(BigDecimal amount) {
return new BigDecimal("1.00").add(amount.multiply(new BigDecimal("0.02")));
}
代码解释:
//执行上下文:
class Context {
public Context() {
}
//上下文会维护指向某个策略对象的引用
private PricingStrategy pricingStrategy;
// 上下文通常通过构造方法来设置策略
public Context(PricingStrategy pricingStrategy) {
this.pricingStrategy = pricingStrategy;
}
//也可以通过设置方法来切换策略
public void setPricingStrategy(PricingStrategy pricingStrategy) {
this.pricingStrategy = pricingStrategy;
}
// 上下文会将一些工作委派给策略对象,而不是自行实现不同版本的算法。
public BigDecimal executeStrategy(BigDecimal amount) {
return pricingStrategy.calculate(amount);
}
}
代码解释:
public class ClientMain {
@Test
void testConstructorCreation() {
//构造器方式指定策略
Context context = new Context(new FixedPricingStrategy());
BigDecimal fixedResult = context.executeStrategy(new BigDecimal("100"));
Assertions.assertEquals(fixedResult, new BigDecimal("1"));
//切换策略
context.setPricingStrategy(new PercentPricingStrategy());
BigDecimal percentResult = context.executeStrategy(new BigDecimal("100"));
Assertions.assertEquals(percentResult, new BigDecimal("2"));
}
@Test
void testSetMethodCreation() {
//set设置方法设置策略和切换
Context context = new Context();
//设置固定计费策略
context.setPricingStrategy(new FixedPricingStrategy());
BigDecimal fixedResult = context.executeStrategy(new BigDecimal("100"));
Assertions.assertEquals(fixedResult, new BigDecimal("1.00"));
//设置百分比计费策略
context.setPricingStrategy(new PercentPricingStrategy());
BigDecimal percentResult = context.executeStrategy(new BigDecimal("100"));
Assertions.assertEquals(percentResult, new BigDecimal("2.00"));
//设置固定值+百分比计费策略
context.setPricingStrategy(new FixedAndPercentagePricingStrategy());
BigDecimal fixedAndPercentResult = context.executeStrategy(new BigDecimal("100"));
Assertions.assertEquals(fixedAndPercentResult, new BigDecimal("3.00"));
}
@ParameterizedTest
@MethodSource(value = "allStrategy")
void testRouteStrategy(String strategy) {
Context context = new Context();
if ("fixed".equals(strategy)) {
context.setPricingStrategy(new FixedPricingStrategy());
}
if ("percent".equals(strategy)) {
context.setPricingStrategy(new PercentPricingStrategy());
}
if ("fixedAndPercentage".equals(strategy)) {
context.setPricingStrategy(new FixedAndPercentagePricingStrategy());
}
BigDecimal result = context.executeStrategy(new BigDecimal("100"));
//打印结果
}
static Object[] allStrategy() {
return new Object[]{"fixed", "percent", "fixedAndPercentage"};
}
}
上述测试方法很清晰:
testConstructorCreation
通过构造器方式生成上下文,设定具体策略实现,同样后续可以通过set方法进行切换策略算法。testSetMethodCreation
初始化空构造器方式生成上下文,通过set方法进行切换设置策略算法。routeStrategy
初始化空构造器方式生成上下文,通过外部所传策略方法,进行路由具体策略算法实现。上述策略模式重构相比于if-else
switch
等分支逻辑已经结构化,相互解耦,减少维护成本。总结有以下优点:
有有点,当然也有缺点:
策略算法较少时,没必要引入新的类、接口,增加程序复杂度。
客户端必须知道所有的策略实现类,才能够通过上下文进行设置选择合适的策略。
那么,我们可以通过工厂模式+策略模式进行结构化、抽象此类策略算法的结构。在扩展中进行演示。
上述策略模式的重构仍然可以有优化的余地:
那我们直接上代码:
public enum StrategyEnum {
FIXED,
PERCENT,
FIXED_AND_PERCENTAGE;
StrategyEnum() {
}
}
在该枚举中定义了所有策略类型,后续新增策略算法是进行扩展该枚举即可。
public interface PricingStrategy {
BigDecimal calculate(BigDecimal amount);
//每次一个策略接口实现均会实现该接口 用以标记策略实现的具体类型
String getServiceCode();
}
修改策略接口,增加getServiceCode()
方法。
//固定计费模式
public class FixedPricingStrategy implements PricingStrategy {
@Override
public BigDecimal calculate(BigDecimal amount) {
return new BigDecimal("1.00");
}
@Override
public String getServiceCode() {
return StrategyEnum.FIXED.name();
}
}
//固定百分比计费
class PercentPricingStrategy implements PricingStrategy {
@Override
public BigDecimal calculate(BigDecimal amount) {
if (amount == null) {
return BigDecimal.ZERO;
}
return amount.multiply(new BigDecimal("0.02"));
}
@Override
public String getServiceCode() {
return StrategyEnum.PERCENT.name();
}
}
策略实现类中均实现了getServiceCode()
接口,用以返回该策略实现类所属类型或服务编码,后续可以通过工厂模式进行生产获取该类型策略。
@Slf4j
public class PricingStrategyFactory {
public PricingStrategyFactory(List<PricingStrategy> list) {
init(list);
}
private final Map<String, PricingStrategy> pricingStrategyMap = new HashMap<>();
private void init(List<PricingStrategy> strategyList) {
if (strategyList == null) {
return;
}
for (PricingStrategy strategy : strategyList) {
String serviceCode = strategy.getServiceCode();
if (serviceCode == null) {
throw new IllegalArgumentException(String.format("Registration service code cannot be empty:%s",
strategy.getClass()));
}
if (!pricingStrategyMap.containsKey(serviceCode)) {
pricingStrategyMap.put(serviceCode, strategy);
log.info("Registration service: {} , {}", serviceCode, strategy.getClass());
} else {
throw new IllegalArgumentException(String.format("Duplicate registration service: %s , %s , %s",
serviceCode, pricingStrategyMap.get(serviceCode).getClass(), strategy.getClass()));
}
}
}
public PricingStrategy getService(String serviceCode) {
return pricingStrategyMap.get(serviceCode);
}
}
这里我们将所有计费策略实现全部存储在一个Map中进行初始化。
通过策略接口中getServiceCode() 作为key进行存储,后续通过getService(String serviceCode)
即可获得对应的策略接口实现。
public class TestStrategyFactory {
private PricingStrategyFactory pricingStrategyFactory;
@BeforeEach
void init() {
List<PricingStrategy> strategyList = initPricingStrategy();
pricingStrategyFactory = new PricingStrategyFactory(strategyList);
}
@Test
void testFixedCalculate() {
PricingStrategy strategy = pricingStrategyFactory.getService(StrategyEnum.FIXED.name());
BigDecimal fixedResult = strategy.calculate(new BigDecimal("100"));
Assertions.assertEquals(fixedResult, new BigDecimal("1.00"));
}
@Test
void testPercentCalculate() {
PricingStrategy strategy = pricingStrategyFactory.getService(StrategyEnum.PERCENT.name());
BigDecimal percentResult = strategy.calculate(new BigDecimal("100"));
Assertions.assertEquals(percentResult, new BigDecimal("2.00"));
}
// 这里初始化所有PricingStrategy 接口的所有实现。
// 如果你是通过spring管理,直接通过@Autowrited即可注入得到List
static List<PricingStrategy> initPricingStrategy() {
return Arrays.asList(new FixedPricingStrategy(), new PercentPricingStrategy());
}
}
上面使用方式就不过多解释了。注意初始化PricingStrategyFactory
的方式,此处只有一个构造器,需要注入策略接口的所有实现。
如果你是用的是spring进行管理,那么直接可以通过@Autowired
的方式即可将所有接口实现进入注入,得到一个List
。
下面看看执行启动日志,可以很清晰的看到目前应用已注册多种策略:
Registration service: FIXED , class cc.ccoder.designpatterns.strategy.refactor.FixedPricingStrategy
Registration service: PERCENT , class cc.ccoder.designpatterns.strategy.refactor.PercentPricingStrategy
当然如果你有多个平行的策略时,都需要这样创建一个工厂,岂不是重复的逻辑又增加了。
是否有一种优雅的方式进行工厂方法复用呢?
上述将策略接口实现类放入Map中、从Map中通过serviceCode获取对应策略接口实现的逻辑应该是一致的,我们便可将其进行抽象出来。
以上便是我们的需求,那么接下来就开始编码看看。
public interface CodeService {
/**
* 服务编码必须唯一
*
* @return 服务编码
*/
String getServiceCode();
}
这是一个顶层接口,后续所有策略工厂模式接口均需要实现该接口,根据策略枚举内容进行返回。
public interface CodeServiceFactory<Provider extends CodeService> {
/**
* 获取服务
*
* @param serviceCode 服务编码
* @return 返回服务,不存在时返回null
*/
Provider getService(String serviceCode);
}
这里限定了后续所有的策略提供者Provider
均需要继承于CodeService
接口。
public abstract class AbstractCodeServiceFactory<Provider extends CodeService>
implements CodeServiceFactory<Provider> {
private static final Logger log = LoggerFactory.getLogger(AbstractCodeServiceFactory.class);
private final Map<String, Provider> serviceProviderMap = new HashMap<>();
protected AbstractCodeServiceFactory(List<Provider> providers) {
initializeProviderMap(providers);
}
/**
* Initialize Factory Service
*
* @param providers 服务接口
*/
private void initializeProviderMap(List<Provider> providers) {
log.info("Initialize Factory Service:{}", getFactoryName());
if (providers == null) {
return;
}
for (Provider provider : providers) {
String serviceCode = provider.getServiceCode();
if (serviceCode == null) {
throw new IllegalArgumentException(
String.format("Registration service code cannot be empty :%s", provider.getClass()));
}
if (!serviceProviderMap.containsKey(serviceCode)) {
serviceProviderMap.put(serviceCode, provider);
log.info("Registered service: {}, {}", serviceCode, provider.getClass());
} else {
throw new IllegalArgumentException(String.format("Duplicate registration service: %s, %s, %s",
serviceCode, serviceProviderMap.get(serviceCode).getClass(), provider.getClass()));
}
}
}
/**
* 获取服务 服务接口不存在时返回null
*
* @param serviceCode 服务编码
* @return 服务接口c
*/
@Override
public Provider getService(String serviceCode) {
return serviceProviderMap.get(serviceCode);
}
/**
* 服务工厂名称
*
* @return 工厂名称用于日志
*/
protected abstract String getFactoryName();
}
将上述逻辑抽象到一个抽象工厂中,后续策略工厂便可通用。
getService()
方法,获取serviceCode
对应的策略接口提供者。getFactoryName()
方法,底层工厂方法需实现,用以区分各个策略工厂。那么问题来了?
如何使用这样一个抽象工厂+策略模式的组合呢?
那么现在我们有既定的计费策略需要实现,同时还增加了根据支付方式不同路由到不同的渠道进行支付的场景。
就需要有以下两个策略场景
public class PricingStrategyFactory extends AbstractCodeServiceFactory<PricingStrategy> {
public PricingStrategyFactory(List<PricingStrategy> pricingStrategies) {
super(pricingStrategies);
}
@Override
protected String getFactoryName() {
return "计费策略工厂";
}
}
实现顶层抽象工厂模板即可。
构造方式初始化所有该策略算法的实现。
如果你是spring管理,直接将该工厂进行@Component
管理即可使用。
public class FundServiceFactory extends AbstractCodeServiceFactory<FundService> {
public FundServiceFactory(List<FundService> fundServiceList) {
super(fundServiceList);
}
@Override
protected String getFactoryName() {
return "资金支付工厂";
}
}
public class TestAbstractStrategyFactory {
private FundServiceFactory fundServiceFactory;
private PricingStrategyFactory pricingStrategyFactory;
@BeforeEach
void init() {
fundServiceFactory = new FundServiceFactory(initFundService());
pricingStrategyFactory = new PricingStrategyFactory(initPricingStrategy());
}
@Test
void testFixedCalculate() {
PricingStrategy strategy = pricingStrategyFactory.getService(StrategyEnum.FIXED.name());
BigDecimal fixedResult = strategy.calculate(new BigDecimal("100"));
Assertions.assertEquals(fixedResult, new BigDecimal("1.00"));
}
@Test
void testPercentCalculate() {
PricingStrategy strategy = pricingStrategyFactory.getService(StrategyEnum.PERCENT.name());
BigDecimal percentResult = strategy.calculate(new BigDecimal("100"));
Assertions.assertEquals(percentResult, new BigDecimal("2.00"));
}
@Test
void testFundServiceAlipay() {
FundService service = fundServiceFactory.getService(ThirdPayChannel.ALIPAY.name());
String result = service.pay("pay parameters");
Assertions.assertTrue(result.startsWith(ThirdPayChannel.ALIPAY.name()));
}
@Test
void testFundServiceWeChat() {
FundService service = fundServiceFactory.getService(ThirdPayChannel.WECHAT.name());
String result = service.pay("pay parameters");
Assertions.assertTrue(result.startsWith(ThirdPayChannel.WECHAT.name()));
}
// 这里初始化所有PricingStrategy 接口的所有实现。
// 如果你是通过spring管理,直接通过@Autowrited即可注入得到List
static List<PricingStrategy> initPricingStrategy() {
return Arrays.asList(new FixedPricingStrategy(), new PercentPricingStrategy());
}
//如上所示
static List<FundService> initFundService() {
return Arrays.asList(new WeChatFundService(), new AliPayFundService());
}
}
通过设计模式中各类模式的组装、演变可以让我们在系统设计、代码架构方面能力得到提升。
切记:
不可有了锤子,遍地都是钉子。
设计模式二三事情:https://tech.meituan.com/2022/03/10/interesting-talk-about-design-patterns.html
[2] 弗里曼. Head First 设计模式 [M]. 中国电力出版社, 2007.
最后的最后。
以上所有涉及源码均会在GitHub进行同步更新。欢迎follow and star。
https://github.com/ccoderJava/designpatterns
全文完。
了解更多内容,可以关注我的微信公众号,更多首发文章。