
本文介绍在 spring boot 中如何合理划分 service 层职责,通过工厂模式与策略模式解耦解析、查询、结果转换等环节,避免将业务逻辑和数据状态混杂在单一类中,提升可维护性与可测试性。
本文介绍在 spring boot 中如何合理划分 service 层职责,通过工厂模式与策略模式解耦解析、查询、结果转换等环节,避免将业务逻辑和数据状态混杂在单一类中,提升可维护性与可测试性。
在构建面向领域逻辑的 Spring Boot 应用时,一个常见挑战是:当多个相似但语义不同的业务流程(如按流派查询歌曲)共享相同执行骨架(解析输入 → 构建查询 → 执行数据库操作 → 组装响应),却因实现细节差异而难以复用又不致耦合过重。此时,简单地将所有逻辑堆砌在 SongService 中,或让 Query 子类自行持有 List
正确的做法是明确各组件边界,让 Service 充当协调者(Orchestrator),而非执行者(Executor)。具体而言:
- ✅ Query 接口只负责「输入到查询条件」的转换,不涉及数据库访问或 DTO 构建;
- ✅ SongRepository 专注数据访问,接收标准化查询参数(如 Specification、QueryBuilder 或自定义条件对象),返回原始领域实体;
- ✅ PrepareAnswer(或 SongDTOMapper)仅负责领域对象到 DTO 的无副作用转换;
- ❌ 绝不让 Query 实例持有 List
成员变量 ——这会将瞬态执行结果固化为状态,导致线程不安全、难以单元测试,并模糊生命周期边界。
以下是一个生产就绪的结构示例:
// 1. 定义查询策略契约(无状态、纯函数式)
public interface Query {
Data parse(String rawInput); // 解析原始输入
QueryBuilder createQueryBuilder(Data parsed); // 构建可被 Repository 消费的查询对象
}
// 2. 具体实现(如 RockQuery)
@Component
public class RockQuery implements Query {
@Override
public Data parse(String rawInput) {
return new Data("rock", extractYear(rawInput));
}
@Override
public QueryBuilder createQueryBuilder(Data parsed) {
return QueryBuilder.where("genre").eq(parsed.genre())
.and("year").gte(parsed.year());
}
}
// 3. 工厂解耦创建逻辑
@Component
public class QueryFactory {
private final Map<String, Query> queryMap = Map.of(
"rock", new RockQuery(),
"pop", new PopQuery(),
"folk", new FolkQuery()
);
public Query createQuery(String type) {
return queryMap.getOrDefault(type.toLowerCase(),
throw new IllegalArgumentException("Unknown query type: " + type));
}
}对应的服务层实现应保持轻量、高内聚:
@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, String input) {
// Step 1: 获取策略
Query query = queryFactory.createQuery(genre);
// Step 2: 解析 + 构建查询条件(无副作用)
Data parsed = query.parse(input);
QueryBuilder builder = query.createQueryBuilder(parsed);
// Step 3: 执行查询(交由 Repository 封装 JPA/MyBatis 细节)
List<SongItem> songs = songRepository.findByQueryBuilder(builder);
// Step 4: 转换结果(交由独立映射器)
PrepareAnswer mapper = prepareAnswerFactory.createPrepareAnswer(genre);
return mapper.prepareAnswer(songs);
}
}⚠️ 关键注意事项:
- 所有策略类(Query、PrepareAnswer)应设计为无状态(stateless),避免使用实例变量存储中间结果(如 List
)。若需缓存解析结果,应使用 ThreadLocal 或方法级局部变量; - SongRepository 接口不应接受 Query 实例,而应接收更底层、更稳定的契约(如 QueryBuilder、Specification
或自定义 SongSearchCriteria),以隔离策略实现变更对数据访问层的影响; - 工厂类(QueryFactory)推荐使用 Spring 的 @Primary + @Qualifier 或 ObjectProvider 实现运行时动态注册,便于后续新增流派而无需修改核心服务;
- 单元测试时,可分别 Mock QueryFactory 和 PrepareAnswerFactory,使 SongService 测试完全脱离数据库与具体策略实现,聚焦于流程编排逻辑。
这种分层并非过度设计,而是将“做什么”(what)、“怎么做”(how)与“谁来做”(who)清晰分离。它让每个类只回答一个问题,也让每次需求变更(例如新增 Jazz 查询支持)仅需新增一个 JazzQuery 实现并注册到工厂——零侵入现有代码,真正践行开闭原则。










