在实现RESTful API的项目中我们经常会使用三层架构,Controller层, Service层和DAO层。 Service层包含了核心业务规则,Controller层在接收到HTTP请求后,通过调用Service层中的方法来获取返回结果。例如通过调用UserService
中的getUser
来获取User
的详细信息。
package controller;
import service.UserService;
@RestController
@RequestMapping("/users")
class UserController {
private UserService userService;
@GetMapping
public User getUser(Identity identity) {
return userService.getUser(identity);
}
}
package service;
class UserService {
public User getUser(Identity identity) {
....
}
}
那么Identity
和User
应该定义在Controller层还是Service层呢?
有的人认为应该定义在Controller层,然后由Service层负责类型转换。例如将Identity
转换为IdentityModel
,将UserModel
转换为User
。
package service;
class IdentityModel {
public IdentityModel(Identity identity) {
....
}
}
class UserModel {
public User toUser() {
...
}
}
class UserService {
public User getUser(Identity identity) {
IdentityModel identityModel = new IdentityModel(identity);
....
UserModel userModel = ....
return userModel.toUser();
}
}
有的人认为应该定义在Service层,然后由Controller层负责类型转换。例如将IdentityRequest
转换为Identity
,将User
转换为UserResponse
。
package controller;
public IdentityRequest {
public Identity toIdentity() {
....
}
}
public UserResponse {
public UserResponse(User user) {
....
}
}
@RestController
@RequestMapping("/users")
class UserController {
private UserService userService;
@GetMapping
public UserResponse getUser(IdentityRequest identityRequest) {
User user = userService.getUser(identityRequest.toIdentity());
return new UserResponse(User);
}
}
从静态的角度看,上述两种方案都没有问题,因为我们不会再修改这部分代码。但从动态的角度看,第二种方案会让未来需求变更的工作量小一些。
首先,它可以消除循环依赖,Service层不再依赖Controller层。在方案一中,Controller层需要调用UserService.getUser
,所以其依赖于Service层。而Service层不但要用Identity
来初始化IdentityModel
,还要将UserModel
转换为User
,所以其又依赖于Controller层。在循环依赖的情况下,任何一方的变更都可能引起另一方的变更,这增加了潜在的工作量。在方案二中,Service层只会因为业务规则的改变而改变,更加符合SRP原则。
其次,它可以在Controller层出现变更时,不用修改Service层的代码。Service层包含了核心业务规则,我们希望它是稳定的,也就是变更的工作量会很大(注意这里和变更频率无关)。我们希望未来的大多数变更发生在Controller层,因为它是不稳定的,变更的工作量小。根据SDP原则,Controller层应该依赖于更加稳定的Service层,否则任何Controller层的变更都有可能修改需要更多工作量的Service层。
最后,它可以在Controller层出现变更时,不让Service层的测试失败。修复失败的测试也是工作量的一部分,Service层有更多的测试。我们不希望看到因为Controller层一个小的改动导致Service层有几十甚至上百个测试失败。
可能会有人基于YAGNI原则认为方案二是过度设计,但我不这么认为。YAGNI原则是说我们不应该在当前业务不需要的情况下增加代码的复杂度。而方案二并没有让代码变的更复杂,只是挪动了一下类型转换的位置。
也可能有人认为Controller层可以直接使用Service层中的User
和Identity
,不需要自己定义UserResponse
和IdentityRequest
,我认为要视情况而定。如果Controller层可以直接使用User
和Identity
,就不需要自己定义数据类型。如果不能直接使用,就需要按方案二自己定义数据类型。
我们设计软件架构的目的是用最少的工作量来完成需求变更,在Controller层进行类型转换可以减少工作量,但这是一个可以被随时推翻的决定。在设计架构的过程中,有一些原则要求我们可以预见到未来的变更,例如SOLID原则,Refactoring。而有一些原则又要求我们只关注当前的变更,例如YAGNI原则。这里没有银弹,我们当前做的任何决定都有可能被推翻,关键是要让我们的决定是可以被推翻的。