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

最近在读《实现领域驱动设计》这本书,对于业务模型有了不少的看法,也知道该怎么去设计一个系统,下面我经过一个例子,将我以前的代码进行一个重构操做前端

前言

若是你如今在使用Eclipse,固然不是说Eclipse彻底是落后的,相比于IDEA,内存消耗和搜索方面是一个很是大的亮点,可是建议仍是用IDEA,也就是JetBean出品的那一套,若是你是学生或者毕业不过久的学生,用你的教育邮箱就能够免费获得一个专业版的,何乐不为,至于更多IDEA的好处,能够Google一下看看java

代码重构

规范化

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

  • domain包名

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

  • 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对象设计模式

重点就在这一步api

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

可是就出现个问题,如今三个字端已经够繁杂了,可是若是20个字端,那代码冗余度就很高了,因此这是最不推荐的作法。数组

使用工具类

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

这里注意,阿里手册上是不推荐使用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;
}
复制代码
相关文章
相关标签/搜索