使用Java接口分离领域逻辑与实现时,异常与TDD的冲突问题咨询
这个问题我之前在做DDD+TDD项目的时候也碰到过,确实挺头疼的——既要保持领域接口的纯净性,又要满足TDD中异常场景的测试需求,得在抽象和实现之间找个平衡点。这里有几个我亲测有效的方案,你可以根据项目情况选择:
1. 用领域异常包装技术异常
核心思路是:让接口只声明业务语义相关的异常,实现类将底层技术异常(比如SQLException、IOException)包装成领域异常抛出。这样接口完全不涉及技术细节,同时能在TDD中测试异常场景。
示例代码:
首先定义一个领域级的检查型异常(也可以用运行时异常,看团队规范):
public class DomainOperationException extends Exception { public DomainOperationException(String message, Throwable cause) { super(message, cause); } }
然后领域接口只声明这个领域异常:
public interface UserRepository { User findById(String userId) throws DomainOperationException; }
实现类中捕获技术异常并包装:
public class JdbcUserRepository implements UserRepository { @Override public User findById(String userId) throws DomainOperationException { try { // JDBC查询逻辑 return jdbcTemplate.queryForObject("SELECT * FROM users WHERE id = ?", new Object[]{userId}, new UserRowMapper()); } catch (DataAccessException e) { throw new DomainOperationException("查询用户失败", e); } } }
TDD测试时,你可以验证抛出的DomainOperationException,同时通过getCause()确认底层技术异常的类型(如果需要的话)。
2. 统一使用运行时异常
如果你的项目允许不强制检查异常,可以将技术异常转换为自定义运行时领域异常,这样接口方法不需要声明throws,完全保持纯净。
示例代码:
自定义运行时领域异常:
public class DomainRuntimeException extends RuntimeException { public DomainRuntimeException(String message, Throwable cause) { super(message, cause); } }
领域接口无需任何throws声明:
public interface UserRepository { User findById(String userId); }
实现类中转换异常:
public class JdbcUserRepository implements UserRepository { @Override public User findById(String userId) { try { // JDBC查询逻辑 return jdbcTemplate.queryForObject("SELECT * FROM users WHERE id = ?", new Object[]{userId}, new UserRowMapper()); } catch (DataAccessException e) { throw new DomainRuntimeException("查询用户失败", e); } } }
TDD测试时,直接断言抛出DomainRuntimeException即可,这种方式更符合现代Java项目的趋势(比如Spring生态就大量使用运行时异常)。
3. 适配器模式隔离技术细节
如果想彻底把领域逻辑和技术实现解耦,可以用适配器模式:领域层定义纯业务接口,适配器层负责对接技术实现并处理异常。
示例结构:
- 领域层接口(完全纯净):
public interface UserRepository { User findById(String userId); }
- 技术适配器(实现领域接口,处理异常):
public class JdbcUserRepositoryAdapter implements UserRepository { private final JdbcUserDao jdbcUserDao; // 纯技术层面的DAO,抛出技术异常 @Override public User findById(String userId) { try { return jdbcUserDao.getById(userId); } catch (SQLException e) { throw new DomainRuntimeException("查询用户失败", e); } } }
- 技术DAO(只负责技术操作,抛出技术异常):
public class JdbcUserDao { public User getById(String userId) throws SQLException { // 原生JDBC操作,直接抛出SQLException } }
这种方式让领域接口完全不接触任何技术细节,TDD可以分层测试:先测试领域逻辑(用Mock适配器),再测试适配器的异常转换逻辑。
总结
核心原则是领域接口只暴露业务语义,隐藏技术实现细节。不管用哪种方案,本质都是把技术异常转换为业务可理解的异常(或者运行时异常),这样既满足TDD的测试需求,又符合接口分离原则。
内容的提问来源于stack exchange,提问作者Azure

