new具体类使测试变难、扩展变脆,因直接依赖实现细节(如数据库类型、连接方式),导致修改一处需全局排查;应面向接口编程,注入接口类型而非实现类,确保可替换性与可测性。

为什么 new 一个具体类会让测试变难、扩展变脆
因为直接依赖实现,等于把“怎么干活”的细节写死在调用方代码里。比如你写了 new MySQLUserRepository(),那这行代码就同时绑定了数据库类型、连接方式、甚至表结构变更——只要它一改,所有用到它的服务都得跟着测、跟着调。
常见错误现象:NullPointerException 频发(mock 不到位)、单元测试必须连真实 DB、换 Redis 缓存时要改十几处 new 调用。
- 使用场景:服务层调用数据访问、第三方 SDK 封装、策略类初始化
- 参数差异:面向接口传参只需关心
UserService,面向实现则得塞一堆DataSource、RedisTemplate进构造函数 - 性能影响几乎为零,但可维护性断崖下跌——改一个实现,得 grep 全局找
new XxxImpl()
interface 不是摆设:定义契约时盯住「能做什么」,不是「怎么做的」
接口名别叫 MySQLUserDao,这是自曝实现;要叫 UserRepository。方法签名也一样:save(User) 合理,insertIntoUsersTable(User) 就越界了——后者把 SQL 细节漏给了上层。
容易踩的坑:default 方法里写 JDBC 逻辑、接口里暴露 Connection 或 JdbcTemplate 类型参数、给接口加 @Repository 这种框架绑定注解。
- 正确示例:
public interface UserRepository { User findById(Long id); void save(User user); } - 错误示例:
void saveWithTransaction(User user, JdbcTemplate template)—— 框架细节泄漏 - 兼容性注意:接口一旦发布,新增方法要谨慎,老实现类会编译失败;优先用
default补充,但别塞核心逻辑
Spring 里不写 new,但也不是所有 @Autowired 都安全
自动注入解决的是对象创建问题,不是设计问题。如果注入的是 PayServiceImpl 而不是 PaymentService,DIP 依然没成立——只是把 new 换成了框架代劳。
典型错误现象:IDE 提示 “Could not autowire. No beans of ‘XXXImpl’ type found”,或者测试时只能靠 @MockBean 强行替换,说明接口抽象没立住。
- 检查点:注入点变量类型是否为
interface?比如@Autowired private UserService userService;(对) vs@Autowired private UserServiceImpl userServiceImpl;(错) - 配置项陷阱:
@Bean方法返回类型写实现类,会导致 Spring 容器里注册的是具体类型,后续按接口注入可能失败 - 多实现场景:用
@Qualifier("alipayService")比硬编码new AlipayService()好,但前提是它们共同实现了PaymentService
测试时 mock 接口比 patch 实现类省力十倍
因为你 mock 的是行为契约,不是某个类的私有字段或 final 方法。Junit + Mockito 下,when(userRepo.findById(1L)).thenReturn(mockUser) 这行能跑通,前提是 userRepo 是 UserRepository 类型,而不是某个具体实现。
容易被忽略的地方:有些团队写集成测试时直接 new 出实现类再塞进 service,美其名曰“不依赖 Spring 上下文”,结果测试代码里全是 new H2UserRepository(),跟业务代码一样脆弱。
- 正确姿势:测试类里
@Mock private UserRepository userRepo;,然后@InjectMocks private UserServiceImpl userService; - 反模式:
UserRepository repo = new InMemoryUserRepository();—— 又绕回面向实现 - 性能提示:mock 接口方法调用开销可忽略,但若在 mock 里做耗时操作(如读文件),就违背了测试快的本质











