
本文介绍在 spring boot 中如何合理拆分 service 层职责,通过工厂+策略模式将“数据解析→查询构建→数据库执行→dto 转换”四步流程解耦,避免 service 膨胀,提升可测试性与可扩展性。
本文介绍在 spring boot 中如何合理拆分 service 层职责,通过工厂+策略模式将“数据解析→查询构建→数据库执行→dto 转换”四步流程解耦,避免 service 膨胀,提升可测试性与可扩展性。
在构建高内聚、低耦合的业务服务时,一个常见误区是将所有逻辑(如参数解析、条件组装、数据库调用、结果转换)全部堆砌在单个 @Service 类中。这不仅导致类职责不清、难以单元测试,也阻碍了按类型(如 Rock、Pop、Folk)灵活扩展查询行为。理想的设计应遵循 单一职责原则(SRP) 和 依赖倒置原则(DIP),让 Service 仅承担协调者(Orchestrator)角色,而非执行者。
✅ 推荐架构:工厂 + 策略 + 不可变数据流
核心思想是:Service 不持有中间状态(如 List
以下为完整实现示例:
// 1. 定义统一策略接口(不暴露内部状态)
public interface Query {
Object parse(); // 解析输入数据(如 JSON/Map → 查询条件对象)
QueryBuilder createQueryBuilder(); // 构建 JPA Criteria 或自定义查询器
}
public interface PrepareAnswer {
List<SongDTO> prepareAnswer(List<SongItem> items);
}// 2. 工厂类(支持运行时策略选择)
@Component
public class QueryFactory {
private final Map<String, Query> queryMap;
public QueryFactory(RockQuery rockQuery, PopQuery popQuery, FolkQuery folkQuery) {
this.queryMap = Map.of(
"ROCK", rockQuery,
"POP", popQuery,
"FOLK", folkQuery
);
}
public Query createQuery(String type) {
return queryMap.getOrDefault(type.toUpperCase(),
throw new IllegalArgumentException("Unknown query type: " + type));
}
}
@Component
public class PrepareAnswerFactory {
private final Map<String, PrepareAnswer> answerMap;
public PrepareAnswerFactory(RockAnswer rockAnswer, PopAnswer popAnswer) {
this.answerMap = Map.of(
"ROCK", rockAnswer,
"POP", popAnswer
);
}
public PrepareAnswer createPrepareAnswer(String type) {
return answerMap.getOrDefault(type.toUpperCase(),
throw new IllegalArgumentException("No PrepareAnswer for: " + type));
}
}// 3. Service 层:纯粹协调,零业务逻辑
@Service
public class SongService {
private final SongRepository songRepository;
private final QueryFactory queryFactory;
private final PrepareAnswerFactory prepareAnswerFactory;
// 构造注入,显式声明依赖,利于测试与维护
public SongService(SongRepository songRepository,
QueryFactory queryFactory,
PrepareAnswerFactory prepareAnswerFactory) {
this.songRepository = songRepository;
this.queryFactory = queryFactory;
this.prepareAnswerFactory = prepareAnswerFactory;
}
public List<SongDTO> getSongsByGenre(String genre, Data input) {
// Step 1: 获取策略实例
Query query = queryFactory.createQuery(genre);
// Step 2: 解析输入 → 构建查询器(无副作用,返回新对象)
Object parsed = query.parse(); // 可能是 Map<String, Object> 或自定义 CriteriaParam
QueryBuilder builder = query.createQueryBuilder();
// Step 3: 执行查询(Repository 负责具体实现)
List<SongItem> songs = songRepository.findByCriteria(parsed, builder);
// Step 4: 转换结果(策略决定 DTO 结构与字段映射)
PrepareAnswer preparer = prepareAnswerFactory.createPrepareAnswer(genre);
return preparer.prepareAnswer(songs);
}
}⚠️ 关键设计说明与注意事项
-
禁止状态共享:Query 和 PrepareAnswer 实现类必须是无状态(stateless)的——不保存 List
成员变量。否则会引发并发问题(Spring Bean 默认 singleton)且破坏策略的可重入性。 -
数据流单向传递:List
仅作为方法参数在 prepareAnswer() 中短暂存在,不被任何策略类持有或缓存。这保证了线程安全与逻辑隔离。 -
工厂优于 if-else:使用 Map
注入策略实例,比在 Service 中写 if ("ROCK".equals(type)) 更易维护、更易测试、更易集成 Spring 的条件化 Bean(如 @ConditionalOnProperty)。 - Repository 职责明确:SongRepository 应只负责数据访问,不参与业务规则判断(如“Rock 歌曲需额外过滤年代”应在 RockQuery.parse() 中完成)。
-
可测试性保障:每个组件均可独立 Mock:
- QueryFactory → 返回模拟 Query
- SongRepository → 返回预设 List
- PrepareAnswerFactory → 返回模拟 PrepareAnswer 从而对 SongService.getSongsByGenre() 进行精准单元测试,无需启动容器。
✅ 总结
将 Service 视为“编排中枢”,而非“逻辑仓库”,是 Spring Boot 微服务架构中保持长期可维护性的关键。通过策略接口抽象行为、工厂管理实例生命周期、构造注入明确依赖关系,不仅能自然支持多类型查询扩展(新增 JazzQuery 只需新增实现类并注册到工厂),还能让每个模块专注自身契约,显著降低修改成本与回归风险。记住:好的 Service 不写逻辑,只写流程;不存数据,只传数据。










