分层解耦
1. 三层架构
1.1 传统问题
- 可以发现,我们刚刚编写的程序有很多问题:
- 所有逻辑代码都写在了Controller类中
- 开发比较复杂的功能,Controller类中就会存在大量的数据操作、逻辑处理的代码
- 代码复用性、拓展性比较差
- 项目中如果都这样做,将变得难以维护
1.2 优点
- 在进行软件设计、开发时:
- 需要尽量让每一个接口、类、方法的职责单一
- 一个接口、类、方法都只做一件事情,只管一块功能
- 这就称为:单一职责原则
- 这样可使接口、类、方法的 复杂度更低,可读性更强,拓展性更好,也更利于后期的维护
- 基于此,在Web开发中才有了 三层架构
1.3 概述
- controller:控制层,接收前端发送的请求,对请求进行处理,并响应数据。
- service:业务逻辑层,处理具体的业务逻辑。
- dao:数据访问层(Data Access Object)(持久层),负责数据访问操作,包括数据的增、删、改、查。
- 解析:
- 前端发起请求后,先会到达Controller
- Controller层接收请求后,要调用Service进行逻辑处理
- 逻辑处理的前提是拿到数据,Service层要调用Dao层
- Dao层再去操作文件中的数据,把数据拿到之后,将其返回给Service层
- Service拿到数据之后,进行逻辑处理,将处理结果再返回给Controller层
- Controller最后响应数据给前端
1.4 优化代码
- 思考:
- 所编写的Dao层的代码最终是要被Service层调用的,而数据访问Dao层的实现方式可能有很多
- 例如:
- 访问的可能是文件中、数据库中或者别人给我们提供的一个接口获取到的数据
- 要想灵活的切换各种实现,可以通过面向接口的方式进行编程
(1) Dao层
-
接口(EmpDao):
package com.app.dao; import com.app.pojo.Emp; import java.util.List; public interface EmpDao { //获取员工数据 public List<Emp> listEmp(); }
-
接口实现类(EmpDaoA):
package com.app.dao.impl; import com.app.dao.EmpDao; import com.app.pojo.Emp; import com.app.uitls.XmlParserUtils; import java.util.List; public class EmpDaoA implements EmpDao { @Override public List<Emp> listEmp() { //1. 解析xml文件 //动态获取emp.xml的磁盘路径 String file = this.getClass().getClassLoader() .getResource("emp.xml").getFile(); System.out.println(file); //解析xml文件 List<Emp> empList = XmlParserUtils.parse(file, Emp.class); //2. 返回获取到的数据 return empList; } }
(2) Service层
-
接口(EmpService):
package com.app.service; import com.app.pojo.Emp; import java.util.List; public interface EmpService { //获取员工列表 public List<Emp> listEmp(); }
-
接口实现类(EmpServiceA):
package com.app.service.impl; import com.app.dao.EmpDao; import com.app.dao.impl.EmpDaoA; import com.app.pojo.Emp; import com.app.service.EmpService; import java.util.List; public class EmpServiceA implements EmpService { private EmpDao empDao = new EmpDaoA(); @Override public List<Emp> listEmp() { //1. 调用dao,获取数据 List<Emp> empList = empDao.listEmp(); //2. 处理转换数据 empList.forEach(emp -> { //(1)处理性别gender: //获取当前用户性别数据 String gender = emp.getGender(); //如果为1,则重新将该用户的性别赋值为:男 if ("1".equals(gender)){ emp.setGender("男"); } else if ("2".equals(gender)) { //如果为2,则赋值为:女 emp.setGender("女"); } //(2)处理职位job: //获取当前用户职位数据 String job = emp.getJob(); //如果为1,则重新将该用户的职位赋值为:讲师 if ("1".equals(job)){ emp.setJob("讲师"); } else if ("2".equals(job)) { //如果为2,则赋值为:班主任 emp.setJob("班主任"); } else if ("3".equals(job)) { //如果为3,则赋值为:就业指导 emp.setJob("就业指导"); } }); //3. 返回处理完毕的数据 return empList; } }
(3) Controller层
-
控制类(EmpController):
package com.app.controller; import com.app.pojo.Emp; import com.app.pojo.Result; import com.app.service.EmpService; import com.app.service.impl.EmpServiceA; import com.app.uitls.XmlParserUtils; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController public class EmpController { private EmpService empService = new EmpServiceA(); @RequestMapping("/listEmp") public Result listEmp(){ //1. 调用service,获取数据 List<Emp> empList = empService.listEmp(); //2. 响应数据 return Result.success(empList); } }
(4) 测试
(5) 解析
- 红色箭头是请求、调用
- 橙色箭头是响应、返回
(6) 小结
-
三层架构:
-
拆分前后对比:
2. 分层解耦
2.1 内聚
-
软件中各个功能模块内部的功能联系。
2.2 耦合
-
衡量软件中各个层/模块之间的依赖、关联的程度。
2.3 软件设计原则
-
高内聚低耦合:
-
高内聚:模块内部的功能联系越紧密越好。
-
低耦合:尽可能去降低各个层/模块之间的依赖、关联。
-
最好是能够做到解除耦合:各个层/模块之间的依赖、关联就没有了。
-
此时,Service层的代码如果需要改动或者切换实现类,不会影响到controller和dao层的代码
-
这样程序的灵活性及可扩展性就更好了
-
-
2.4 案例分析
-
我们所编写的案例,三层架构之间就是耦合了!
(1) 问题
-
既然耦合了,要降低耦合,就必须解耦。
-
那怎么办?
-
需要一个容器,Service实现对象可以放到这个容器中
-
需要改动Service层代码或切换实现类时,也只需要改动Service层的代码,其他不用动!
-
-
问题:
- (1)对象如何交给容器来管理
- (2)容器如何提供程序所需的依赖的资源
(2) 解决问题
- 控制反转:
I
nversionO
fC
ontrol,简称IOC。对象的创建控制权由程序自身转移到外部(容器),这种思想称为控制反转。 - 依赖注入:
D
ependencyI
njection,简称DI。容器为应用程序提供运行时,所依赖的资源,称之为依赖注入。 - Bean对象:IOC容器中创建、管理的对象,称之为bean。
3. IOC & DI入门
3.1 控制反转
-
Service层 及 Dao层的实现类,交给IOC容器管理。
3.2 依赖注入
-
为Controller及Service注入运行时,依赖的对象。
3.3 运行测试
3.4 成功解耦
-
如果想要改动Service层的代码,或者切换实现类
-
例如我想要从实现类A切换到B,则只需要在实现类A中将 @Component 注解注释掉:
- 测试没有问题!
-
4. IOC详解
4.1 Bean声明
- 要把某个对象交给IOC容器管理,需要在对应的类上加上如下注解之一:
注解 | 说明 | 位置 |
---|---|---|
@Component | 声明bean的基础注解 | 不属于以下三类时,用此注解(例如:工具类) |
@Controller | @Component的衍生注解 | 标注在控制器类上 |
@Service | @Component的衍生注解 | 标注在业务类上 |
@Repository | @Component的衍生注解 | 标注在数据访问类上(由于与mybatis整合,用的少) |
-
@Controller
-
@Service
-
@Repository
-
测试:
-
注意:
-
声明bean的时候,可以通过value属性指定bean的名字,如果没有指定,默认为类名首字母小写。
-
使用以上四个注解都可以声明bean,但是在springboot集成web开发中,声明控制器bean只能用@Controller。
-
4.2 Bean组件扫描
-
前面声明bean的四大注解,要想生效,还需要被组件扫描注解@ComponentScan扫描。
-
@ComponentScan注解虽然没有显式配置,但是实际上已经包含在了启动类声明注解 @SpringBootApplication 中,默认扫描的范围是启动类所在包及其子包。
-
因为创建SpringBoot项目时,启动类默认是在com.itheima下的,当dao层放到以外的范围,启动项目时是无法扫描到的,因此无法生效!
-
解决:这种方案不推荐的
-
建议的是按照规范来,这样可以规避这些问题:
4.3 小结
(1)声明bean的注解
- @Component,@Controller,@Service,@Repository
- @SpringBootApplication具有包扫描作用,默认扫描当前包及其子包
5. DI详解
5.1 Bean注入
-
@Autowired(自动装配)注解,默认是按照类型进行,如果存在多个相同类型的bean,将会报出如下错误:
-
通过以下几种方案来解决:
-
@Primary
-
@Qualifier
-
@Resource
-
5.2 小结
(1)依赖注入的注解
-
@Autowired:默认按照类型自动装配。
-
如果同类型的bean存在多个:
@Primary @Autowired + @Qualifier("bean的名称") @Resource(name="bean的名称")
(2)@Resource 与 @Autowired区别(面试)
-
@Autowired 是spring框架提供的注解,而@Resource是JDK提供的注解。
-
@Autowired 默认是按照类型注入,而@Resource默认是按照名称注入。