内聚和耦合

在软件的设计开发中离不开两个词,内聚性和耦合性 🤔。
内聚性是指机能相关的程序组合成一模块的程度,以下的情形会降低程序的内聚性:

  • 许多机能封装在一类型内,可以借由方法供外界使用,但机能彼此类似之处不多。
  • 在方法中进行许多不同的机能,使用的是相关性低或不相关的资料。

低内聚性的缺点如下:

  • 增加理解模块的困难度。
  • 增加维护系统的困难度,因为一个逻辑修改会影响许多模块,而一个模块的修改会使得一些相关模块也要修改。
  • 增加模块复用困难度,因为大部分的应用程序无法复用一个由许多不一定相关的机能组成的模块。

耦合性是与耦合性是指一程序中模块及模块之间信息或参数依赖的程度。耦合性可以是低耦合性(或称为松散耦合),也可以是高耦合性(或称为紧密耦合)。

紧密耦合的系统在开发阶段有以下的缺点:

  • 一个模块的修改会产生涟漪效应,其他模块也需随之修改。
  • 由于模块之间的相依性,模块的组合会需要更多的精力及时间。
  • 由于一个模块有许多的相依模块,模块的可复用性低。

一个优秀的应用程序应当是高内聚和松散耦合的。在开发 web 应用程序时,解决内聚性和耦合性是一个不得不面对的问题,而 spring 框架为我们提供一个很好的解决方案。

三层架构

在这里我们给出一个示例程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RestController
public class RequestController {
@RequestMapping("/listEmp")
public Result listEmp() {
String file = this.getClass().getClassLoader().getResource("emp.xml").getFile();
List<Emp> list = XmlParserUtils.parse(file, Emp.class);
list.forEach(emp -> {
String gender = emp.getGender();
switch (gender) {
case "1" -> emp.setGender("男");
case "2" -> emp.setGender("女");
case "3" -> emp.setGender("其他");
}
String job = emp.getJob();
switch (job) {
case "1" -> emp.setJob("讲师");
case "2" -> emp.setJob("班主任");
case "3" -> emp.setJob("就业指导");
}
});
return Result.success(list);
}
}

在这个示例程序中我们在接收到请求时,会将读取emp.xml中的数据并将数据封装入 JavaBean 对象中,然后替换对应字段,接着将处理好的数据封装入 Result 对象中并以 json 的形式返回响应。
这个程序代码并不长但是这并不是一个好的程序。它将数据的读取,处理和返回都放在了同一个方法中,这功能之间确实是互相依赖的但彼此之间是可以相互独立的,所以这种写法并不便于后续的开发和维护。根据前面内聚性和耦合性的知识,我们可以判断出这是一个高耦合且低内聚的程序。
现在,我们将要对其进行改造。我们将这个方法中对数据的操作分为三个部分:

  1. 数据的读取
  2. 数据的处理
  3. 数据的返回

并用 spring 中三个层来进行改造,我们先看看 spring 中的三个层之间与前端和数据库的关系:

Controller:用于处理 http 请求和返回响应
Service:用于处理业务逻辑,处理数据
Dao:data access object的缩写,用于和数据库交互获取数据

现在我们基于这三层架构对程序进行改造:

Controller 代码:

1
2
3
4
5
6
7
8
9
@RestController
public class EmpController {
private EmpService empService = new EmpServiceA();

@RequestMapping("/listEmp")
public Result listEmp() {
return Result.success(empService.listEmp());
}
}

Service 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Service层接口
public interface EmpService {
List<Emp> listEmp();
}

// Service层实现类
@Service
public class EmpServiceA implements EmpService {
private EmpDao empDao = new EmpDaoA();

@Override
public List<Emp> listEmp() {
List<Emp> list = empDao.listEmp();
list.forEach(emp -> {
String gender = emp.getGender();
switch (gender) {
case "1" -> emp.setGender("男");
case "2" -> emp.setGender("女");
case "3" -> emp.setGender("其他 ");
}
String job = emp.getJob();
switch (job) {
case "1" -> emp.setJob("讲师");
case "2" -> emp.setJob("班主任");
case "3" -> emp.setJob("就业指导");
}
});
return list;
}
}

Dao 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Dao层接口
public interface EmpDao {
List<Emp> listEmp();
}

// Dao层实现类
@Repository
public class EmpDaoA implements EmpDao {

@Override
public List<Emp> listEmp() {
String file = Objects.requireNonNull(this.getClass().getClassLoader().getResource("emp.xml")).getFile();
return XmlParserUtils.parse(file, Emp.class);
}
}

经过改造后,我们成功将每个模块都独立了出来,便于我们后期对各个模块进行维护和拓展功能,虽然我们提高了每个模块自身的内聚性,但是我们并没有解决每个层之间的耦合性,当我们产品迭代需要开发新的实现类时,我们还是需要到每个层中实例化对应的对象。这仍然不便于协作开发,因此我们需要分层解耦。

分层解耦

IOC(控制反转,Inversion of Control)和 DI(依赖注入,Dependency Injection),通过使用这两个概念我们能实现组件之间的解耦和提高代码的可维护性。下面是这两个概念的详细解释。

IOC

控制反转是一种设计原则,它将对象的创建和管理权从应用程序代码转移到外部容器。换句话说,IOC 让框架或容器负责控制程序的执行流,而不是由程序自身来控制。
在传统的编程中,程序通常会直接创建和管理依赖对象。但在 IOC 中,应用程序通过依赖于抽象(如接口或基类)来声明所需的依赖,而具体的实现和生命周期则由 IOC 容器负责。

DI

依赖注入是指在一个 JavaBean 对象在使用时将其依赖的其他对象从 IOC 容器中注入到 JavaBean 对象中。

spring 中提供了这两个概念的具体实现,使用以下注释可以将 JavaBean 的控制权交给 spring 的 IoC 容器:

注解 说明 位置
@Component 声明 bean 的基础注解 不属于下面三类
@Controller @Component 的衍生注解 控制器类
@Service @Component 的衍生注解 业务类
@Repository @Component 的衍生注解 数据访问类

使用@Autowired注解可以在使用到相关类时让 spring 的 IoC 容器自动装配到此类中。
使用这些注解改造后的代码:

Controller 代码:

1
2
3
4
5
6
7
8
9
10
@RestController
public class EmpController {
@Autowired
private EmpService empService;

@RequestMapping("/listEmp")
public Result listEmp() {
return Result.success(empService.listEmp());
}
}

Service 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// Service层接口
public interface EmpService {
List<Emp> listEmp();
}

// Service层实现类
@Service
public class EmpServiceA implements EmpService {
@Autowired
private EmpDao empDao;

@Override
public List<Emp> listEmp() {
List<Emp> list = empDao.listEmp();
list.forEach(emp -> {
String gender = emp.getGender();
switch (gender) {
case "1" -> emp.setGender("男");
case "2" -> emp.setGender("女");
case "3" -> emp.setGender("其他 ");
}
String job = emp.getJob();
switch (job) {
case "1" -> emp.setJob("讲师");
case "2" -> emp.setJob("班主任");
case "3" -> emp.setJob("就业指导");
}
});
return list;
}
}

Dao 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Dao层接口
public interface EmpDao {
List<Emp> listEmp();
}

// Dao层实现类
@Repository
public class EmpDaoA implements EmpDao {
@Override
public List<Emp> listEmp() {
String file = Objects.requireNonNull(this.getClass().getClassLoader().getResource("emp.xml")).getFile();
return XmlParserUtils.parse(file, Emp.class);
}
}

这里的@RestController注解是包含了@Controller@ResponseBody注解的。

通过这些操作我们成功降低了三层之间的耦合性 🥳

不过还有一个小问题,如果在后期我们新增了一个 Service 接口的实现类,那么在 autowired 时就会出现 Error,IoC 容器并不知道要注入哪个实现类对象,对于这个问题有三种解决方法:

  1. @Primary:使用@Primary提升注入的优先级
    使用方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    @Primary
    @Service
    public class EmpServiceA implements EmpService {
    @Autowired
    private EmpDao empDao;

    @Override
    public List<Emp> listEmp() {
    List<Emp> list = empDao.listEmp();
    list.forEach(emp -> {
    String gender = emp.getGender();
    switch (gender) {
    case "1" -> emp.setGender("男");
    case "2" -> emp.setGender("女");
    case "3" -> emp.setGender("其他 ");
    }
    String job = emp.getJob();
    switch (job) {
    case "1" -> emp.setJob("讲师");
    case "2" -> emp.setJob("班主任");
    case "3" -> emp.setJob("就业指导");
    }
    });
    return list;
    }
    }
  2. @Qualifier:使用@Qualifier预选择注入的类,通过名称指定,名称可以在@Component处修改
    使用方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @RestController
    public class EmpController {
    @Qualifier("empServiceA")
    @Autowired
    private EmpService empService;

    @RequestMapping("/listEmp")
    public Result listEmp() {
    return Result.success(empService.listEmp());
    }
    }
  3. @Resource:这是 Java EE 规范中提供的,@Resource用于资源的查找的注入
    使用方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @RestController
    public class EmpController {
    @Resource(name = "empServiceA")
    private EmpService empService;

    @RequestMapping("/listEmp")
    public Result listEmp() {
    return Result.success(empService.listEmp());
    }
    }