代码整洁之道——如何写出整洁高效的代码


前言:

以下内容是本人阅读书籍《代码整洁之道》整理摘抄出来的读书笔记,未涉及全部,但是在不断更新中。

本书有些地方可能与阿里巴巴代码规范冲突,冲突的地方我们先接受阿里巴巴的吧。毕竟这本书09年出版的了。不过虽然这么说,代码规范基本都是一脉相承的,大可不必过于担心,非要对比出个所以然来,任何一种流行于市面上的代码规范都是大同小异的。这些规范你遵循一种,广泛用在项目中,项目经理看到都把你夸得嗷嗷爽。

我们都知道好代码是什么样子的,java的三要素嘛,封装,继承,多态。
抽象代码,使得代码易于维护和拓展,但是如何实现代码简洁,降低依赖的同时又看起来很美观呢,我想这本书里蕴藏着一些答案。

一、名称规范:

1. 类名称:名词

2. 函数名称:动词

3. 不要使用中文汉字、中文拼音,中英混合

4. 不要添加没有意义的语境:例如每个类都加GS(GS无实际意义),都加等于没加

5. 不要假扮可爱,取一些只有你知道的典故什么的

二、函数规范:

1.函数应短小,每个函数一个抽象层级,只做一件事

2.函数名称应具有描述性
别害怕长的名称,长而具有描述性的名称,要比短而令人费解的名称好。长而具有描述性的名称,要比描述性的长注释好。

使用某种命名约定,让函数名称中的多个单词容易阅读,然后使用这些单词给函数取个能说清其功用的名称。

别害怕花时间取名字。

3.使用与模块名一脉相承的短语、名词和动词给函数命名。
比如你的模块名叫做xx购物商城,你的类名,函数名就是商品、购物车、订单,这种模块名和类名息息相关的名称

4.参数最好是0个 其次1个 2个;参数禁止传输布尔值

最好不要三个参数(三个参数应尽量避免)

5.函数要么做什么事,要么回答什么事,二者不可得兼
切勿返回有歧义的返回值,使得调用方需要用各种循环判断来分析被调用的函数,这是劳民伤财的

6.函数只做一件事

  • 函数只做一件事,错误处理就是一件事
  • try应该是函数的第一行 第一个单词,而且catch/finally 后面不应该有其他逻辑代码,try是包围整个函数的
  • 抽离trycatch,如下delete函数(是剥离出来的错误函数)只与错误处理有关
 public void delete(P p) {
        try {
            deletePage(p);
        } catch (Exceptione) {
            logError(e);
        }
    }
 private void deletePage(P p) throws Exception {
     delete(p);
 }
 private void logError(Exception e) {
     logger.log(e.getMessage());
 }

7.别重复自己,重复是一切邪恶的根源
多多封装,多多抽象

8.一个方法最好有一个return 语句

三、注释

决定写注释就花必要的时间写最好的注释

1.注释的意义

注释的恰当用法是弥补我们在用代码表达意图时遭遇的失败。
注意,我用了“失败”一词。我是说真的。注释总是一种失败。我们总无法找到不用注释就能表达自我的方法,所以总要有注释,这并不值得庆贺。如果你发现自己需要写注释,再想想看是否有办法翻盘,用代码来表达。每次用代码表达,你都该夸奖一下自己。每次写注释,你都该做个鬼脸,感受自己在表达能力上的失败。
我为什么要极力贬低注释?因为注释会撒谎。也不是说总是如此或有意如此,但出现得实在太频繁。注释存在的时间越久,就离其所描述的代码越远,越来越变得全然错误。原因很简单。程序员不能坚持维护注释。
代码在变动,在演化。从这里移到那里。彼此分离、重造又合到一处。很不幸,注释并不总是随之变动——不能总是跟着走。注释常常会与其所描述的代码分隔开来,子然飘零,越来越不准确。

所以,有时候也需要注释,也该花心思减少注释

当然 ,有些注释是必须的,也是有利的。来看看一些我认为值得写的注释。不过要记住,唯一真正好的注释是你想办法不去写的注释。

注释不能美化糟糕的代码。

与其花时间梳理糟糕代码的注释,不然花时间清理糟糕的代码

因为整齐而有表达力的代码,比带有大量注释的零碎而复杂的代码像样

2.需要用注释的地方

1.阐释(一定要保证正确性)

有时,注释把某些晦涩难明的参数或返回值的意义翻译为某种可读形式,也会是有用的。
通常,更好的方法是尽量让参数或返回值自身就足够清楚;但如果参数或返回值是某个标准库的一部分,或是你不能修改的代码,帮助阐释其含义的代码就会有用。

public void testCompareTo() throws Exception{
        WikiPagePath a = PathParser . parse ( " PageA " ) ;
        WikiPagePath ab = PathParser . parse ( " PageA . PageB " ) ;
        WikiPagePath b = PathParser . parse ( " PageB " ) ;
        assertTrue(b.compareTo(a)==1);//b>a
        assertTrue(ab.compareTo(a)==1);//ab>aa
        assertTrue(b.compareTo(ba)==1);//bb>ba
}

2.警示注释

有时,用于警告其他程序员会出现某种后果的注释也是有用的。

下面的注释绝对有必要存在

例如,下面的注释解释了为什么要关闭某个特定的函数:

//除非你有时间消磨
// 否则不要跑这段代码
public void_testWithReallyBigFile(){
      writeLinesToFile(10000000);
      response.setBody(testFile);
      response.readyToSend(this);
      String responseString = output . toString ( ) ;
}

3.放大不合理代码的重要性
例如

//这段代码非常重要
//删除他会导致非常严重的后果
writeLinesToFile(10000000);

4.注释一定要有联系

别让读者看注释还觉得需要一段注释来描述这段注释。

3.不需要注释的地方

一张图解释为什么有些注释不必要

在这里插入图片描述

1.用代码来阐述

只要想上那么几秒钟,就能用代码解释你大部分的意图。很多时候,简单到只需要创建个描述与注释所言同一事物的函数即可。

你愿意看到这个:

// flag是为了获取当前饭菜是否香甜
if (flag()) {
}

还是这个?

if (foodIsGood()){
}

2.及时删除todo注释

todo注释是一种程序员认为应该做,但是由于某些原因还没做的工作

千万不要因为todo代码的存在变成一堆垃圾,所有要及时删除不需要的todo注释

3.不要写自己楠楠自语(我觉的需要就加)的注释,到底要干嘛呢?

4.不要写多余的注释 例如

//它是一个int属性
private int a;
//这是月的一天
private String dataOfMath

这种注释就像上面的那张西瓜的图

5.注释掉的代码及时删除

其他人也不敢删除你注释掉的代码,所以代码会一直保留,堆积在一起,就像垃圾堆

四、格式

1.垂直格式

1.变量

变量声明应该靠近其使用的位置

所有实体变量放在类的顶部,因为是被所有方法引用

2.相关函数:

某个函数调用了另一个,就应该把它们放到一起,并且调用者在被调用者上方,这样程序才有自然的顺序

并且概念相关的代码应该放到一起,相关性越强,垂直距离就应该越短

2.横向格式

代码缩进

避免各个语句层级坍塌,不好阅读

五、异常处理

如果将异常代码隔离于主体逻辑之外,就能写出整洁而坚固的代码,我们就能单独处理它,也提高了代码的可维护性

在某种意义上,try代码块就像是事务。catch代码块将程序维持在一种持续状态,无论try代码块中发生了什么均如此。所以,在编写可能抛出异常的代码时,最好先写出try-catch-finally语句。这能帮你定义代码的用户应该期待什么,无论try代码块中执行的代码出什么错都一样。

1.使用异常而非返回码

在很久以前,许多语言都不支持异常。这些语言处理和汇报错误的手段都有限。你要么设置一个错误标识,要么返回给调用者检查的错误码。

     public void sendShutDown () {
            DeviceHandle handle = getHandle(DEV1);
            if (handle != DeviceHandle.INVALID) {
                xxx();
                if (record.getstatus() != DEVICE_SUSPENDED) {
                    xxx2();
                } else {
                    logger.log("Device suspended. Unable to shut down");
                }
            } else {
                logger.log("Invalid handle for:" + DEV1.toString());

            }
        }

这类手段的问题在于,它们搞乱了调用者代码。调用者必须在调用之后即刻检查错误。
这个步骤很容易被遗忘。所以,遇到错误时,最好抛出一个异常。调用代码很整洁,其逻辑不会被错误处理搞乱。

如下是抛出异常的情况

public void sendShutDown () {
   try {
       DeviceHandle handle = tryToS(DEV1);
   } catch (Exception e) {
       log.error(e);
   } 
}
public void tryToS (DEV1 id) throws DeviceShutDownException {
   getHandle(DEV1 id);
   xxx();
   xxx2();
}
public void getHandle (DEV1 id){
   ...
   throw new DeviceShutDownException("Invilad handle fooe:" + id.toString());
   ...
}

2.给出异常发生的环境说明

你抛出的每个异常,都应当提供足够的环境说明,以便判断错误的来源和处所。在Java中,你可以从任何异常里得到堆栈踪迹(stack trace);然而,堆栈踪迹却无法告诉你该失败操作的初衷。
应创建信息充分的错误消息,并和异常一起传递出去。在消息中,包括失败的操作和失败类型。如果你的应用程序有日志系统,传递足够的信息给catch块,并记录下来。

3.调用者需要定义异常类

对错误分类有很多方式。可以依其来源分类:是来自组件还是其他地方?或依其类型分类:是设备错误、网络错误还是编程错误?

不过,当我们在应用程序中定义异常类时,最重要的考虑应该是它们如何被捕获。
来看一个不太好的异常分类例子。下面的try-catch-finally语句是对某个第三方代码库的调用。它覆盖了该调用可能抛出的所有异常

语句包含了一大堆重复代码,这并不出奇。在大多数异常处理
我们总是做相对标准的处理。我们得记录错误,确保能继续工作。

   ACMEPort port = new ACMEPort(12);
      try {
          port.open();
      } catch (DeviceResponseException e) {
          reportPortError(e);
          logger.log("Device response exception", e);
      } catch (ATM1212UnlockedException e) {
          reportPortError(e);
          logger.log("Unlock exception", e);
      } catch (GMXError e) {
          reportPortError(e);
          logger.log("Device response exception");
      } finally {
          ...
      }

我们可以改造一下,打包调用API,确保他返回的是通用的异常类型 改造如下:

   LocalPort port = new LocalPort(12);
        try {
            port.open();
        } catch (PortDeviceFailure e) {
            reportError(e);
            log.error(e.getMessage(), e);
        } finally {
            ...
        }

LocalPort类就是个简单的打包类,拥获由ACMEPort 抛出的异常

  public class LocalPort {
        private ACMEPort innerPort;

        public LocalPort(int portNumber) {
            innerPort = new ACMEPort(portNumber)
        }

        public void open() {
            try {
                innerPort.open();
            } catch (DeviceResponseException e) {
                throw new PortDeviceFailure(e);
            } catch (ATM1212UnlockedException e) {
                throw new PortDeviceFailure(e);
            } catch (GMXError e) {
                throw new.PortDeviceFailure(e);
            }
        }
    }

类似我们为ACMEPort定义的这种打包类非常有用。实际上,将第三方API打包是个良好的实践手段。当你打包一个第三方API,你就降低了对它的依赖。在上例中,我们为port设备错误定义了一个异常类型,然后发现这样能写出更整洁的代码。
对于代码的某个特定区域,单一异常类通常可行。伴随异常发送出来的信息能够区分不
同错误。如果你想要捕获某个异常,并且放过其他异常,就使用不同的异常类。

六、单元测试

整洁的测试还遵循以下5条规则:

1.快速(Fast)

测试应该够快。测试应该能快速运行。测试运行缓慢,你就不会想要频繁地运行它。如果你不频繁运行测试,就不能尽早发现问题,也无法轻易修正,从而也不能轻而易举地清理代码。最终,代码就会腐坏。

2.独立(Independent)

测试应该相互独立。某个测试不应为下一个测试设定条件。你应该可以单独运行每个测试,及以任何顺序运行测试。当测试互相依赖时,头一个没通过就会导致一连串的测试失败,使问题诊断变得困难,隐藏了下级错误。

3.可重复(Repeatable)

测试应当可在任何环境中重复通过。你应该能够在生产环境、质检环境中运行测试,也能够在无网络的列车上用笔记本电脑运行测试。如果测试不能在任意环境中重复,你就总会有个解释其失败的接口。当环境条件不具备时,你也会无法运行测试。

4. 自足验证(Self-Validating)

测试应该有布尔值输出。无论是通过或失败,你不应该查看日志文件来确认测试是否通过。你不应该手工对比两个不同文本文件来确认测试是否通过。如果测试不能自足验证,对失败的判断就会变得依赖主观,而运行测试也需要更长的手工操作时间。

5. 及时(Timely)

测试应及时编写。单元测试应该恰好在使其通过的生产代码之前编写。
如果在编写生产代码之后编写测试,你会发现生产代码难以测试。你可能会认为某些生产代
码本身难以测试。你可能不会去设计可测试的代码。

总结

写代码和写别的东西很像。在写论文或文章时,你先想什么就写什么,然后再打磨它。

初稿也许粗陋无序,你就勘酌推敲,直至达到你心目中的样子。

我写函数时,一开始都穴长而复杂。有太多缩进和嵌套循环。有过长的参数列表。名称是随意取的,也会有重复的代码。

不过我会配上一套单元测试,覆盖每行丑陋的代码。然后我打磨这些代码,分解函数、修改名称、消除重复。我缩短和重新安置方法。有时我还拆散类。同时保持测试通过。

最后,遵循本章列出的规则,我组装好这些函数。
我并不从一开始就按照规则写函数。我想没人做得到。

大师级程序员把系统当作故事来讲,而不是当作程序来写。他们使用选定编程语言提供的工具构建一种更为丰富且更具表达力的语言,用来讲那个故事。如果你遵循这些规则,函数就会短小,有好名字,而且被很好地归置。不过永远别忘记,真正的目标在于讲述系统的故事,而你编写的函数必须干净利落地拼装到一起,形成一种精确而清晰的语言,帮助你讲故事。

  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值