Spring Boot - 讨论为什么 Service 层需要写 Service 接口再写其实现类 impl?
接口的概念
接口(interface)是一种在面向对象编程中非常重要的概念,它有助于提高程序的可扩展性和灵活性。以下是接口如何体现程序高扩展性的几个方面:
- 分离接口与实现:接口定义了类应该具备的行为,但不提供具体的实现细节。这使得你可以将接口与其实现分离开来。当你需要更改或扩展程序的功能时,你可以创建新的实现类,而无需修改现有的代码。
- 多态性:接口允许多个类实现相同的接口,这意味着你可以通过接口类型引用不同的实现类对象,从而实现多态性。这可以在运行时动态选择不同的实现类,而无需修改客户端代码,从而提高了程序的灵活性和可扩展性。
- 松耦合:使用接口可以减少类之间的耦合度。耦合度低的代码更容易维护和扩展,因为修改一个类的实现通常不会对其他类产生影响,只要它们仍然遵循相同的接口。
- 替代实现:当你需要改变或优化某个功能时,可以创建新的实现类来替代旧的实现,而不需要修改现有代码。这种灵活性使得在不破坏现有功能的情况下进行系统升级变得更容易。
- 接口继承:接口可以继承其他接口,从而允许在已有接口的基础上进一步扩展功能。这样可以将功能分解为更小的部分,使系统更易于管理和扩展。
- 插件和扩展性:接口常常用于定义插件接口或扩展点,这允许其他开发者编写自定义实现,以扩展现有系统的功能,而无需修改原始代码。
是的,上面就是接口的好处,乍一看好处颇多,在很多项目中人家都是这么写的,但也有人提出疑问为什么非要这样写,貌似不写接口也可以啊,而且也能省去不少麻烦事儿。我也包括在其中,于是我亲自动手敲,理解为什么这样做,终于得到了心中理想的答案,为自己解惑,于是写下这篇博文以免自己忘记。
大白话理解接口
其实联想到现实世界中来,可以更快速地理解接口。教育部、学校,我们举的例子有这两个实体,教育部制定一个管理规定,里面的条条不是写死的。具体实施还是需要各学校的真实情况来决定。
比如:学生申请休学或者学校认为应当休学的,经学校批准,可以休学。休学次数和期限由学校规定。
上面这条管理规定没有限制死,具体情况要根据学生的情况,学校来决定。
举例子
MinistryOfEducation
/**
* @description:
* @package: com.example.iocdi.service
* @author: zheng
* @date: 2023/9/12
*/
public interface MinistryOfEducation {
/**
* 学生申请休学
*
* @return 休学结果
*/
String suspend();
}
接口定义了有学生申请休学的管理规定,但是具体实施交给学校。基本的流程你每个学校都要照着走,必须返回一个休学结果出来。
No1MiddleSchool
/**
* @description:
* @package: com.example.iocdi.service.impl
* @author: zheng
* @date: 2023/9/12
*/
@Service("no1MiddleSchool")
public class No1MiddleSchool implements MinistryOfEducation {
@Override
public String suspend() {
return null;
}
}
第一中学将教育部的规定添加到自己学校管理办法手册中,并制定具体的执行流程。
No2MiddleSchool
/**
* @description:
* @package: com.example.iocdi.service.impl
* @author: zheng
* @date: 2023/9/12
*/
@Service("no2MiddleSchool")
public class No2MiddleSchool implements MinistryOfEducation {
@Override
public String suspend() {
return null;
}
}
测试
这里我通过测试执行效果,这里就不能通过现实例子来理解了,从程序的角度出发,我们定义了两个实现类,接口都是 MinistryOfEducation
,根据接口的定义——多态性,在声明对象的变量时通过接口类型来定义。
tip:[start]
多态性?
接口允许多个类实现相同的接口,这意味着你可以通过接口类型引用不同的实现类对象,从而实现多态性。这可以在运行时动态选择不同的实现类,而无需修改客户端代码,从而提高了程序的灵活性和可扩展性。
tip:[end]
@SpringBootTest
class IocDiApplicationTests {
@Qualifier("no1MiddleSchool")
@Autowired
private MinistryOfEducation education;
@Test
void mainTest() {
education.suspend();
}
@Test
void mainTest2() {
education.stop();
}
}
此时,根据业务需求,我们需要调整这个类传递的实现类,也就是 new 的对象不再是 No1MiddleSchool,而是 No2MiddleSchool。
tip:[start]
@Qualifier?
@Qualifier
是在整个项目中存在多个接口实现类,也就是类型一致的情况下(如果两个类实现了同一个接口它们就属于同一个类型),会出现混乱的情况,这个时候需要加上该注解指明具体使用的实现类是哪一个。
tip:[end]
通过这种方式,我们下面的其他函数依赖了这个实现类中的函数时不再需要修改变量的引用,只要是符合了这个接口类型的实现类,都具备这个函数,都可以调用,不报错,可以正常执行。
No1MiddleSchool
和 No2MiddleSchool
两个实现类中对于同一个接口定义的函数都有不同的业务处理,而我们在调用的地方只需要修改注入的实体类是谁,就可以修改整个程序的业务代码,而不需要再改动其他地方,导致牵一发而动全身。
再举例子
如上图所示,定义了实现类 UserServiceImpl 和 UserServiceImpl2,实现的接口是 UserService。
UserController
和 UserController2
分别调用了不同的 UserService
实现类。但是它们引用的都是接口类型。访问两个不同的路径,业务都是不一样的。
如果我要在 UserController
中改成 UserServiceImpl2
的实现类,只需要修改 @Qualifier
的名称就可以了,不需要再多改任何的地方。对于 UserController2
也是一样的。
如果哪天,UserController
既不需要 UserServiceImpl
,也不需要 UserServiceImpl2
,需要的是 UserServiceImpl3
,那也无妨,只要这个实现类实现的还是 UserService
,控制层注入代码时也只需要修改名称即可。
总结
Service 层写接口还是有必要的,尤其是考虑到程序日后可能会扩展业务或修改业务,建议都这样做。
但是,具体情况具体分析,如果这个业务真的不可能再变更或者扩展,那你也可以不需要写接口再写实现类。