你是怎么在项目中做对象转换的

最近在读《实现领域驱动设计》这本书,对于业务模型有了很多的见解,也知道该怎么去设计一个系统,下面我通过一个例子,将我之前的代码进行一个重构操作

前言

如果你现在在使用Eclipse,当然不是说Eclipse完全是落后的,相比于IDEA,内存消耗和搜索方面是一个非常大的亮点,但是建议还是用IDEA,也就是JetBean出品的那一套,如果你是学生或者毕业不太久的学生,用你的教育邮箱就可以免费得到一个专业版的,何乐不为,至于更多IDEA的好处,可以Google一下看看

代码重构

规范化

根据以往程序员观念,包括我之前代码,都有这个毛病

  • domain包名

之前关于包名,都是用com.xxx.domain来命名,觉得这个是一个领域对象,针对每一个数据库表都建立一个domain来对应,但是实际上不是这样,Domain是一个领域对象,在实现领域驱动设计中,Domain是一个贫血模型,是没有行为的,或者说是没有实现领域模型的行为。所以这些对象应该属于entity对象,而不是领域对象,应该命名为com.xxx.entity, 当然具体贫血模型和领域对象的区别最好是看看这本书。

  • DTO

对于DTO对象,很多人认为只有在输入输出里面算,或者只能在上层调用对象才算DTO,但是这种说法不完全正确,对于DTO其实只要在网络中传输的对象,都可以叫DTO对象,比如RPC调用等等。

场景描述

现在有一个商品项目,我们有一个用户信息表,需要维护,里面有三个字端:username,Age,Sex

@RequestMapping("/v1/api/user")
@RestController
public class UserApi {

    @Autowired
    private UserService userService;

    @PostMapping
    public User addUser(UserInputDTO userInputDTO){
        User user = new User();
        user.setUsername(userInputDTO.getUsername());
        user.setAge(userInputDTO.getAge());
				user.setSex(userInputDTO.getSex)
        return userService.addUser(user);
    }
}
复制代码

相信很多人都这样写的,在Controller收到UserDTO对象,我们需要在Service层转换成BO或者Entity对象

重点就在这一步

User user = new User();
        user.setUsername(userInputDTO.getUsername());
        user.setAge(userInputDTO.getAge());
				user.setSex(userInputDTO.getSex)
复制代码

但是就出现个问题,现在三个字端已经够繁杂了,但是如果20个字端,那代码冗余度就很高了,所以这是最不推荐的做法。

使用工具类

我们了解到,这个时候拷贝技术就用到了,直接拷贝过来是最方便最优雅的,比如org.springframework.beans.BeanUtils#copyProperties这方法,我们用这个工具类直接进行拷贝,这里注意,这个方法是一个浅拷贝方法,我们优化一下代码

这里注意,阿里手册上是不推荐使用Apache的BeanUtils,因为性能问题,但是这是Spring的工具类

@PostMapping
public User addUser(UserInputDTO userInputDTO){
    User user = new User();
    BeanUtils.copyProperties(userInputDTO,user);
    return userService.addUser(user);
}
复制代码

这样的话,代码就精简多了,只要把user这个entity对象的字段设置和UserInputDTO对象字端一样就行了,就算再多字端也不怕了。

语义问题

上面代码看起来精简了很多,但是是存在语义问题的,因为不具备很好的可读性,所以我们最好还是专门写在一个方法里面,实现DTO的转换,详细如下

@PostMapping
 public User addUser(UserInputDTO userInputDTO){
         User user = convertFor(userInputDTO);
         return userService.addUser(user);
 }
// 专门实现一个私有方法,来对DTO实现转换
 private User convertFor(UserInputDTO userInputDTO){
         User user = new User();
         BeanUtils.copyProperties(userInputDTO,user);
         return user;
 }
复制代码

这里其实也应该引起我们的注意,就是我们写代码时候,也要考虑到不要随便实现一个转化,可读性很差,而且改动也是直接在原有地方改,风险很大,例如如果转换方式变了这里,你就要修改addUser方法,下面这种方法,直接在ConvertFor改动即可。所以我们应该将相同语义的代码放到同一个层次地方,这里可以看到,我们将转换方法convertFor私有化了,在重构书里,我们把这种重构方式叫做Extract Method,如何在同一个方法中,做一组相同层次的语义操作,而不是暴露具体的实现。

抽象接口定义

在实际写代码时候,我们可能需要大量做一个这样的操作,UserDTO转换,ItemDTO转换等等,我们应该将这个共同操作给抽离出来,这样所有操作就有规则执行了,这个时候,我们知道,convertFor这个方法就不能是一个统一方法,因为入参是根据不同DTO变的,这个时候我们就需要用泛型了。我们定义一个抽象接口。

public interface DTOConvert {
	T convert(S s);
}
复制代码

现在这个接口实现了,我们应该将ConvertFor实现类重新实现一遍了

public class UserInputDTOConvert implements DTOConvert {
	@Override
	public User convert(UserInputDTO userInputDTO) {
	User user = new User();
	BeanUtils.copyProperties(userInputDTO,user);
	return user;
	}
}
复制代码

这样,在Service层,我们就将代码规范了

@RequestMapping("/v1/api/user")
@RestController
public class UserApi {
    @Autowired
    private UserService userService;

    @PostMapping
    public User addUser(UserInputDTO userInputDTO){
        User user = new UserInputDTOConvert().convert(userInputDTO);

        return userService.addUser(user);
    }
}
复制代码

Code Review

我们在看看这里面,这里有个小问题,在AddUser这里,直接返回User,暴露信息很多,前文我们说,既然进去是DTO,出来也是DTO,那么这里我们完全可以在规范一点,返回的也是一个DTO对象,没有必要直接返回一个完整的User对象

@PostMapping
public UserOutputDTO addUser(UserInputDTO userInputDTO){
        User user = new UserInputDTOConvert().convert(userInputDTO);
        User saveUserResult = userService.addUser(user);
        UserOutputDTO result = new UserOutDTOConvert().convertToUser(saveUserResult);
        return result;
}
复制代码

我们在这里,你会发现,new这样一个DTO转化对象是没有必要的,而且每一个转化对象都是由在遇到DTO转化的时候才会出现,那我们应该考虑一下,是否可以将这个类和DTO进行聚合呢

User user = new UserInputDTOConvert().convert(userInputDTO);
复制代码

我们用的就是这个convert方法,我们直接将其融合到UserInputDTO里面

public class UserInputDTO {
    private String username;
    private int age;
    private String sex;
    
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }
	
    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
    
    public String getSex(){
				return sex;
		}

		public void setSex(String sex){
				this.sex = sex;
		}
		
    public User convertToUser(){
        UserInputDTOConvert userInputDTOConvert = new UserInputDTOConvert();
        User convert = userInputDTOConvert.convert(this);
        return convert;
    }

    private static class UserInputDTOConvert implements DTOConvert{
        @Override
        public User convert(UserInputDTO userInputDTO) {
            User user = new User();
            BeanUtils.copyProperties(userInputDTO,user);
            return user;
        }
    }
}
复制代码

这样可读性也很高,我们的输入DTO提供了转换Entity方法

这样在Service中的转换

User user = userInputDTO.convertToUser();
User saveUserResult = userService.addUser(user);
复制代码

再看工具类

我们上文实现了一个工具类,通过定义一个抽象接口,我们能够实现转换,但是这样转换是不完美的,很多工具类都是有转换类的,比如GUAVA的源码中也有一个转换类,我们可以参考一下,看有什么不同。

// com.google.common.base.Convert转换
public abstract class Converter<A, B> implements Function<A, B> {
    protected abstract B doForward(A a);
    protected abstract A doBackward(B b);
    //其他略
}
复制代码

我们看到,他是实现了两个抽象方法,doForward 和doBackward方法,也就是我们说的正向和逆向转化,我们可以仿照写一下

原来的

public class UserInputDTOConvert implements DTOConvert {
	@Override
	public User convert(UserInputDTO userInputDTO) {
	User user = new User();
	BeanUtils.copyProperties(userInputDTO,user);
	return user;
	}
}
复制代码

修改一下

private static class UserInputDTOConvert extends Converter<UserInputDTO, User> {
         @Override
         protected User doForward(UserInputDTO userInputDTO) {
                 User user = new User();
                 BeanUtils.copyProperties(userInputDTO,user);
                 return user;
         }

         @Override
         protected UserInputDTO doBackward(User user) {
                 UserInputDTO userInputDTO = new UserInputDTO();
                 BeanUtils.copyProperties(user,userInputDTO);
                 return userInputDTO;
         }
 }
复制代码

可能你觉得这样写有么有必要,但是在大多数系统中,入参和形参都是一样的,这样的话,正向转换和逆向转化就很方便了

例如我们将入DTO和出DTO都综合在一起,组成一个UserDTO,可以正向转也可以逆向转

public class UserDTO {
    private String username;
    private int age;
		private String sex;
		
    public String getUsername() {
            return username;
    }

    public void setUsername(String username) {
            this.username = username;
    }

    public int getAge() {
            return age;
    }

    public void setAge(int age) {
            this.age = age;
    }
	
		public String getSex(){
				return sex;
		}
		
		public void setSex(){
				this.sex = sex;
		}
		
    public User convertToUser(){
            UserDTOConvert userDTOConvert = new UserDTOConvert();
            User convert = userDTOConvert.doForward(this);
            return convert;
    }

    public UserDTO convertFor(User user){
            UserDTOConvert userDTOConvert = new UserDTOConvert();
            UserDTO convert = userDTOConvert.doBackward(user);
            return convert;
    }

    private static class UserDTOConvert extends Converter<UserDTO, User> {
            @Override
            protected User doForward(UserDTO userDTO) {
                    User user = new User();
                    BeanUtils.copyProperties(userDTO,user);
                    return user;
            }

            @Override
            protected UserDTO doBackward(User user) {
                    UserDTO userDTO = new UserDTO();
                    BeanUtils.copyProperties(user,userDTO);
                    return userDTO;
            }
    }

}

复制代码

这样在Service层的代码就更加精简了,因为入和出都是一样的

@PostMapping
 public UserDTO addUser(UserDTO userDTO){
         User user =  userDTO.convertToUser();
         User saveResultUser = userService.addUser(user);
         UserDTO result = userDTO.convertFor(saveResultUser);
         return result;
 }
复制代码

在特殊情况下,入和出都不一定是一样的,所以我们需要禁用逆向

private static class UserDTOConvert extends Converter<UserDTO, User> {
         @Override
         protected User doForward(UserDTO userDTO) {
                 User user = new User();
                 BeanUtils.copyProperties(userDTO,user);
                 return user;
         }

         @Override
         protected UserDTO doBackward(User user) {
                 throw new AssertionError("不支持逆向转化方法!");
         }
 }
复制代码

bean验证

现在我们写完了接口,但是可能也存在个问题,就是我们好像没有严重DTO,看起来好像比较完美,可能你也会存在疑问,比如关于验证,无论是前端提供限制,还是权限验证,这些不都做了嘛,后端还需要什么验证。

任何调用我api或者方法的人,比如前端验证失败了,或者某些人通过一些特殊的渠道(比如Charles进行抓包),直接将数据传入到我的api,那我仍然进行正常的业务逻辑处理,那么就有可能产生脏数据!

Jar 303验证

public class UserDTO {
    @NotNull
    private String username;
    @NotNull
    private int age;
    @NotNull
 		private String sex;
  	// 余下省略
}
复制代码

api验证

@PostMapping
    public UserDTO addUser(@Valid UserDTO userDTO){
            User user =  userDTO.convertToUser();
            User saveResultUser = userService.addUser(user);
            UserDTO result = userDTO.convertFor(saveResultUser);
            return result;
    }
复制代码

我们将这个验证传到前端,并转换为一个API异常

@PostMapping
public UserDTO addUser(@Valid UserDTO userDTO, BindingResult bindingResult){
     checkDTOParams(bindingResult);

     User user =  userDTO.convertToUser();
     User saveResultUser = userService.addUser(user);
     UserDTO result = userDTO.convertFor(saveResultUser);
     return result;
}
private void checkDTOParams(BindingResult bindingResult){
     if(bindingResult.hasErrors()){
             //throw new 带验证码的验证错误异常
     }
}
复制代码

BindingResult是Spring MVC验证DTO后的一个结果集,可以参考spring 官方文档

lomlock

lomlock当初用得很早,详细很多人都在用这个工具,能够省略我们大量getter setter操作

@Setter
@Getter
public class UserDTO {
    @NotNull
    private String username;
    @NotNull
    private int age;

    public User convertToUser(){
        UserDTOConvert userDTOConvert = new UserDTOConvert();
        User convert = userDTOConvert.convert(this);
        return convert;
    }

    public UserDTO convertFor(User user){
        UserDTOConvert userDTOConvert = new UserDTOConvert();
        UserDTO convert = userDTOConvert.reverse().convert(user);
        return convert;
    }

    private static class UserDTOConvert extends Converter{
        @Override
        protected User doForward(UserDTO userDTO) {
            User user = new User();
            BeanUtils.copyProperties(userDTO,user);
            return user;
        }

        @Override
        protected UserDTO doBackward(User user) {
            throw new AssertionError("不支持逆向转化方法!");
        }
    }

}
复制代码

当然如果只是做这些做操作当然不足以体现lomlock的强大,具体详细查看文档

链式风格

这在大数据一些框架里面很多体现,通常一个类有大几个个方法,而且要重复调用,甚至还有顺序

例如赋值操作

User user = new User();
user.setName("fourous");
user.setPassword("12345");
复制代码

同样的,如果有20个属性,这个清单会拉很长

我们将这个类再优化一下

public class Student {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public Student setName(String name) {
        this.name = name;
        return this;
    }

    public int getAge() {
        return age;
    }

    public Student setAge(int age) {
        return this;
    }
}
复制代码

现在调用变成了

User user = new User();
user.setName("fourous").setPassWord("12345");
复制代码

好,我们在用lomlock优化

@Accessors(chain = true)
@Setter
@Getter
public class Student {
    private String name;
    private int age;
}
复制代码

使用静态构造方法

我们之前发现,每次都要new 一个对象,其实我们可以用静态构造方法来简化一部分,语义也更加优美一点

例如对于数组创建

List list = new ArrayList();
复制代码

而在GUANA中,是这样的,提供了一个Lists工具类

Listlist = Lists.newArrayList();
复制代码

Lists命名是一种约定(俗话说:约定优于配置),它是指Lists是List这个类的一个工具类,那么使用List的工具类去产生List,这样的语义是不是要比直接new一个子类来的更直接一些呢,答案是肯定

再回过头来看刚刚的Student,很多时候,我们去写Student这个bean的时候,他会有一些必输字段,比如Student中的name字段,一般处理的方式是将name字段包装成一个构造方法,只有传入name这样的构造方法,才能创建一个Student对象。

这种完全可以用lomlock来优化

@Accessors(chain = true)
@Setter
@Getter
@RequiredArgsConstructor(staticName = "of")
public class Student {
    @NonNull private String name;
    private int age;
}
复制代码

这样创建对象时候就是这样的

Student student = Student.of("zs");
复制代码

我们链式调用一次

Student student = Student.of("zs").setAge(24);
复制代码

这样来的话,可读性强,而且代码冗余和代码量都不大

Build模式

我们设计模式有这个模式,我们先用原生的试试

public class Student {
    private String name;
    private int age;

    public String getName() {
            return name;
    }

    public void setName(String name) {
            this.name = name;
    }

    public int getAge() {
            return age;
    }

    public void setAge(int age) {
            this.age = age;
    }

    public static Builder builder(){
            return new Builder();
    }
    public static class Builder{
            private String name;
            private int age;
            public Builder name(String name){
                    this.name = name;
                    return this;
            }

            public Builder age(int age){
                    this.age = age;
                    return this;
            }

            public Student build(){
                    Student student = new Student();
                    student.setAge(age);
                    student.setName(name);
                    return student;
            }
    }

}
复制代码

调用方式

Student student = Student.builder().name("zs").age(24).build();
复制代码

我们lomlock优化一下

@Builder
public class Student {
    private String name;
    private int age;
}
复制代码

调用方式

Student student = Student.builder().name("zs").age(24).build();
复制代码

代理模式

正如我们所知的,在程序中调用rest接口是一个常见的行为动作,如果你和我一样使用过Spring 的RestTemplate,我相信你会我和一样,对他抛出的非http状态码异常深恶痛绝。

所以我们考虑将RestTemplate最为底层包装器进行包装器模式的设计:

public abstract class FilterRestTemplate implements RestOperations {
        protected volatile RestTemplate restTemplate;

        protected FilterRestTemplate(RestTemplate restTemplate){
                this.restTemplate = restTemplate;
        }

        //实现RestOperations所有的接口
}
复制代码

然后再由扩展类对FilterRestTemplate进行包装扩展:

public class ExtractRestTemplate extends FilterRestTemplate {
    private RestTemplate restTemplate;
    public ExtractRestTemplate(RestTemplate restTemplate) {
            super(restTemplate);
            this.restTemplate = restTemplate;
    }

    public RestResponseDTOpostForEntityWithNoException(String url, Object request, ClassresponseType, Object... uriVariables)
                    throws RestClientException{
            RestResponseDTOrestResponseDTO = new RestResponseDTO();
            ResponseEntitytResponseEntity;
            try {
                    tResponseEntity = restTemplate.postForEntity(url, request, responseType, uriVariables);
                    restResponseDTO.setData(tResponseEntity.getBody());
                    restResponseDTO.setMessage(tResponseEntity.getStatusCode().name());
                    restResponseDTO.setStatusCode(tResponseEntity.getStatusCodeValue());
            }catch (Exception e){
                    restResponseDTO.setStatusCode(RestResponseDTO.UNKNOWN_ERROR);
                    restResponseDTO.setMessage(e.getMessage());
                    restResponseDTO.setData(null);
            }
            return restResponseDTO;
    }
}
复制代码

包装器ExtractRestTemplate很完美的更改了异常抛出的行为,让程序更具有容错性。

在这里我们不考虑ExtractRestTemplate完成的功能,让我们把焦点放在FilterRestTemplate上,“实现RestOperations所有的接口”,这个操作绝对不是一时半会可以写完的

public abstract class FilterRestTemplate implements RestOperations {

    protected volatile RestTemplate restTemplate;

    protected FilterRestTemplate(RestTemplate restTemplate) {
            this.restTemplate = restTemplate;
    }

    @Override
    public T getForObject(String url, ClassresponseType, Object... uriVariables) throws RestClientException {
            return restTemplate.getForObject(url,responseType,uriVariables);
    }

    @Override
    public T getForObject(String url, ClassresponseType, MapuriVariables) throws RestClientException {
            return restTemplate.getForObject(url,responseType,uriVariables);
    }

    @Override
    public T getForObject(URI url, ClassresponseType) throws RestClientException {
            return restTemplate.getForObject(url,responseType);
    }

    @Override
    public ResponseEntitygetForEntity(String url, ClassresponseType, Object... uriVariables) throws RestClientException{
            return restTemplate.getForEntity(url,responseType,uriVariables);
    }
    //其他实现代码略。。。
}
复制代码

我们用lomlock就很简洁

@AllArgsConstructor
public abstract class FilterRestTemplate implements RestOperations {
    @Delegate
    protected volatile RestTemplate restTemplate;
}
复制代码

猜你喜欢

转载自juejin.im/post/5e6229305188254916542261