策略模式实践:干掉 if-else 地狱
2025/8/10非最佳实践#Java#设计模式
策略模式实践:干掉 if-else 地狱
背景
最近在维护一个数据查询服务,发现一个让人头疼的方法:
public List<Map<String, Object>> getResultFromDb(Map<String, Object> detail, String sql) {
String dbs = (String) detail.get("dbs");
List<Map<String, Object>> resultList;
if ("mysql".equals(dbs)) {
resultList = mysqlTemplate.queryForList(sql);
} else if ("postgresql".equals(dbs)) {
resultList = postgresTemplate.queryForList(sql);
} else if ("mongodb".equals(dbs)) {
// MongoDB需要特殊处理
sql = convertToMongoQuery(sql);
resultList = mongoTemplate.find(sql);
} else if ("elasticsearch".equals(dbs)) {
resultList = esClient.search(sql);
} else if ("redis".equals(dbs)) {
resultList = redisTemplate.get(sql);
} else if ("cassandra".equals(dbs)) {
// Cassandra需要建立session
CqlSession session = buildSession();
resultList = session.execute(sql);
} else if ("hive".equals(dbs)) {
resultList = hiveTemplate.queryForList(sql);
} else if ("clickhouse".equals(dbs)) {
// ClickHouse需要处理特殊语法
sql = sql.replace("/*", "/*!");
resultList = clickhouseTemplate.queryForList(sql);
}
// ... 还有10多个else if
else {
resultList = defaultTemplate.queryForList(sql);
}
return resultList;
}
看到这段代码的第一反应:这不就是教科书级别的策略模式使用场景吗?
策略模式重构
1. 定义策略接口
public interface DatabaseQueryStrategy {
/**
* 执行查询
*/
List<Map<String, Object>> executeQuery(String sql, Map<String, Object> context);
/**
* 是否支持该数据库类型
*/
boolean supports(String dbType);
}
2. 实现具体策略
先来个抽象基类,处理通用逻辑:
@Slf4j
public abstract class AbstractDatabaseQueryStrategy implements DatabaseQueryStrategy {
protected final String dbType;
protected AbstractDatabaseQueryStrategy(String dbType) {
this.dbType = dbType;
}
@Override
public boolean supports(String dbType) {
return this.dbType.equals(dbType);
}
// 通用的SQL预处理
protected String preprocessSql(String sql) {
return sql;
}
// 统一的日志记录
protected void logQuery(String sql) {
log.info("[{}] executing: {}", dbType, sql);
}
}
然后各种数据库实现自己的策略:
// MySQL策略 - 最简单的实现
@Component
public class MysqlQueryStrategy extends AbstractDatabaseQueryStrategy {
@Autowired
private JdbcTemplate mysqlTemplate;
public MysqlQueryStrategy() {
super("mysql");
}
@Override
public List<Map<String, Object>> executeQuery(String sql, Map<String, Object> context) {
logQuery(sql);
return mysqlTemplate.queryForList(sql);
}
}
// MongoDB策略 - 需要SQL转换
@Component
public class MongoQueryStrategy extends AbstractDatabaseQueryStrategy {
@Autowired
private MongoTemplate mongoTemplate;
public MongoQueryStrategy() {
super("mongodb");
}
@Override
protected String preprocessSql(String sql) {
// 将SQL转换为MongoDB查询
return SqlToMongoConverter.convert(sql);
}
@Override
public List<Map<String, Object>> executeQuery(String sql, Map<String, Object> context) {
String mongoQuery = preprocessSql(sql);
logQuery(mongoQuery);
return mongoTemplate.find(mongoQuery);
}
}
// Cassandra策略 - 需要Session管理
@Component
public class CassandraQueryStrategy extends AbstractDatabaseQueryStrategy {
@Value("${cassandra.hosts}")
private String hosts;
@Value("${cassandra.keyspace}")
private String keyspace;
public CassandraQueryStrategy() {
super("cassandra");
}
@Override
public List<Map<String, Object>> executeQuery(String sql, Map<String, Object> context) {
logQuery(sql);
// 使用try-with-resources自动管理session
try (CqlSession session = CqlSession.builder()
.addContactPoints(parseHosts())
.withKeyspace(keyspace)
.build()) {
ResultSet rs = session.execute(sql);
return convertResultSet(rs);
}
}
private List<InetSocketAddress> parseHosts() {
// 解析hosts配置
return Arrays.stream(hosts.split(","))
.map(h -> new InetSocketAddress(h, 9042))
.collect(Collectors.toList());
}
}
3. 策略工厂
使用 Spring 的依赖注入,自动收集所有策略:
@Component
@Slf4j
public class DatabaseStrategyFactory {
private final Map<String, DatabaseQueryStrategy> strategyMap = new HashMap<>();
private DatabaseQueryStrategy defaultStrategy;
@Autowired
public DatabaseStrategyFactory(List<DatabaseQueryStrategy> strategies) {
// Spring会自动注入所有实现了DatabaseQueryStrategy的Bean
for (DatabaseQueryStrategy strategy : strategies) {
// 这里偷个懒,假设每个策略只支持一种数据库类型
String dbType = strategy.getClass().getSimpleName()
.replace("QueryStrategy", "")
.toLowerCase();
strategyMap.put(dbType, strategy);
}
// 设置默认策略
this.defaultStrategy = strategyMap.get("mysql");
log.info("Loaded {} database strategies", strategyMap.size());
}
public DatabaseQueryStrategy getStrategy(String dbType) {
return strategyMap.getOrDefault(dbType, defaultStrategy);
}
}
4. 重构后的查询方法
@Service
public class DataQueryService {
@Autowired
private DatabaseStrategyFactory strategyFactory;
public List<Map<String, Object>> getResultFromDb(Map<String, Object> detail, String sql) {
String dbType = (String) detail.getOrDefault("dbs", "mysql");
// 一行代码搞定!
return strategyFactory.getStrategy(dbType)
.executeQuery(sql, detail);
}
}
进一步优化
1. 策略缓存
如果策略对象创建成本高,可以加个缓存:
@Component
public class CachedDatabaseStrategyFactory {
private final Map<String, DatabaseQueryStrategy> cache = new ConcurrentHashMap<>();
@Autowired
private ApplicationContext context;
public DatabaseQueryStrategy getStrategy(String dbType) {
return cache.computeIfAbsent(dbType, this::createStrategy);
}
private DatabaseQueryStrategy createStrategy(String dbType) {
// 根据dbType动态创建策略
String beanName = dbType + "QueryStrategy";
return context.getBean(beanName, DatabaseQueryStrategy.class);
}
}
2. 责任链模式结合
有时候一个查询可能需要多个策略协作:
public abstract class ChainedQueryStrategy implements DatabaseQueryStrategy {
private DatabaseQueryStrategy next;
public ChainedQueryStrategy setNext(DatabaseQueryStrategy next) {
this.next = next;
return next;
}
protected List<Map<String, Object>> doNext(String sql, Map<String, Object> context) {
if (next != null) {
return next.executeQuery(sql, context);
}
return Collections.emptyList();
}
}
// 缓存策略
@Component
public class CacheQueryStrategy extends ChainedQueryStrategy {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public List<Map<String, Object>> executeQuery(String sql, Map<String, Object> context) {
String cacheKey = DigestUtils.md5Hex(sql);
// 先查缓存
Object cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
log.info("Cache hit for query");
return (List<Map<String, Object>>) cached;
}
// 缓存未命中,执行下一个策略
List<Map<String, Object>> result = doNext(sql, context);
// 写入缓存
redisTemplate.opsForValue().set(cacheKey, result, 5, TimeUnit.MINUTES);
return result;
}
@Override
public boolean supports(String dbType) {
// 缓存策略对所有类型都支持
return true;
}
}
3. 动态策略加载
通过配置文件动态加载策略:
database:
strategies:
mysql:
class: com.example.MysqlQueryStrategy
properties:
maxPoolSize: 10
mongodb:
class: com.example.MongoQueryStrategy
properties:
connectionString: mongodb://localhost:27017
custom:
class: com.example.CustomQueryStrategy
enabled: true
@Component
@ConfigurationProperties(prefix = "database")
public class DynamicStrategyLoader {
private Map<String, StrategyConfig> strategies;
@PostConstruct
public void loadStrategies() {
strategies.forEach((name, config) -> {
if (config.isEnabled()) {
Class<?> strategyClass = Class.forName(config.getClassName());
DatabaseQueryStrategy strategy = (DatabaseQueryStrategy)
strategyClass.getDeclaredConstructor().newInstance();
// 注册到Spring容器
registerBean(name + "Strategy", strategy);
}
});
}
}
性能对比
做了个简单的 JMH 基准测试:
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
public class QueryBenchmark {
@Benchmark
public void testIfElse(QueryState state) {
state.oldService.getResultFromDb(state.detail, state.sql);
}
@Benchmark
public void testStrategy(QueryState state) {
state.newService.getResultFromDb(state.detail, state.sql);
}
}
结果:
- if-else 版本:约 1,200,000 ops/s
- 策略模式版本:约 1,180,000 ops/s
性能损失几乎可以忽略(< 2%),但代码可维护性提升巨大。
适用场景思考
策略模式特别适合以下场景:
- 多种算法切换:比如不同的加密算法、压缩算法
- 多数据源处理:像我们这个案例
- 支付方式选择:支付宝、微信、银行卡等
- 消息发送渠道:短信、邮件、推送等
- 导出格式选择:Excel、PDF、CSV 等
// 导出策略示例
public interface ExportStrategy {
void export(List<Data> data, OutputStream output);
}
@Component
public class ExcelExportStrategy implements ExportStrategy {
public void export(List<Data> data, OutputStream output) {
// Apache POI处理Excel导出
}
}
@Component
public class PdfExportStrategy implements ExportStrategy {
public void export(List<Data> data, OutputStream output) {
// iText处理PDF导出
}
}
踩坑记录
-
Spring Bean 命名冲突:多个策略类如果不指定 Bean 名称,可能会冲突
@Component("mysqlStrategy") // 指定名称避免冲突 public class MysqlQueryStrategy { } -
策略初始化顺序:如果策略之间有依赖,需要用
@DependsOn或@Order控制顺序 -
线程安全问题:策略类最好设计成无状态的,避免并发问题
-
策略过多导致类爆炸:可以考虑用枚举+lambda 简化:
public enum DbStrategy { MYSQL(sql -> mysqlTemplate.queryForList(sql)), POSTGRES(sql -> postgresTemplate.queryForList(sql)); private final Function<String, List> executor; }
其他设计模式思考
模板方法 vs 策略模式
// 模板方法 - 继承关系,算法骨架固定
public abstract class DataProcessor {
public final void process() {
connect();
query();
disconnect();
}
protected abstract void connect();
protected abstract void query();
protected abstract void disconnect();
}
// 策略模式 - 组合关系,算法可替换
public class QueryExecutor {
private QueryStrategy strategy;
public void execute() {
strategy.query();
}
}
我们选择策略模式是因为:
- 需要运行时动态切换
- 避免继承带来的耦合
- 更符合开闭原则
工厂模式的必要性
有人可能会问:直接用 Map 存储策略不就行了,为什么还要工厂?
// 简单Map方式
Map<String, DatabaseQueryStrategy> strategies = new HashMap<>();
// 工厂模式
DatabaseStrategyFactory factory = new DatabaseStrategyFactory();
工厂模式的优势:
- 封装创建逻辑,可以加入复杂的初始化
- 统一管理策略生命周期
- 便于添加缓存、监控等横切关注点
- 更好的测试性
实战案例二:模块服务路由
除了数据库查询,还遇到了另一个类似的场景:
// 原始代码:80+行的switch-case地狱
public List<Map<String, Object>> queryDb(Map<String, Object> parameter,
Map<String, Object> queryDetail,
boolean isQueryDb) {
String module = (String) parameter.get("module");
switch (module) {
case "vod":
return vodService.getSqlAndQuery(parameter, queryDetail, isQueryDb);
case "error":
return errorService.getSqlAndQuery(parameter, queryDetail, isQueryDb);
case "iptv":
return iptvService.getSqlAndQuery(parameter, queryDetail, isQueryDb);
// ... 25+ more cases
default:
return defaultService.query(parameter, queryDetail, isQueryDb);
}
}
注册模式 + 注解扫描
这次用注册模式(Registry Pattern)配合注解:
// 1. 定义模块服务注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ModuleService {
String[] modules();
boolean isDefault() default false;
}
// 2. 服务类标注支持的模块
@Service
@ModuleService(modules = {"vod", "common", "app_error"})
public class VodService extends BaseService {
// 业务逻辑不变
}
// 3. 自动扫描注册中心
@Component
public class ModuleRegistry {
@Autowired
private ApplicationContext context;
private Map<String, Service> moduleMap = new ConcurrentHashMap<>();
@PostConstruct
public void init() {
// 自动扫描并注册
context.getBeansWithAnnotation(ModuleService.class)
.values().stream()
.filter(bean -> bean instanceof Service)
.forEach(this::registerService);
}
public Service getService(String module) {
return moduleMap.getOrDefault(module, defaultService);
}
}
// 4. 重构后:1行代码!
public List<Map<String, Object>> queryDb(Map<String, Object> parameter,
Map<String, Object> queryDetail,
boolean isQueryDb) {
return moduleRegistry.getService(parameter.get("module"))
.execute(parameter, queryDetail, isQueryDb);
}
效果:80 行 → 1 行,新增模块只需加注解,完美!
总结
这次重构让我深刻体会到:
- 设计模式不是银弹,但用对地方确实能大幅提升代码质量
- Spring 框架与设计模式结合,可以写出很优雅的代码
- 过度设计也是问题,如果只有 2-3 个 if-else,可能不需要策略模式
- 性能通常不是使用设计模式的阻碍,可维护性更重要
最后,代码的可读性和可维护性永远是第一位的。当你看到大量 if-else/switch-case 时,不妨停下来想想:
- 是不是该用策略模式了?
- 是不是可以用注册模式?
- Spring 的 IoC 能否帮忙自动化?
延伸阅读
- 如果对更多设计模式实践感兴趣,推荐看看 Spring 源码
- JDK 中的
Comparator就是策略模式的经典应用 - Netty 的
ChannelHandler链也是策略+责任链的绝佳案例