导语
自2013年毕业后,今年已经是我工作的第4个年头了,总在做java相关的工作,终于有时间坐下来,写一篇关于java写法的一篇文章,来探讨一下如果你真的是一个java程序员,那你真的会写java吗?
笔者是一个务实的程序员,故本文绝非扯淡文章,文中内容都是干货,望读者看后,能有所收获。
文章核心
其实,本不想把标题写的那么恐怖,只是发现很多人干了几年java以后,都自认为是一个不错的java程序员了,可以拿着上万的工资都处宣扬自己了,写这篇文章的目的并不是嘲讽和我一样做java的同行们,只是希望读者看到此骗文章后,可以和我一样,心平气和的争取做一个优秀的程序员。
讲述方向
由于一直从事移动互联网相关工作,java开发中经常和移动端打交道或者做一些后端的工作,所以本篇文章更可能涉及和移动端的交互或者与后端的交互方式,笔者希望以自身的一些学习经验或者开发经验,可以带动认真阅读本篇文章的读者们,让大家对java有一个更好的态度去学习它,它不只是一个赚钱的工具而已。
笔者身边有很多与笔者年龄相仿或年龄更大的朋友或同事,经常有人问我:“你现在还在学习吗?我觉得没什么好学的,这些东西都差不多”,我总是回答只要有时间,我就要看一会书,这个时候,大家都会露出一副不屑的眼神或笑容。其实,非常能理解身边朋友或同事的看法,以目前状态来讲,大多都是工作至少5年的程序员了,对于公司大大小小的业务需要,以目前的知识储备来讲,都可以轻松应对,“没有什么好学的”其实这句话没有多大的问题,但是,如果你对编程还有一点点兴趣,只是不知道如何努力或改进,希望本篇文章可以帮到你。
技术点
本文不是一个吹嘘的文章,不会讲很多高深的架构,相反,会讲解很多基础的问题和写法问题,如果读者自认为基础问题和写法问题都是不是问题,那请忽略这篇文章,节省出时间去做一些有意义的事情。
开发工具
不知道有多少”老”程序员还在使用eclipse,这些程序员们要不就是因循守旧,要不就是根本就不知道其他好的开发工具的存在,eclipse吃内存卡顿的现象以及各种偶然莫名异常的出现,都告知我们是时候寻找新的开发工具了。
更换IDE
根本就不想多解释要换什么样的IDE,如果你想成为一个优秀的java程序员,请更换intellij idea. 使用idea的好处,请搜索谷歌。
别告诉我快捷键不好用
更换IDE不在我本文的重点内容中,所以不下想用太多的篇幅去写为什么更换IDE,请谷歌。
在这里,我只能告诉你,更换IDE只为了更好、更快的写好java代码。原因略。
别告诉我快捷键不好用,请尝试新事物。
bean
bean使我们使用最多的模型之一,我将以大篇幅去讲解bean,希望读者好好体会。
domain包名
根据很多java程序员的”经验”来看,一个数据库表则对应着一个domain对象,所以很多程序员在写代码时,包名则使用:com.xxx.domain ,这样写好像已经成为了行业的一种约束,数据库映射对象就应该是domain。但是你错了,domain是一个领域对象,往往我们再做传统java软件web开发中,这些domain都是贫血模型,是没有行为的,或是没有足够的领域模型的行为的,所以,以这个理论来讲,这些domain都应该是一个普通的entity对象,并非领域对象,所以请把包名改为:com.xxx.entity。
如果你还不理解我说的话,请看一下Vaughn Vernon出的一本叫做《IMPLEMENTING DOMAIN-DRIVEN DESIGN》(实现领域驱动设计)这本书,书中讲解了贫血模型与领域模型的区别,相信你会受益匪浅。
DTO
数据传输我们应该使用DTO对象作为传输对象,这是我们所约定的,因为很长时间我一直都在做移动端api设计的工作,有很多人告诉我,他们认为只有给手机端传输数据的时候(input or output),这些对象成为DTO对象。请注意!这种理解是错误的,只要是用于网络传输的对象,我们都认为他们可以当做是DTO对象,比如电商平台中,用户进行下单,下单后的数据,订单会发到OMS 或者 ERP系统,这些对接的返回值以及入参也叫DTO对象。
我们约定某对象如果是DTO对象,就将名称改为XXDTO,比如订单下发OMS:OMSOrderInputDTO。
DTO转化
正如我们所知,DTO为系统与外界交互的模型对象,那么肯定会有一个步骤是将DTO对象转化为BO对象或者是普通的entity对象,让service层去处理。
场景
比如添加会员操作,由于用于演示,我只考虑用户的一些简单数据,当后台管理员点击添加用户时,只需要传过来用户的姓名和年龄就可以了,后端接受到数据后,将添加创建时间和更新时间和默认密码三个字段,然后保存数据库。
- @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());
-
- return userService.addUser(user);
- }
- }
我们只关注一下上述代码中的转化代码,其他内容请忽略:
- User user = new User();
- user.setUsername(userInputDTO.getUsername());
- user.setAge(userInputDTO.getAge());
请使用工具
上边的代码,从逻辑上讲,是没有问题的,只是这种写法让我很厌烦,例子中只有两个字段,如果有20个字段,我们要如何做呢? 一个一个进行set数据吗?当然,如果你这么做了,肯定不会有什么问题,但是,这肯定不是一个最优的做法。
网上有很多工具,支持浅拷贝或深拷贝的Utils. 举个例子,我们可以使用org.springframework.beans.BeanUtils#copyProperties对代码进行重构和优化:
- @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程序员,请为你想做的抽象接口,做好泛型吧。
我们再来看接口实现:
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);
- }
- }
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这样的定义:
- 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 implements DTOConvert<UserInputDTO,User> {
- @Override
- public User convert(UserInputDTO userInputDTO) {
- User user = new User();
- BeanUtils.copyProperties(userInputDTO,user);
- return user;
- }
- }
修改后:
- private static class UserInputDTOConvert extends Converter
, 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:
- public class UserDTO {
- 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(){
- 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) {
- 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对象是不同的,那么你需要更明显的告诉程序:逆向是无法调用的:
- private static class UserDTOConvert extends Converter
, User> { - @Override
- protected User doForward(UserDTO userDTO) {
-
原文:https://blog.csdn.net/slw213106/article/details/121697268