← 返回文章列表

策略模式实践:干掉 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%),但代码可维护性提升巨大。

适用场景思考

策略模式特别适合以下场景:

  1. 多种算法切换:比如不同的加密算法、压缩算法
  2. 多数据源处理:像我们这个案例
  3. 支付方式选择:支付宝、微信、银行卡等
  4. 消息发送渠道:短信、邮件、推送等
  5. 导出格式选择: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导出
    }
}

踩坑记录

  1. Spring Bean 命名冲突:多个策略类如果不指定 Bean 名称,可能会冲突

    @Component("mysqlStrategy")  // 指定名称避免冲突
    public class MysqlQueryStrategy { }
    
  2. 策略初始化顺序:如果策略之间有依赖,需要用@DependsOn@Order控制顺序

  3. 线程安全问题:策略类最好设计成无状态的,避免并发问题

  4. 策略过多导致类爆炸:可以考虑用枚举+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();

工厂模式的优势:

  1. 封装创建逻辑,可以加入复杂的初始化
  2. 统一管理策略生命周期
  3. 便于添加缓存、监控等横切关注点
  4. 更好的测试性

实战案例二:模块服务路由

除了数据库查询,还遇到了另一个类似的场景:

// 原始代码: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 行,新增模块只需加注解,完美!

总结

这次重构让我深刻体会到:

  1. 设计模式不是银弹,但用对地方确实能大幅提升代码质量
  2. Spring 框架与设计模式结合,可以写出很优雅的代码
  3. 过度设计也是问题,如果只有 2-3 个 if-else,可能不需要策略模式
  4. 性能通常不是使用设计模式的阻碍,可维护性更重要

最后,代码的可读性和可维护性永远是第一位的。当你看到大量 if-else/switch-case 时,不妨停下来想想:

  • 是不是该用策略模式了?
  • 是不是可以用注册模式?
  • Spring 的 IoC 能否帮忙自动化?

延伸阅读

  • 如果对更多设计模式实践感兴趣,推荐看看 Spring 源码
  • JDK 中的Comparator就是策略模式的经典应用
  • Netty 的ChannelHandler链也是策略+责任链的绝佳案例