细思极恐,你真的会写java吗

@PostMapping

public User addUser(UserInputDTO userInputDTO){

User user = new User();

BeanUtils.copyProperties(userInputDTO,user);

return userService.addUser(user);

}

BeanUtils.copyProperties是一个浅拷贝方法,复制属性时,我们只需要把DTO对象和要转化的对象两个的属性值设置为一样的名称,并且保证一样的类型就可以了。如果你在做DTO转化的时候一直使用set进行属性赋值,那么请尝试这种方式简化代码,让代码更加清晰!

转化的语义

上边的转化过程,读者看后肯定觉得优雅很多,但是我们再写java代码时,更多的需要考虑语义的操作,再看上边的代码:

User user = new User();

BeanUtils.copyProperties(userInputDTO,user);

虽然这段代码很好的简化和优化了代码,但是他的语义是有问题的,我们需要提现一个转化过程才好,所以代码改成如下:

@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 interface DTOConvert<S,T> {

T convert(S s);

}

虽然这个接口很简单,但是这里告诉我们一个事情,要去使用泛型,如果你是一个优秀的java程序员,请为你想做的抽象接口,做好泛型吧。

我们再来看接口实现:

publicclass UserInputDTOConvert implements DTOConvert {

@Override

public User convert(UserInputDTO userInputDTO) {

User user = new User();

BeanUtils.copyProperties(userInputDTO,user);

return user;

}

}

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

@RequestMapping(“/v1/api/user”)

@RestController

publicclass UserApi {

@Autowired

private UserService userService;

@PostMapping

public User addUser(UserInputDTO userInputDTO){

User user = new UserInputDTOConvert().convert(userInputDTO);

return userService.addUser(user);

}

}

review code

如果你是一个优秀的java程序员,我相信你应该和我一样,已经数次重复review过自己的代码很多次了。我们再看这个保存用户的例子,你将发现,api中返回值是有些问题的,问题就在于不应该直接返回User实体,因为如果这样的话,就暴露了太多实体相关的信息,这样的返回值是不安全的,所以我们更应该返回一个DTO对象,我们可称它为UserOutputDTO:

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

}

这样你的api才更健全。

不知道在看完这段代码之后,读者有是否发现还有其他问题的存在,作为一个优秀的java程序员,请看一下这段我们刚刚抽象完的代码:

User user = new UserInputDTOConvert().convert(userInputDTO);

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

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;

}

}

}

然后api中的转化则由:

User user = new UserInputDTOConvert().convert(userInputDTO);

User saveUserResult = userService.addUser(user);

变成了:

User user = userInputDTO.convertToUser();

User saveUserResult = userService.addUser(user);

我们再DTO对象中添加了转化的行为,我相信这样的操作可以让代码的可读性变得更强,并且是符合语义的。

再查工具类

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

publicabstractclass Converter<A, B> implements Function<A, B> {

protected abstract B doForward(A a);

protected abstract A doBackward(B b);

//其他略

}

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

privatestaticclass UserInputDTOConvert implements DTOConvert<UserInputDTO,User> {

@Override

public User convert(UserInputDTO userInputDTO) {

User user = new User();

BeanUtils.copyProperties(userInputDTO,user);

return user;

}

}

修改后:

privatestaticclass 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;

}

}

看了这部分代码以后,你可能会问,那逆向转化会有什么用呢?其实我们有很多小的业务需求中,入参和出参是一样的,那么我们变可以轻松的进行转化,我将上边所提到的UserInputDTO和UserOutputDTO都转成UserDTO展示给大家:

DTO:

publicclass UserDTO {

private String username;

privateint 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(){

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;

}

privatestaticclass 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;

}

}

}

api:

@PostMapping

public UserDTO addUser(UserDTO userDTO){

User user = userDTO.convertToUser();

User saveResultUser = userService.addUser(user);

UserDTO result = userDTO.convertFor(saveResultUser);

return result;

}

当然,上述只是表明了转化方向的正向或逆向,很多业务需求的出参和入参的DTO对象是不同的,那么你需要更明显的告诉程序:逆向是无法调用的:

privatestaticclass 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) {

thrownew AssertionError(“不支持逆向转化方法!”);

}

}

看一下doBackward方法,直接抛出了一个断言异常,而不是业务异常,这段代码告诉代码的调用者,这个方法不是准你调用的,如果你调用,我就”断言”你调用错误了。

bean的验证

如果你认为我上边写的那个添加用户api写的已经非常完美了,那只能说明你还不是一个优秀的程序员。我们应该保证任何数据的入参到方法体内都是合法的。

为什么要验证

很多人会告诉我,如果这些api是提供给前端进行调用的,前端都会进行验证啊,你为什还要验证?其实答案是这样的,我从不相信任何调用我api或者方法的人,比如前端验证失败了,或者某些人通过一些特殊的渠道(比如Charles进行抓包),直接将数据传入到我的api,那我仍然进行正常的业务逻辑处理,那么就有可能产生脏数据!“对于脏数据的产生一定是致命”,这句话希望大家牢记在心,再小的脏数据也有可能让你找几个通宵!

jsr 303验证

hibernate提供的jsr 303实现,我觉得目前仍然是很优秀的,具体如何使用,我不想讲,因为谷歌上你可以搜索出很多答案! 再以上班的api实例进行说明,我们现在对DTO数据进行检查:

publicclass UserDTO {

@NotNull

private String username;

@NotNull

privateint age;

//其他代码略

}

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 官方文档

拥抱lombok

上边的DTO代码,已经让我看的很累了,我相信读者也是一样,看到那么多的Getter和Setter方法,太烦躁了,那时候有什么方法可以简化这些呢。请拥抱lombok,它会帮助我们解决一些让我们很烦躁的问题

去掉Setter和Getter

其实这个标题,我不太想说,因为网上太多,但是因为很多人告诉我,他们根本就不知道lombok的存在,所以为了让读者更好的学习,我愿意写这样一个例子:

@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<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(“不支持逆向转化方法!”);

}

}

}

看到了吧,烦人的Getter和Setter方法已经去掉了。但是上边的例子根本不足以体现lombok的强大。我希望写一些网上很难查到,或者很少人进行说明的lombok的使用以及在使用时程序语义上的说明。比如:@Data,@AllArgsConstructor,@NoArgsConstructor…这些我就不进行一一说明了,请大家自行查询资料.

bean中的链式风格

什么是链式风格?我来举个例子,看下面这个Student的bean:

publicclass Student {

private String name;

privateint age;

public String getName() {

return name;

}

public Student setName(String name) {

this.name = name;

returnthis;

}

public int getAge() {

return age;

}

public Student setAge(int age) {

returnthis;

}

}

仔细看一下set方法,这样的设置便是chain的style,调用的时候,可以这样使用:

Student student = new Student()

.setAge(24)

.setName(“zs”);

相信合理使用这样的链式代码,会更多的程序带来很好的可读性,那看一下如果使用lombok进行改善呢,请使用 @Accessors(chain = true),看如下代码:

@Accessors(chain = true)

@Setter

@Getter

publicclass Student {

private String name;

privateint age;

}

这样就完成了一个对于bean来讲很友好的链式操作。

静态构造方法

静态构造方法的语义和简化程度真的高于直接去new一个对象。比如new一个List对象,过去的使用是这样的:

List list = new ArrayList<>();

看一下guava中的创建方式:

List list = Lists.newArrayList();

Lists命名是一种约定(俗话说:约定优于配置),它是指Lists是List这个类的一个工具类,那么使用List的工具类去产生List,这样的语义是不是要比直接new一个子类来的更直接一些呢,答案是肯定的,再比如如果有一个工具类叫做Maps,那你是否想到了创建Map的方法呢:

HashMap<String, String> objectObjectHashMap = Maps.newHashMap();

好了,如果你理解了我说的语义,那么,你已经向成为java程序员更近了一步了。

再回过头来看刚刚的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

Builder模式我不想再多解释了,读者可以看一下《Head First》(设计模式) 的建造者模式。

今天其实要说的是一种变种的builder模式,那就是构建bean的builder模式,其实主要的思想是带着大家一起看一下lombok给我们带来了什么。

看一下Student这个类的原始builder状态:

publicclass Student {

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值