既然是实现类干活儿,为何不直接写实现类,还要多此一举去写接口?

一、接口与实现类代码

package com.xxx;
import xxx;

public interface IOrderService 
{
    public default Map<String, Object> receiveOrderV1(OrderV1 orderV1) 
	{
        return null;
    }
} 
package com.xxx;
import xxx;

@Service
public class OrderService implements IOrderService {
    private static final Logger logger = LoggerFactory.getLogger(OrderService.class);

    @Autowired
    private Xxx xxx;

    public Map<String, Object> receiveOrderV1(OrderV1 orderV1){
        ExOrder exOrder = orderV1.getObject();
        return receiveExOrderV1(exOrder);
    }

    public Map<String, Object> receiveExOrderV1(ExOrder exOrder) {
	...
    }
}

二、分析

核心疑问可以总结为一句话:

"既然所有事都是OrderService实现的,为什么还要多绕一层接口IOrderService?直接用类不香吗?"

✅ 首先,从Java语言角度讲,这个接口不是必须的。

没错 —— 如果你只是想写一个OrderService干活,然后Controller调用它,其实你完全可以不写接口,直接@Autowired OrderService,完全work。

那问题来了:

🧱 为什么要写这个接口?

答案是:为了架构的"灵活性"和"扩展性"。

这是"面向接口编程"的设计思想。我们来看看几个实际用途:

1️⃣ 方便未来有多个实现类

假设以后OrderService有不同版本,比如:

  • OrderServiceV1
  • OrderServiceV2
  • MockOrderServiceForTest

只要你用的是接口IOrderService,你就可以在Spring中轻松切换实现:

@Autowired
private IOrderService orderService;

想换一个实现?只要换个@Primary,或者在配置文件里指定即可,不用改任何业务代码。

2️⃣ 方便做单元测试

写单元测试时,如果你用了接口,你可以轻松写一个假的Mock实现类:

public class MockOrderService implements IOrderService {
    @Override
    public Map<String, Object> receiveOrderV1(OrderV1 orderV1) {
        return Map.of("msg", "mock success");
    }
}

然后在测试中注入MockOrderService,你就能绕开实际的业务逻辑、数据库等,专心测接口逻辑。

3️⃣ 团队协作/规范统一

很多大公司或者平台架构都规定:所有服务必须有接口,即使现在只有一个实现,也要按规范来。

这样未来好维护、好替换、好接入框架。

这就像你知道家里电视插的是三孔插头,但你可能一辈子没换过插座 —— 可是只要你哪天真要换,插头统一就显得太香了。

4️⃣ 框架/中间件/动态代理兼容性

有时候框架(比如 Dubbo、Feign、MyBatis)都要求你传的是接口而不是实现类。比如:

@FeignClient("service")
public interface IOrderService {
    @PostMapping("/receiveV1")
    Map<String, Object> receiveOrderV1(@RequestBody OrderV1 orderV1);
}

接口让它更容易被 Spring AOP、代理等机制管理。

那你又会问:

你说这些我懂了,但这段代码里的IOrderService,就写了个default return null,这不是摆烂吗?

是的,在你现在看到的这段代码中,这个接口的确没有“实质性功能”

但你可以把它理解为一种"设计上的预埋",为将来做准备。

总结:一句话打底

接口就是一种"对未来变化的预留"。现在看着没用,不代表以后不会有用。❞

当然,如果你在做一些小项目、小团队、只追求效率,不搞那么多规范,那也完全可以不写接口,直接上类。

三、没有接口vs有接口 —— 对比示例

那我们就来对比一下——用不用接口,在测试、维护、扩展性这三方面到底有啥区别。

我们用刚才说的OrderService为例,演示一下:

✅ 场景设定

我们有一个控制器:

@RestController
public class OrderInController {
    @Autowired
    private IOrderService orderService;

    @PostMapping("/xxx/order")
    public Map<String, Object> receive(@RequestBody OrderV1 orderV1) {
        return orderService.receiveOrderV1(orderV1);
    }
}

方案一:用了接口IOrderService

1️⃣ 正常开发时

你实现这个接口:

@Service
public class OrderService implements IOrderService {
    @Override
    public Map<String, Object> receiveOrderV1(OrderV1 orderV1) {
        // 真实业务逻辑
    }
}

2️⃣ 测试时:写个假的Mock实现!

@Component
@Primary // 指定默认注入这个mock
public class MockOrderService implements IOrderService {
    @Override
    public Map<String, Object> receiveOrderV1(OrderV1 orderV1) {
        return Map.of("msg", "mocked", "code", 200);
    }
}

💡好处是:不依赖真实数据库、服务、网络,单元测试超级快,还能随便模拟返回数据。

3️⃣ 未来扩展时:新增一个实现类

@Service
public class OrderServiceV2 implements IOrderService {
    @Override
    public Map<String, Object> receiveOrderV1(OrderV1 orderV1) {
        // 全新逻辑,比如AI自动验单
    }
}

通过配置文件或注解,快速切换实现类。

方案二:没用接口,直接写死类

@Service
public class OrderService {
    public Map<String, Object> receiveOrderV1(OrderV1 orderV1) {
        // 真实业务逻辑
    }
}

🚫 问题来了:

❌ 1.测试难了

你想mock它,就只能用工具如Mockito:

when(orderService.receiveOrderV1(...)).thenReturn(...);

而且你要加@MockBean、@SpringBootTest,起一个大工程,性能慢,复杂度高。

❌ 2.扩展难了

以后你要加V2版本,没法让Spring根据接口切换,只能自己手动在Controller判断逻辑:

if (version.equals("v2")) {
   new OrderServiceV2().receive(...);
} else {
   new OrderService().receive(...);
}

丑爆了,还容易出错。

🔚 最终总结表格

对比点

用接口(IOrderService)

不用接口(直接用类)

测试 Mock

✅ 很方便,随便写一个实现注入

❌ 比较难,要配合框架写假类

多实现版本切换

✅ 很灵活,Spring 配置就行

❌ 要手动 if 分支判断

面向框架兼容性

✅ 支持 Feign、Dubbo、AOP 等

❌ 有些框架不支持类注入

初期开发快慢

❌ 多写一个接口略慢

✅ 快速开发直接写类

项目代码结构清晰度

✅ 有规范,易读可扩展

❌ 类膨胀,维护成本高

全部评论

相关推荐

评论
点赞
3
分享

创作者周榜

更多
牛客网
牛客企业服务