前言
大家有没有想过程序员为什么这么累,其实作为一个程序员对于个人来说,技术很重要,但是对于工作来说,编码的习惯比技术更加主要。工作中你面试的大部分技术都不需要用到的。工作中,因为你的编码习惯不好,写的代码质量差,代码冗余重复多,很多无关的代码和业务代码搅在一起,导致了你疲于奔命应付各种问题。
良好的编码习惯加上各种开源的工具可以是提高我们的生产效率,减少很多不必要的加班时间。衡量一个开发人员的产出、质量和效率不是流水线一样计计件越多越好,在我看来有以下几点
可阅读性 (团队开发,高效协作)
可拓展性 (面对需求更变,高效开发)
健硕性 (少点BUG,就算出了BUG更容易定位问题)
本次会议的目标
改善大家编码习惯,提高代码可读性与开发效率,降低维护与问题定位的成本
使用开源框架降低不必要的重复造轮子时间
编码规范
装一个代码规范插件
首先推荐装一个阿里的插件:alibaba-java-coding-guidelines,可以帮你更正很多不健康的写法
POJO类命名规范
概念:像DTO、 Entity等业务对象统称POJO
关于对象类的后缀,业界没有一个硬性统一,只要做到好理解,同一个项目规范统一就好了,比如实体类,有的项目叫Entity,有的叫Model。以下是我个人觉得最优的方案:
视图对象 Result:返参结果,例:OrderResult
视图对象 VO:用法跟Result一致,个人觉得Result更有辨识度,所以推荐使用 Result
数据对象 Entity:跟数据表名对应,例:OrderEntity
业务对象 BO:内部使用的类,无需暴露在外面(不能在controller使用)
关于CRUD操作相关DTO的全命名方式推荐格式: {业务单词} {动作} {后缀} .java
例:TeacherQueryDTO、TeacherUpdateDTO
反例:ClasscourseDTO (无法从命名获得业务场景)
Controller编写规范
接口返参一定要使用泛型,除非只是返回成功或失败的操作,如:Result<此处一定要写返回的类>
原因:可读性强,调用方可直接查看返回的对象属性,并且Swagger、smart-doc等框架支持自动生成文档
Controller原则上只能做参数校验、属性补充、格式转换等操作,不能写业务逻辑,就算有也不能超过5行
原因:解耦,职责问题,不同的类有不同的职责
领域层若只是返回简单的数据,不需要错误提示的接口,如获取xx详情,可不使用Result<>,直接return对象即可
原因:若不需要错误提示信息,没必要再包一层,直接返回所需对象即可
不建议在Controller类加上@RequestMapping,请直接在方法的@PostMapping或@GetMapping 写接口全路径
原因:方便搜索 且 方便根据业务拆分重构Controller时可随意copy 移动代码
禁止使用request.getParameter()获得参数,请直接声明在方法体,若参数过多请使用DTO
原因:可读性,且符合文档自动生成规则
禁止使用Map/JsonObject入参或返参
请不要为了方便就这样写,不利于阅读与维护
不建议使用@PathVariable
原因:调试时(浏览器F12)无法直观查看接口的参数名是什么;spring mvc下非RESTful的风格的接口响应性能会比RESTful风格高2倍(因为涉及正则解析);另外对URL进行权限控制的时候也不好做
控制代码宽度
方法参数数量请控制在4个内,如果不是99%肯定以后不会加参数,请建一个DTO,像多条件查询的方法,一定要建对象,不然以后加需求就无线叠加方法参数,后果就是:
另外请控制代码长度,太长的方法请学会换行,原则就是不能出现滚动条,例:
/***获取所有表单模板*@paramformId*@paramonlyShowChecked只返回需填的字段*@return*/List<PduFormListResult>getFormDetail(@Param("formId")intformId,@Param("onlyShowChecked")booleanonlyShowChecked);
控制代码高度
一个类的代码与方法不宜过多,如果可以预见的是一个类会有很多方法,应该根据单一设计原则进行拆分,比如教师拥有教师资料、教师下单、教师数据同步等等,拆分成多个Service,不建议一个Service完成所有的职责,一个文件行数超过千行后,维护成本会渐渐变高。
一个方法的行数也不应该太多,根据阿里的规范,一个方法超过80行就应该拆分出来。
异常处理原则
能不使用try catch就不要使用,大家不要害怕有异常就隐藏掉它,只要交给全局异常处理器就可以了,一切异常往外抛
/***不推荐*/@PostMapping("/test1")publicAjaxResult<TeacherSyncDetailDTO>test1(HttpServletRequestrequest){try{//业务代码returnAjaxResult.success();}catch(Exceptione){returnAjaxResult.fail("错误啦");}}/***推荐*/@PostMapping("/test2")publicAjaxResult<TeacherSyncDetailDTO>test2(Stringid){if(1!=1){thrownewNsbCommonException("错误啦");}returnAjaxResult.success();}
原因:业务代码不应该自己捕获异常,不要关心异常怎么处理,交给全局处理器来处理,这也是职责的问题,解耦代码,错误的日志、邮件通知等等的逻辑可以交给全局异常处理器记录,这样就可以做到代码的复用,更重要的是代码美观,可读性更强。
比方说我要对某个异常出现时特殊处理,比如发邮件,如果用trycatch那是不是我要写很多重复的代码?就算你封装成一个方法,那也得在很多的catch中调用,那也一点都不优雅。
//例:业务异常,尽情往外抛@TransactionalpublicvoidsubmitUnConfirm(OrderTourConfirmDTOdto){OrderBaseModelbase=orderBaseService.getByOrderNum(dto.getOrdernumber());OrderTourModelorder=this.getByOrderNum(dto.getOrdernumber());if(!order.getStatus().equals(OrderTourStatusConst.REVIEW)){thrownewCommonException("提交失败,该订单非审核中状态");}if(base.getAdultQty()==null||base.getAdultQty()==0){thrownewCommonException("提交失败,请填写订单的成人数、儿童数");}if(base.getPduId()==null){thrownewCommonException("提交失败,请选择商品和规格");}order.setStatus(OrderTourStatusConst.APPOINT);order.setSubmitTime(newDate());this.updateById(order);}
合理的注释
这个大家都懂,只要你维护过没有注释的代码你就明白这个事情的重要性。
在此提出几个建议
重要流程请加上注释
无用的代码请删掉,不要一顿注释就完事
废弃但不能删除的代码请使用@Deprecated告知
可分模块的流程善用分割符========= 优雅的注释可以降低很多维护成本
关于service层的写法
我们在定义Service层的时候有两种常规做法:
直接一个 service 实现类
定义service接口 + serviceImpl 实现类
思考1:使用service
在日常业务开发中,在常规的三层架构(controller+service+mapper)中写业务,使用service+serviceImpl其实没有带来设计上的优点和使用interface的初衷,反而带来一些不必要的工作量。
开发/维护的时候我需要改2个文件,实现类也没法直观查看接口注释,浪费了一些开发时间,而且阅读的代码的时候链路多了一层
在大部分业务开发场景不需要一个service一个impl,因为你的实现类基本只有一个,无法体现接口的初衷,也没有所谓的解耦
这里有一篇更完整的文章大家可以看看:https://mp.weixin.qq.com/s/ykEno7L5Xr1VHQ-ItQ-FYQ
所以大部分开发业务的场景,并不需要使用interface定义一层接口,如果不需要多实现,也没有用到设计模式去解耦,直接定义一个实现的Service其实开发效率更高
当然了,也有缺点就是:
不用使用接口,使用this.xx()调用本类方法会使AOP失效,如事务;当然也有解决方法,有兴趣自行百度。
当一个类的方法太多的时候,不方便查阅,但其实可以缩进,而且一个service层不应该承担太多的职责,代码行数是可控的
思考2:使用service + serviceImpl
其实网上很多开源项目大多数都是使用service + serviceImpl ,只是我觉我们日常业务开发没有把这种模式的优势发挥出来,比如:
需要用到一些设计模式,比如策略模式、工厂模式等
我和同事分别做项目的2个不同功能模块,但是同事的功能中却需要调用我这头实现的部分逻辑.为了让他有一个"占位符"可用,我可以快速的写个接口扔给他
某个service需要多继承
结论引用网络上的文章: 这些情况其实可以说是接口好处的体现,所以java有面向接口编程的建议.但是说回Service层一定要有接口吗?那到未必,因为说到底,多一个接口仅仅是扩展性和某些情况下有优势,但是是否会用到接口的便利性,不确定的情况下我们未必一定要为"可能"买单.只是多写那几行代码,付出一点就可能避免"未来"的大"麻烦",何乐而不为
所以这里给到的建议是根据实际情况按需使用。
工具类编写规范
相信大家都知道Hutool这个神器,建议写工具类的时候去看看这个文档,如果有重复的就不要再造轮子了
但是如果遇到没有的呢,比如在做学段学科需求的时候发现,CollUtil就没有取两List的笛卡尔积,那请新建一个ExtCollUtil,并且继承它,这个类即同时拥有hutool的方法,也有自定义的方法。
PS:其实公司应该有自己一个工具工程,原则上开发人员不可以偷偷在各自的工程写Util,若有需要提PR到专门的工具类工程,可以有效降低重复造轮子的时间
Lombok 更优雅的用法
Lombok相信大家都会用,但除了@Data 还有更多优雅的用法
在对象上加上注释@Accessors(chain = true)
,可以使用链式写法
this.save(newMemTeacherEntity().setId("id").setTeacherTypeName("名字"));
@RequiredArgsConstructor
替代@Autowired
构造注入,多个bean 注入时更加清晰
@RequiredArgsConstructor@ServicepublicclassMemTeacherSyncService{privatefinalMemTeacherServicememTeacherService;privatefinalMemTeacherEduServicememTeacherEduService;}
@CleanUp
清理流对象,不用手动去关闭流
@CleanupOutputStreamoutStream=newFileOutputStream(newFile("text.txt"));@CleanupInputStreaminStream=newFileInputStream(newFile("text2.txt"));byte[]b=newbyte[65536];while(true){intr=inStream.read(b);if(r==-1)break;outStream.write(b,0,r);}
包分级与类的存放
合理的代码结构可有效降低代码的维护成本,在常规的项目中一般有2种存放的思路
按照技术角度或者文件类型来分(by-tech or by-type),下文简称by-tech
按照业务功能来分(by-feature or by-business),下文简称by-biz
先了解一下这两者具体是什么
by-tech
以文件类型作为顶层包,其次再以业务进行划分
com└─github└─heys1├─common│├─exction│└─util├─controller│├─student│└─teacher└─service├─student└─teacher
by-biz
以业务作为顶层包,其次再以文件类型进行划分
com└─github└─heys1├─common│├─exction│└─util├─controller│├─student│└─teacher└─modular├─student│├─dto│├─mapper│└─service└─teacher├─dto├─mapper└─service
使用modular/modules根据业务对类进行拆分
controller比较特殊类似聚合层,可以按by-biz拆分,也可按by-tech拆分,根据业务情况选择即可
通用的util、异常、处理器、拦截器等等放在common包
如何选择
在我以往的项目中我是采用第二种,理由如下:
开发方便,只需在一个包下即可开发,左侧的IDE菜单无需切来切去;
要添加或者移除一块业务时通常更加方便。比如大的应用做拆分,一般都是按照业务功能拆分的,则直接拆出某个包到新应用即可;
通过应用的包结构目录,就能大致知道这个模块在做什么,贴近DDD的思想,以controller作为聚合层、module作为领域层,结构清晰。
在《聊一聊DDD应用的代码结构》 一文中作者写到:
按照业务来分包的思路在网上占绝对优势
所以建议选择方案二。