• 后端开发怎么做得更优秀?记住这15个好习惯


    目录

    一. 注释尽可能全面,要写有意义的注释

    二. 项目拆分合理的目录结构

    三. 尽量不在循环里远程调用或者数据库操作,优先考虑批量进行

    四. 封装方法形参

    五. 封装通用模板

    六. 封装复杂的逻辑判断条件 

    七. 保持优化性能的嗅觉

    八. 可变参数的配置化处理

    九. 会总结并使用工具类 

    十. 控制方法函数复杂度

    十一. 在 finally 块中对资源进行释放

    十二. 把日志打印好

    十三. 考虑异常,处理好异常

    十四. 考虑系统、接口的兼容性 

    十五. 采取措施避免运行时错误


    一. 注释尽可能全面,要写有意义的注释

    • 接口方法、类、复杂的业务逻辑,都应该添加有意义的注释
    1. 对于接口方法的注释,应该包含详细的入参和结果说明,有异常抛出的情况也要详细叙述;
    2. 类的注释应该包含类的功能说明、作者和修改者;
    3. 如果是业务逻辑很复杂的代码,真的非常有必要写清楚注释,越是复杂的代码逻辑,就越需要添加清楚详细的注释;
    •  清楚的注释,更有利于后面的维护

    二. 项目拆分合理的目录结构

            目前大学生学做各种项目总会尝试去做各种各样的管理系统,基本上都是采用的 MVC 模式,也就是 controller、service、mapper、entity。如果我们所做系统未来有业务扩展,而我们没有提前拆分业务结构的话,很有可能就会发现,一个 service 包下,有上百个服务!!!

            正确的做法,如果服务过多,应该根据不同的业务进行划分,比如订单、登录、积分等等:

            当然,我们也可以根据不同的业务划分模块,比如建一个 moudles 包,然后按订单、登录等业务划分,每个业务都有自己的 controller、service、mapper、entity(Iservice)。

            我们拆分的目的,是为了让项目结构更清晰,可读性更强,更容易维护。 


    三. 尽量不在循环里远程调用或者数据库操作,优先考虑批量进行

    • 远程操作或者数据库操作都是 比较耗网络、IO资源 的,所以尽量不在循环里远程调用、不在循环里操作数据库,能批量一次性去查回来尽量不要循环多次去查
    • 如果是操作数据库,也不要一次性差太多数据,可以分批500一次这样子;

    正例:

    remoteBatchQuery(param);
    

     反例:

    1. for(int i=0;i<n;i++){
    2. remoteSingleQuery(param)
    3. }

    四. 封装方法形参

    如果你的方法参数过多,要封装一个对象出来。反例如下:

    1. public void getUserInfo(String name,String age,String sex,String mobile,String idNo){
    2. // do something ...
    3. }

    如果参数很多,做新老接口兼容处理也比较麻烦。建议写个对象出来,如下:

    1. public void getUserInfo(UserInfoParamDTO userInfoParamDTO){
    2. // do something ...
    3. }
    4. class UserInfoParamDTO{
    5. private String name;
    6. private String age;
    7. private String sex;
    8. private String mobile;
    9. private String idNo;
    10. }

    五. 封装通用模板

    一个优秀的后端开发,应该具备封装通用模板的编码能力。

    我们来看一个业务需求:

            假设我们有这么一个业务场景,内部系统不同商户,调用我们系统接口,去跟外部第三方系统交互(http 方式),走类似这么一个流程,如下: 

            一个请求都会经历这几个流程:

    • 查询商户信息;

    • 对请求报文加签;

    • 发送 http 请求出去;

    • 对返回的报文验签。

            通过 HTTP 发请求出去时,有的商户可能是走代理的,有的是走直连。假设当前有 A、B 商户接入,不少伙伴可能这么实现,伪代码如下: 

    1. // 商户A处理句柄
    2. CompanyAHandler implements RequestHandler {
    3. Resp hander(req){
    4. //查询商户信息
    5. queryMerchantInfo();
    6. //加签
    7. signature();
    8. //http请求(A商户假设走的是代理)
    9. httpRequestbyProxy()
    10. //验签
    11. verify();
    12. }
    13. }
    14. // 商户B处理句柄
    15. CompanyBHandler implements RequestHandler {
    16. Resp hander(Rreq){
    17. //查询商户信息
    18. queryMerchantInfo();
    19. //加签
    20. signature();
    21. // http请求(B商户不走代理,直连)
    22. httpRequestbyDirect();
    23. // 验签
    24. verify();
    25. }
    26. }

            假设新加一个 C 商户接入,你需要再实现一套这样的代码。显然,这样代码就重复了。这时候我们可以封装一个通用模板!我们就可以定义一个抽象类,包含请求流程的几个方法,伪代码如下:

    1. abstract class AbstractMerchantService {
    2. //模板方法流程
    3. Resp handlerTempPlate(req){
    4. //查询商户信息
    5. queryMerchantInfo();
    6. //加签
    7. signature();
    8. //http 请求
    9. httpRequest();
    10. // 验签
    11. verifySinature();
    12. }
    13. // Http是否走代理(提供给子类实现)
    14. abstract boolean isRequestByProxy();
    15. }

            然后所有商户接入,都做这个流程。如果这个通用模板是你抽取的,别的小伙伴接到开发任务,都是接入你的模板,你就是给别人提供方便的大佬了~~

            封装通用模板,就是抽个模板模式吗?其实不仅仅是,而是自己对需求、代码的思考与总结,一种编程思想的升华。 


    六. 封装复杂的逻辑判断条件 

            我们来看下这段代码: 

    1. public void test(UserStatus userStatus){
    2. if (userStatus != UserStatus.BANNED && userStatus != UserStatus.DELETED && userStatus != UserStatus.FROZEN) {
    3. //doSomeThing
    4. return
    5. }
    6. }

             这段代码有什么问题呢?

            是的,逻辑判断条件太复杂啦,我们可以封装一下它。如下:

    1. public void test(UserStatus userStatus){
    2. if (isUserActive(userStatus)) {
    3. //doSomeThing
    4. }
    5. }
    6. private boolean isUserActive(UserStatus userStatus) {
    7. return userStatus != UserStatus.BANNED && userStatus != UserStatus.DELETED && userStatus != UserStatus.FROZEN;
    8. }

    七. 保持优化性能的嗅觉

            优秀的后端开发,应该保持优化性能的嗅觉。比如 避免创建不必要的对象、异步处理、使用缓冲流、减少IO操作 等等。

            比如,我们设计一个APP首页的接口,它需要 查用户信息、需要查banner信息、需要查弹窗信息 等等。假设耗时如下:

            查用户信息200ms,查 banner 信息100ms、查弹窗信息50ms,那一共就耗时350ms了。如果还查其他信息,那耗时就更大了。

            如何优化它呢?

            可以并行发起,耗时可以降为200ms。如下:


    八. 可变参数的配置化处理

            日常开发中,我们经常会遇到一些可变参数,比如 用户多少天没登录注销、运营活动或者不同节日红包皮肤切换、订单多久没付款就删除 等等。对于这些可变的参数,不应该直接写死代码。

            优秀的后端,要做配置化处理,可以把这些可变参数,放到数据库一个配置表里面,也可以放到项目的配置文件或者 Apollo 上。

            比如产品经理提了个红包需求,圣诞节的时候,红包皮肤为圣诞节相关的,春节的时候,为春节红包皮肤等。如果在代码中写死控制,可有类似以下代码:

    1. if(duringChristmas){
    2. img = redPacketChristmasSkin;
    3. }else if(duringSpringFestival){
    4. img = redSpringFestivalSkin;
    5. }

            如果到了元宵节的时候,运营小姐姐突然又有想法,红包皮肤换成灯笼相关的,这时候,是不是要去修改代码了,重新发布了?

            如果从一开始接口设计时,可以实现一张红包皮肤的配置表,将红包皮肤做成配置化呢?更换红包皮肤,只需修改一下表数据就好了。

            当然,还有一些场景适合一些配置化的参数:一个分页多少数量控制、某个抢红包多久时间过期,这些都可以搞到参数配置化表里面。这也是扩展性思想的一种体现。 

    九. 会总结并使用工具类 

    很多小伙伴,判断一个list是否为空,会这么写:

    1. if (list == null || list.size() == 0) {
    2. return null;
    3. }

     这样写呢,逻辑是没什么问题的。但是更建议用工具类,比如:

    1. if (CollectionUtils.isEmpty(list)) {
    2. return null;
    3. }

            日常开发中,我们既要会用工具类,更要学会自己去总结工具类。比如文件处理工具类、日期处理工具类等等。这些都是优秀后端开发的一些好习惯。


    十. 控制方法函数复杂度

            你的方法不要写得太复杂,逻辑不要混乱,也不要太长。一个函数不能超过 80 行。写代码不仅仅是能跑就行,而是为了以后更好地维护。

    反例如下:

    1. public class Test {
    2. private String name;
    3. private Vector orders = new Vector();
    4. public void printOwing() {
    5. //print banner
    6. System.out.println("****************");
    7. System.out.println("*****customer Owes *****");
    8. System.out.println("****************");
    9. //calculate totalAmount
    10. Enumeration env = orders.elements();
    11. double totalAmount = 0.0;
    12. while (env.hasMoreElements()) {
    13. Order order = (Order) env.nextElement();
    14. totalAmount += order.getAmout();
    15. }
    16. //print details
    17. System.out.println("name:" + name);
    18. System.out.println("amount:" + totalAmount);
    19. ......
    20. }
    21. }

            其实可以使用 Extract Method,抽取功能单一的代码段,组成命名清晰的小函数,去解决长函数问题,正例如下:

    1. public class Test {
    2. private String name;
    3. private Vector orders = new Vector();
    4. public void printOwing() {
    5. //print banner
    6. printBanner();
    7. //calculate totalAmount
    8. double totalAmount = getTotalAmount();
    9. //print details
    10. printDetail(totalAmount);
    11. }
    12. void printBanner(){
    13. System.out.println("****************");
    14. System.out.println("*****customer Owes *****");
    15. System.out.println("****************");
    16. }
    17. double getTotalAmount(){
    18. Enumeration env = orders.elements();
    19. double totalAmount = 0.0;
    20. while (env.hasMoreElements()) {
    21. Order order = (Order) env.nextElement();
    22. totalAmount += order.getAmout();
    23. }
    24. return totalAmount;
    25. }
    26. void printDetail(double totalAmount){
    27. System.out.println("name:" + name);
    28. System.out.println("amount:" + totalAmount);
    29. }
    30. }

    十一. 在 finally 块中对资源进行释放

            大家应该都有过这样的经历,windows系统桌面如果打开太多文件或者系统软件,就会觉得电脑很卡。

            当然,我们linux服务器也一样,平时操作文件或者数据库连接,IO资源流如果没关闭,那么这个IO资源就会被它占着,这样别人就没有办法用了,这就造成资源浪费。

            我们操作完文件资源,需要在 finally 块中对资源进行释放。

    1. FileInputStream fdIn = null;
    2. try {
    3. fdIn = new FileInputStream(new File("/公众号_捡田螺的小男孩.txt"));
    4. } catch (FileNotFoundException e) {
    5. log.error(e);
    6. } catch (IOException e) {
    7. log.error(e);
    8. }finally {
    9. try {
    10. if (fdIn != null) {
    11. fdIn.close();
    12. }
    13. } catch (IOException e) {
    14. log.error(e);
    15. }
    16. }

    十二. 把日志打印好

            日常开发中,一定需要把日志打印好。比如:你实现转账业务,转个几百万,然后转失败了,接着客户投诉,然后你还没有打印到日志上,想想那种水深火热的困境下,你却毫无办法。。。

            一般情况,方法入参、出参需要打印日志,异常的时候,也要打印日志,等等。如下:

    1. public void transfer(TransferDTO transferDTO){
    2. log.info("invoke tranfer begin");
    3. //打印入参
    4. log.info("invoke tranfer,paramters:{}",transferDTO);
    5. try {
    6. res= transferService.transfer(transferDTO);
    7. }catch(Exception e){
    8. log.error("transfer fail,account:{}",
    9. transferDTO.getAccount())
    10. log.error("transfer fail,exception:{}",e);
    11. }
    12. log.info("invoke tranfer end");
    13. }

    十三. 考虑异常,处理好异常

            优秀的后端开发,应当考虑到异常,并做好异常处理。这里给大家提 10 个异常处理的建议:

    • 尽量不要使用e.printStackTrace(),而是使用log打印。因为e.printStackTrace()语句可能会导致内存占满。

    • catch住异常时,建议打印出具体的exception,利于更好定位问题。

    • 不要用一个Exception捕捉所有可能的异常。

    • 记得使用finally关闭流资源或者直接使用try-with-resource

    • 捕获异常与抛出异常必须是完全匹配,或者捕获异常是抛出异常的父类。

    • 捕获到的异常,不能忽略它,至少打点日志吧。

    • 注意异常对你的代码层次结构的浸染。

    • 自定义封装异常,不要丢弃原始异常的信息Throwable cause

    • 运行时异常RuntimeException ,不应该通过catch的方式来处理,而是先预检查,比如:NullPointerException处理。

    • 注意异常匹配的顺序,优先捕获具体的异常。


    十四. 考虑系统、接口的兼容性 

            优秀的后端开发,会考虑系统、接口的兼容性。

            如果修改了对外旧接口,但是却不做兼容。这个问题可能比较严重,甚至会直接导致系统发版失败的。新手程序员很容易犯这个错误哦~

            因此,如果你的需求是在原来接口上修改,尤其这个接口是对外提供服务的话,一定要考虑接口兼容。

            举个例子吧,比如dubbo接口,原本是只接收A,B参数,现在加了一个参数C,就可以考虑这样处理:

    1. //老接口
    2. void oldService(A,B){
    3. //兼容新接口,传个null代替C
    4. newService(A,B,null);
    5. }
    6. //新接口,暂时不能删掉老接口,需要做兼容。
    7. void newService(A,B,C){
    8. ...
    9. }

    十五. 采取措施避免运行时错误

            优秀的后端开发,应该在编写代码阶段,就采取措施,避免运行时错误,如数组边界溢出、被零整除、空指针等运行时错误。类似代码比较常见:

    String name = list.get(1).getName(); //list可能越界,因为不一定有2个元素哈

             所以,应该采取措施,预防一下数组边界溢出,正例如下:

    1. if(CollectionsUtil.isNotEmpty(list)&& list.size()>1){
    2. String name = list.get(1).getName();
    3. }
  • 相关阅读:
    【备战蓝桥杯 | 软件Java大学B组】十三届真题深刨详解(2)
    Leetcode152. 连续子数组的最大乘积
    渲染如何做到超强渲染?MAX插件CG MAGIC中的渲染功能!
    实现制作动漫版的你---动漫风格迁移网络AnimeGANv2
    DSPE-PEG-Azide,DSPE-PEG-N3,磷脂-聚乙二醇-叠氮具有亲水和疏水性
    有谁知道这个3D模型是哪个封装吗,power6的封装实在是找不到
    关键路径法的“关键”是什么?是项目经理的进度把控能力!
    Android Camera App启动流程解析
    复杂度分析:卷积、depth-wise卷积
    2-MySQL的基本操作记录
  • 原文地址:https://blog.csdn.net/weixin_53919192/article/details/128097529