为什么要在Controller层进行类型转换?

为什么要在Controller层进行类型转换?

sjmyuan 93 2023-03-26

在实现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) {
        ....
    }
}

那么IdentityUser应该定义在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层中的UserIdentity,不需要自己定义UserResponseIdentityRequest,我认为要视情况而定。如果Controller层可以直接使用UserIdentity,就不需要自己定义数据类型。如果不能直接使用,就需要按方案二自己定义数据类型。

我们设计软件架构的目的是用最少的工作量来完成需求变更,在Controller层进行类型转换可以减少工作量,但这是一个可以被随时推翻的决定。在设计架构的过程中,有一些原则要求我们可以预见到未来的变更,例如SOLID原则,Refactoring。而有一些原则又要求我们只关注当前的变更,例如YAGNI原则。这里没有银弹,我们当前做的任何决定都有可能被推翻,关键是要让我们的决定是可以被推翻的。