美文网首页
让Java代码变得更优雅(二)

让Java代码变得更优雅(二)

作者: 紫厢雨 | 来源:发表于2020-01-12 00:06 被阅读0次

转载 https://mp.weixin.qq.com/s/f5AtQU1TgNWym2aHsKK5CA 作者:SnailClimb

DTO

数据传输我们应该使用DTO对象作为传输对象,这是我们所约定的,因为很长时间我一直都在做移动端api设计的工作,有很多人告诉我,他们认为只有给手机端传输数据的时候(input or output),这些对象成为DTO对象。请注意!这种理解是错误的,只要是用于网络传输的对象,我们都认为他们可以当做是DTO对象,比如电商平台中,用户进行下单,下单后的数据,订单会发到OMS 或者 ERP系统,这些对接的返回值以及入参也叫DTO对象。

@PostMapping
 public User addUser(UserInputDTO userInputDTO){
         User user = convertFor(userInputDTO);

         return userService.addUser(user);
 }
 private User convertFor(UserInputDTO userInputDTO){

         User user = new User();
         BeanUtils.copyProperties(userInputDTO,user);
         return user;
 }

这是一个更好的语义写法,虽然他麻烦了些,但是可读性大大增加了,在写代码时,我们应该尽量把语义层次差不多的放到一个方法中,比如:

User user = convertFor(userInputDTO);
return userService.addUser(user);

这两段代码都没有暴露实现,都是在讲如何在同一个方法中,做一组相同层次的语义操作,而不是暴露具体的实现。
如上所述,是一种重构方式,读者可以参考Martin Fowler的《Refactoring Imporving the Design of Existing Code》(重构 改善既有代码的设计) 这本书中的Extract Method重构方式。

抽象接口定义

当实际工作中,完成了几个api的DTO转化时,我们会发现,这样的操作有很多很多,那么应该定义好一个接口,让所有这样的操作都有规则的进行。如果接口被定义以后,那么convertFor这个方法的语义将产生变化,他将是一个实现类。

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

我们这样重构后,我们发现现在的代码是如此的简洁,并且那么的规范:

@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);
    }
}

 public class UserInputDTO {
    private String username;
    private int age;
    
    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 User convertToUser(){
        UserInputDTOConvert userInputDTOConvert = new UserInputDTOConvert();
        User convert = userInputDTOConvert.convert(this);
        return convert;
    }

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

}

再查工具类

再来看DTO内部转化的代码,它实现了我们自己定义的DTOConvert接口,但是这样真的就没有问题,不需要再思考了吗?我觉得并不是,对于Convert这种转化语义来讲,很多工具类中都有这样的定义,这中Convert并不是业务级别上的接口定义,它只是用于普通bean之间转化属性值的普通意义上的接口定义,所以我们应该更多的去读其他含有Convert转化语义的代码。我仔细阅读了一下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);
    //其他略
}

从源码可以了解到,GUAVA中的Convert可以完成正向转化和逆向转化,继续修改我们DTO中转化的这段代码:(修改之后的代码)

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对象是不同的,那么你需要更明显的告诉程序:逆向是无法调用的:

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验证

很多人会告诉我,如果这些api是提供给前端进行调用的,前端都会进行验证啊,你为什还要验证?其实答案是这样的,我从不相信任何调用我api或者方法的人,比如前端验证失败了,或者某些人通过一些特殊的渠道(比如Charles进行抓包),直接将数据传入到我的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 带验证码的验证错误异常
     }
}

拥抱lombok

bean中的链式风格

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;
    }
}
Student student = new Student()
        .setAge(24)
        .setName("zs");
@Accessors(chain = true)
@Setter
@Getter
public class Student {
    private String name;
    private int age;
}

再回过头来看刚刚的Student,很多时候,我们去写Student这个bean的时候,他会有一些必输字段,比如Student中的name字段,一般处理的方式是将name字段包装成一个构造方法,只有传入name这样的构造方法,才能创建一个Student对象。
接上上边的静态构造方法和必传参数的构造方法,使用lombok将更改成如下写法(@RequiredArgsConstructor和 @NonNull):

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

测试代码

Student student = Student.ofName("zs");

这样构建出的bean语义是否要比直接new一个含参的构造方法(包含 name的构造方法)要好很多。
当然,看过很多源码以后,我想相信将静态构造方法ofName换成of会先的更加简洁:

@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);

使用builder

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();

这样的builder代码,让我是在恶心难受,于是我打算用lombok重构这段代码:

@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 <T> RestResponseDTO<T> postForEntityWithNoException(String url, Object request, Class<T> responseType, Object... uriVariables)
                    throws RestClientException {
            RestResponseDTO<T> restResponseDTO = new RestResponseDTO<T>();
            ResponseEntity<T> tResponseEntity;
            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> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException {
            return restTemplate.getForObject(url,responseType,uriVariables);
    }

    @Override
    public <T> T getForObject(String url, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException {
            return restTemplate.getForObject(url,responseType,uriVariables);
    }

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

    @Override
    public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables) throws RestClientException {
            return restTemplate.getForEntity(url,responseType,uriVariables);
    }
    //其他实现代码略。。。
}

包装器ExtractRestTemplate很完美的更改了异常抛出的行为,让程序更具有容错性。在这里我们不考虑ExtractRestTemplate完成的功能,让我们把焦点放在FilterRestTemplate上,“实现RestOperations所有的接口”,这个操作绝对不是一时半会可以写完的,当时在重构之前我几乎写了半个小时,如下:

public abstract class FilterRestTemplate implements RestOperations {

    protected volatile RestTemplate restTemplate;

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

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

    @Override
    public <T> T getForObject(String url, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException {
            return restTemplate.getForObject(url,responseType,uriVariables);
    }

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

    @Override
    public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables) throws RestClientException {
            return restTemplate.getForEntity(url,responseType,uriVariables);
    }
    //其他实现代码略。。。
}

我相信你看了以上代码,你会和我一样觉得恶心反胃,后来我用lombok提供的代理注解优化了我的代码(@Delegate):

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

相关文章

  • 让Java代码变得更优雅(二)

    转载 https://mp.weixin.qq.com/s/f5AtQU1TgNWym2aHsKK5CA 作者:S...

  • RxSwift(1)- 让代码变得更优雅

    RAC让我们的OC开发过程变得更加的方便、快捷,更加的面向开发者,那么作为苹果主推的swift,是否也有如此便捷的...

  • Lombok:让JAVA代码更优雅

    Lombok:让JAVA代码更优雅关于Lombok,其实在网上可以找到很多如何使用的文章,但是很少能找到比较齐全的...

  • 让java代码变的更优雅

    前言 在我们平常开发过程中,由于项目时间紧张,代码可以用就好,往往会忽视代码的质量问题。甚至有些复制粘贴过来,不加...

  • java 优雅的使用lambda表达式和闭包

    java 1.8 加入了lambda的语法,使用lambda语法糖可以让我们的代码更简洁优雅,写起来更爽。下面看个...

  • Java枚举的几个用法探索

    枚举是java 5之后添加的一个重要特性,这个特性可以让java程序员写出更优雅、合理的代码。 1. 枚举基本用法...

  • 代码优化

    参考: 复杂判断的优雅写法简单几步让你的 JS 写得更漂亮优化 if-else 代码结构 一、复杂判断的优雅写法8...

  • Day04

    Day04 布局一 布局二 布局三 布局四 java代码一 java代码二 java代码三 main代码 移除fr...

  • 如何写单元测试

    二话不说,先上结论王道: 测试代码的优雅和产品代码的优雅,两者一般不可得兼,舍测试代码的优雅而取产品代码的优雅,方...

  • 写出优雅的Java代码

    1 使用 IntelliJ IDEA 作为您的集成开发环境 (IDE) 使用 IntelliJ IDEA。我并不是...

网友评论

      本文标题:让Java代码变得更优雅(二)

      本文链接:https://www.haomeiwen.com/subject/qnzbactx.html