Java开发规范-一篇关于提高开发体验的分享记录

本文一篇公司内部培训分享文稿,脱敏后也分享给大家~

前言

大家有没有想过程序员为什么这么累,其实作为一个程序员对于个人来说,技术很重要,但是对于工作来说,编码的习惯比技术更加主要。工作中你面试的大部分技术都不需要用到的。工作中,因为你的编码习惯不好,写的代码质量差,代码冗余重复多,很多无关的代码和业务代码搅在一起,导致了你疲于奔命应付各种问题。

良好的编码习惯加上各种开源的工具可以是提高我们的生产效率,减少很多不必要的加班时间。衡量一个开发人员的产出、质量和效率不是流水线一样计计件越多越好,在我看来有以下几点

  1. 可阅读性 (团队开发,高效协作)
  2. 可拓展性 (面对需求更变,高效开发)
  3. 健硕性 (少点BUG,就算出了BUG更容易定位问题)

本次会议的目标

  • 改善大家编码习惯,提高代码可读性与开发效率,降低维护与问题定位的成本
  • 使用开源框架降低不必要的重复造轮子时间

编码规范

装一个代码规范插件

首先推荐装一个阿里的插件:alibaba-java-coding-guidelines,可以帮你更正很多不健康的写法

image.png

image.png

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,像多条件查询的方法,一定要建对象,不然以后加需求就无线叠加方法参数,后果就是:

image.png

另外请控制代码长度,太长的方法请学会换行,原则就是不能出现滚动条,例:

/**
 * 获取所有表单模板
 * @param formId
 * @param onlyShowChecked 只返回需填的字段
 * @return
 */
List<PduFormListResult> getFormDetail(
        @Param("formId") int formId,
        @Param("onlyShowChecked") boolean onlyShowChecked
);
复制代码

控制代码高度

一个类的代码与方法不宜过多,如果可以预见的是一个类会有很多方法,应该根据单一设计原则进行拆分,比如教师拥有教师资料、教师下单、教师数据同步等等,拆分成多个Service,不建议一个Service完成所有的职责,一个文件行数超过千行后,维护成本会渐渐变高。

一个方法的行数也不应该太多,根据阿里的规范,一个方法超过80行就应该拆分出来。

异常处理原则

能不使用try catch就不要使用,大家不要害怕有异常就隐藏掉它,只要交给全局异常处理器就可以了,一切异常往外抛

/**
 * 不推荐
 */
@PostMapping("/test1")
public AjaxResult<TeacherSyncDetailDTO> test1(HttpServletRequest request) {
    try {
        //业务代码
        return AjaxResult.success();
    } catch (Exception e) {
        return AjaxResult.fail("错误啦");
    }
}
​
/**
 * 推荐
 */
@PostMapping("/test2")
public AjaxResult<TeacherSyncDetailDTO> test2(String id) {
    if (1 != 1) {
        throw new NsbCommonException("错误啦");
    }
    return AjaxResult.success();
}
复制代码

原因:业务代码不应该自己捕获异常,不要关心异常怎么处理,交给全局处理器来处理,这也是职责的问题,解耦代码,错误的日志、邮件通知等等的逻辑可以交给全局异常处理器记录,这样就可以做到代码的复用,更重要的是代码美观,可读性更强。

比方说我要对某个异常出现时特殊处理,比如发邮件,如果用trycatch那是不是我要写很多重复的代码?就算你封装成一个方法,那也得在很多的catch中调用,那也一点都不优雅。

//例:业务异常,尽情往外抛
@Transactional
public void submitUnConfirm(OrderTourConfirmDTO dto) {
    OrderBaseModel base = orderBaseService.getByOrderNum(dto.getOrdernumber());
    OrderTourModel order = this.getByOrderNum(dto.getOrdernumber());
    if (!order.getStatus().equals(OrderTourStatusConst.REVIEW)) {
        throw new CommonException("提交失败,该订单非审核中状态");
    }
    if (base.getAdultQty() == null || base.getAdultQty() == 0) {
        throw new CommonException("提交失败,请填写订单的成人数、儿童数");
    }
    if (base.getPduId() == null) {
        throw new CommonException("提交失败,请选择商品和规格");
    }
​
    order.setStatus(OrderTourStatusConst.APPOINT);
    order.setSubmitTime(new Date());
    this.updateById(order);
}
复制代码

合理的注释

这个大家都懂,只要你维护过没有注释的代码你就明白这个事情的重要性。

在此提出几个建议

  • 重要流程请加上注释
  • 无用的代码请删掉,不要一顿注释就完事
  • 废弃但不能删除的代码请使用@Deprecated告知
  • 可分模块的流程善用分割符=========

优雅的注释可以降低很多维护成本

关于service层的写法

我们在定义Service层的时候有两种常规做法:

  1. 直接一个 service 实现类
  2. 定义service接口 + serviceImpl 实现类

思考1:使用service

在日常业务开发中,在常规的三层架构(controller+service+mapper)中写业务,使用service+serviceImpl其实没有带来设计上的优点和使用interface的初衷,反而带来一些不必要的工作量。

  • 开发/维护的时候我需要改2个文件,实现类也没法直观查看接口注释,浪费了一些开发时间,而且阅读的代码的时候链路多了一层
  • 在大部分业务开发场景不需要一个service一个impl,因为你的实现类基本只有一个,无法体现接口的初衷,也没有所谓的解耦

这里有一篇更完整的文章大家可以看看:mp.weixin.qq.com/s/ykEno7L5X…

所以大部分开发业务的场景,并不需要使用interface定义一层接口,如果不需要多实现,也没有用到设计模式去解耦,直接定义一个实现的Service其实开发效率更高

当然了,也有缺点就是:

  1. 不用使用接口,使用this.xx()调用本类方法会使AOP失效,如事务;当然也有解决方法,有兴趣自行百度。
  2. 当一个类的方法太多的时候,不方便查阅,但其实可以缩进,而且一个service层不应该承担太多的职责,代码行数是可控的

思考2:使用service + serviceImpl

其实网上很多开源项目大多数都是使用service + serviceImpl ,只是我觉我们日常业务开发没有把这种模式的优势发挥出来,比如:

  1. 需要用到一些设计模式,比如策略模式、工厂模式等
  2. 我和同事分别做项目的2个不同功能模块,但是同事的功能中却需要调用我这头实现的部分逻辑.为了让他有一个"占位符"可用,我可以快速的写个接口扔给他
  3. 某个service需要多继承

结论引用网络上的文章: 这些情况其实可以说是接口好处的体现,所以java有面向接口编程的建议.但是说回Service层一定要有接口吗?那到未必,因为说到底,多一个接口仅仅是扩展性和某些情况下有优势,但是是否会用到接口的便利性,不确定的情况下我们未必一定要为"可能"买单.只是多写那几行代码,付出一点就可能避免"未来"的大"麻烦",何乐而不为

所以这里给到的建议是根据实际情况按需使用。

工具类编写规范

相信大家都知道Hutool这个神器,建议写工具类的时候去看看这个文档,如果有重复的就不要再造轮子了

但是如果遇到没有的呢,比如在做学段学科需求的时候发现,CollUtil就没有取两List的笛卡尔积,那请新建一个ExtCollUtil,并且继承它,这个类即同时拥有hutool的方法,也有自定义的方法。

image.png

PS:其实公司应该有自己一个工具工程,原则上开发人员不可以偷偷在各自的工程写Util,若有需要提PR到专门的工具类工程,可以有效降低重复造轮子的时间

Lombok 更优雅的用法

Lombok相信大家都会用,但除了@Data 还有更多优雅的用法

  1. 在对象上加上注释@Accessors(chain = true) ,可以使用链式写法
this.save(
        new MemTeacherEntity()
                .setId("id")
                .setTeacherTypeName("名字")
);
复制代码
  1. @RequiredArgsConstructor 替代@Autowired构造注入,多个bean 注入时更加清晰
@RequiredArgsConstructor
@Service
public class MemTeacherSyncService {
    private final MemTeacherService memTeacherService;
    private final MemTeacherEduService memTeacherEduService;
}    
复制代码
  1. @CleanUp 清理流对象,不用手动去关闭流

    @Cleanup
    OutputStream outStream = new FileOutputStream(new File("text.txt"));
    @Cleanup
    InputStream inStream = new FileInputStream(new File("text2.txt"));
    byte[] b = new byte[65536];
    while (true) {
       int r = 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
复制代码
  1. 使用modular/modules根据业务对类进行拆分
  2. controller比较特殊类似聚合层,可以按by-biz拆分,也可按by-tech拆分,根据业务情况选择即可
  3. 通用的util、异常、处理器、拦截器等等放在common包

如何选择

在我以往的项目中我是采用第二种,理由如下

  1. 开发方便,只需在一个包下即可开发,左侧的IDE菜单无需切来切去
  2. 要添加或者移除一块业务时通常更加方便。比如大的应用做拆分,一般都是按照业务功能拆分的,则直接拆出某个包到新应用即可
  3. 通过应用的包结构目录,就能大致知道这个模块在做什么,贴近DDD的思想,以controller作为聚合层、module作为领域层,结构清晰

《聊一聊DDD应用的代码结构》 一文中作者写到:

按照业务来分包的思路在网上占绝对优势

所以建议选择方案二

猜你喜欢

转载自juejin.im/post/7014334915261153294