《代码整洁之道》读书笔记

为了获得更好的阅读体验,请访问原文:传送门

一、前言


代码是什么呢?或者说作为程序员的我们,对于写代码这件事又是抱着怎样的一种态度呢?我时常都在想,如今我如愿成为了一名程序员(虽然还很菜),写代码这件事成了我的工作,我期望从工作中获得些什么?而工作又能给予我什么呢?

我在短暂的工作经历中(4 个月),犯下过不少错,少部分是因为经验,但大部分的情况下都是因为对代码没有足够的敬畏之心导致的,并且在工作中也遇到过一些很有意思的代码,所以今天就着这本《代码整洁之道》,来谈一谈对于代码的感受和一些想法。(Ps:想吐槽一下这本书挺魔怔的..)

二、什么是整洁的代码


我搜索「代码」这两个关键字,给出的官方解释都特别有意思,摘一下百度百科的好了:

代码就是程序员用开发工具所支持的语言写出来的源文件,是一组由字符、符号或信号码元以离散形式表示信息的明确的规则体系。代码设计的原则包括唯一确定性、标准化和通用性、可扩充性与稳定性、便于识别与记忆、力求短小与格式统一以及容易修改等。 源代码是代码的分支,某种意义上来说,源代码相当于代码。现代程序语言中,源代码可以书籍或磁带形式出现,但最为常用格式是文本文件,这种典型格式的目的是为了编译出计算机程序。计算机源代码最终目的是将人类可读文本翻译成为计算机可执行的二进制指令,这种过程叫编译,它由通过编译器完成。

好了,学术介绍一大堆,重点还是在最后一句:计算机源代码最终目的是将人类可读文本翻译成为计算机可执行的二进制指令。

再精简一下:「人类可读」、「计算机可执行」。

说到底,代码最终还是写给人看的,所以「可读性」就显得尤为重要,但总归我们是要先有「代码」,再有「可读的代码」,经过不断重构or重写,最终形成我们「简洁的代码」。

说几点感受比较大的吧。

方法尽量短 && 职责单一

有谁能告诉我下面这个方法究竟是在做什么吗?

/**
 * @author Administrator
 *
 */
public class GeneratePrimes {

	/**
	 * @param maxValue is the generation limit.
	 * */
	public static int[] generatePrimes(int maxValue) {
		if (maxValue >= 2){ //the only valid case
			//ddeclarations
			int s = maxValue +1 ;// size of array
			boolean[] f = new boolean[s];
			int i;
			//initialize array to true.
			for ( i = 0;i < s;i++) {
				f[i] = true;
			}
			f[0] = f[1] = false;
			// sieve
			int j;
			for (i = 2;i < Math.sqrt(s) + 1; i++) {
				if (f[i]) { // if i is uncrossed , cross its multiples.
					for (j = 2 * i; j < s ;j += i) {
						f[j] = false; //multiple is not prime
					}
				}
			}
			
			// how many primes are there?
			int count = 0;
			for (i = 0;i < s; i++) {
				if (f[i]) {
					count ++; //bump count.
				}
			}
			
			int[] primes = new int[count];
			
			//move the primes into the result
			for (i = 0,j = 0;i < s;i++) {
				if (f[i]) {
					primes[j++] = i;
				}
			}
			return primes;
		}
		else { //maxValue < 2
			return new int[0]; // return null array if bad input.
		}
	}
}

如果你非常有耐心地看完了,你可能大概或许会了解到,这是一个返回 maxValue 范围以内的质数的方法,但是我们经过简单的重构之后,会变得更加容易理解:

public class PrimeGenerator {
 
	private static boolean[] crossedOut;
	private static int[] result;
	
	public static int[] generatePrimes(int maxValue) {
		if (maxValue < 2) {
			return new int[0];
		}
		else {
			uncrossIntegersUpTo(maxValue);
			crossOutMultiples();
			putUncrossedIntegersIntoResult();
			return result;
		}
	}

	private static void putUncrossedIntegersIntoResult() {
		result = new int[numberOfUncrossedIntegers()];
		for (int j = 0, i = 2; i < crossedOut.length; i++) {
			if (notCrossed(i)) {
				result[j++] = i;
			}
		}
	}

	private static int numberOfUncrossedIntegers() {
		int count = 0;
		for (int i = 2; i < crossedOut.length; i++) {
			if (notCrossed(i)) {
				count++;
			}
		}
		return count;
	}

	private static void crossOutMultiples() {
		int limit = determinuIterationLimit();
		for (int i = 2;i <= limit; i++) {
			if (notCrossed(i)) {
				crossOutMultiplesOf(i);
			}
		}
	}

	private static void crossOutMultiplesOf(int i) {
		for (int multiple = 2 * i; multiple < crossedOut.length; multiple +=i) {
			crossedOut[multiple] = true;
		}
	}

	private static boolean notCrossed(int i) {
		return crossedOut[i] == false;
	}

	private static int determinuIterationLimit() {
		double iterationLimit = Math.sqrt(crossedOut.length);
		return (int)iterationLimit;
	}

	private static void uncrossIntegersUpTo(int maxValue) {
		crossedOut = new boolean[maxValue+1];
		for (int i = 2; i < crossedOut.length ; i++) {
			crossedOut[i] = false;
		}
	}
	
}

首先我们通过私有方法隐藏掉了实现的具体细节,并且使用有意义的命名,使得我们主函数 generatePrimes 更加便于理解。

函数的第一规则就是要短小,第二条规则就是要更短小。每个函数保持职责单一,并且有意识的维持在一定行数内(JVM 就强制要求每个函数要小于 8000 行...也听过每个函数尽量维持在 15 行 or 30 行 之内这样的说法..可能有点魔怔,但要点就是函数要尽量短小),这当然是最理想的情况,而现实的情况往往要糟糕一些。

在工作中,我就遇到过一些长得可怕的方法,他们或许本来保持着单纯,职责单一,但是经过业务不断的改造,需求不断的叠加,甚至是一些临时逻辑的加入,这个方法就变得越来越臃肿不堪...并且因为业务的不断发展,越来越少的人会 care 到它,以至于改造成本越来越大,甚至被遗忘在角落..

这其实是再正常不过的事情,但在多人协作的项目中,有一点需要自己来维持清醒,那就是:「一个方法就可以返回的为什么要写两个?」,关于这一点,保持自己的思考就好了..

注释要体现代码之外的东西

有一句听起来好厉害的话叫做:「代码即注释」,不知道大家是怎么看待这样一句话的,或者说是怎么看待注释的。其实反过来想,如果你的代码需要大量的注释来解释其中的逻辑,会不会是代码本身就存在一定问题?或者换个角度思考,注释是用来解释代码逻辑的吗?

可怕的废话

我们来看下面这一段代码的注释:

/** The name. */
private String name;
/** The version. */
private String version;
/** The licenceName. */
private String licenceName;
/** The version. */
private String info;

上面这些 Javadoc 的目的是什么?答案是:无。并且仔细阅读,你甚至会发现一处剪切-粘贴导致的错误,如果作者在写(或粘贴)注释时都没有花心思,怎么能指望读者从中收益呢?

能用函数或变量时就别用注释

看看以下代码概要:

// does the module from the globale list <mod> depend on the
// subsystem we are part of?
if (smodule.getDependSubsystems().contains(subSysMod.getSubSystem())

可以修改成以下没有注释的版本:

ArrayList moduleDependes = smodule.getDependSubsystems();
String ourSubSystem = subSysMod.getSubSystem();
if  (moduleDependes.contains(ourSubSystem))

用代码来阐述

有时,代码本身不足以解释其行为。但不幸的是,许多程序员以此为由,觉得大部分时候代码都不足以解释工作。这种观点纯属错误,比如你愿意看到下面这个:

// Check to see if the employee is eligible for full benefits
if ((employee.flags & HOURLY_FLAG) && (employee.age > 65))

还是这个:

if (employee.isEligibleForFullBenefits())

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

小结

注释终归是要用来体现代码之外的东西..

名副其实的名字

取名字这件事,真的是程序员的一门艺术。脑海里面能浮现出同事们用翻译软件取名的画面吗?

一个好的名字再怎么夸赞都不为过,但是这个最基础的前提就是,它首先得是一个「正确的名字」。我就遇到过一次,函数名字叫做类似于 listAll 这样的东西,戳进去看实际上还基于业务规则做了过滤..(这样的牛肉不对马嘴的情况又让我联想到了注释这样的东西,可能实际的代码已经作了更改,但是注释还是是维持原样没有变化..)

并且还有一个特别有意思的点,就是关于名字的「长度」。有时候可能为了想要描述清楚一个变量 or 一个类的具体作用,我会给它起一个看起来特别长的名字..关于这个,这里有一些小经验可以分享一下:

  • 去掉 Info 和 Data 这样的后缀:这些就像是英语中的 a/ an/ the 一样,是意义含糊的废话,废话都是冗余的..
  • 不要给变量加前缀来标识:变量不需要一个 m_ or 其他什么的前缀来标识这是一个变量..
  • 思考是否有必要标识出变量的类型:我们标注出变量的类型的目的是什么?对于弱类型的语言,可能有时候还是必要的,因为我们有时候并不能从 students 这个变量中判明我应该怎样对这个变量进行操作,但是对于 Java 这样的强类型的语言,我们就需要根据实际的场景思考是否真有那么必要了。

无副作用

函数承诺只做一件事,但还是会做其他被隐藏起来的事。

public class UserValidator {
    private Cryptographer cryptographer;

    public boolean checkPassword(String userName, String password) {
        User user = UserGateway.findByName(userName);
        if (user != User.NULL) {
            String codedPhrase = user.getPhraseEncodedByPassrod();
            String phrase = cryptographer.decrypt(codedPhrase, password);
            if ("Vliad Passwordw".equals(phrase)) {
                Session.initialize();
                return true;
            }
        }
        return false;
    }
}

上面的函数副作用就在于对 Session.initialize() 的调用。checkPassword 函数,顾名思义就是用来检查密码的,该名称并未暗示它会初始化该次会话。所以,当某个误信了函数名的调用者想要检查用户有效性时,就得冒着抹除现有会话数据的风险。

所以这里可以重命名函数为 checkPasswordAndInitializeSession,虽然这还是违背了 "只做一件事" 的规则。

函数参数尽可能少

一个函数最理想的参数数量是 0,其次是 1,再次是 2.. 要避免使用三个以上参数的情况,因为参数带有太多的概念性,参数过多就会带来更多的复杂性..

我就见过一个查询接口,为了满足不同的复杂查询场景,参数大概可能有接近 10 个.. 就算不为接手的编码人员考虑,测试人员也会头疼的.. 想想看,要覆盖如此兼容如此多场景如此复杂的一个查询接口,测试用例究竟应该怎么写呢?

More..

这本书说实话看下来挺魔怔的.. 里面有许多简洁实用的观点可以让我们受益,我仅仅挑了一些最近比较感同身受的几点,来进行了说明。

代码仓库就像是一本《哈姆雷特》一样,每个人都有自己不同的见解,这无可厚非,我觉得重要的就是要保持对代码的敬畏之心,保持自身的思考,才能让我们不断向前(说话都变魔怔了..)

三、代码之外


每个人都能写出好的代码

这就是一个非常有意思的话题了,我们可以分成几个角度来思考:

  • 好的代码是写出来的吗?(这可能有点类似于好的文章是写出来的吗?)
  • 为什么我们写不出好的代码?

我记得之前在看《重构:改善既有代码的设计》这本老经典的书的时候,就提到一种观点说:「重构不是一个一蹴而就的事,需要长期的实践和经验才能够完成得很好。重构强调的是 Be Better,那在此之前我们首先需要先动起手来搭建我们的系统,而不要一味地“完美主义”。」

好的代码也是这样,需要一个循序渐进的过程,虽然大部分时候,经验可以让我们少走许多弯路,但这些都是一个过程。

当然上面所说的全部,都是理想中的状况,而现实中的情况往往不允许我们这样做。什么之前炒起来的 996,什么 ICU,都无情的揭示着大部分程序员的现状:忙。忙于各种已经堆成山的需求 && 修复各种 BUG 中。

我学到一个很正经的概念,叫做「管窥」,附带的一种概念叫做「稀缺」。(看完下面这个故事应该很容易理解,故这里不作解释..)

我记得之前看过一篇报道,说是香港某富豪在节目中要体验几天环卫工人,参加节目前,他曾说过:“我的人生其实没有很多时间坐下来,想想现在的生活不错,享受一下。我有时间就会计划下一步!"

可几天下来,让他最纠结的竟然是吃饭问题,他对着镜头说:“很奇怪,我这两天只是考虑吃东西,完全没什么盼望,什么都不想。我努力工作,就是希望吃一顿好的。”

程序员是一种很容易陷入,对于时间「稀缺」状态的物种。稀缺会俘获我们的注意力,并带来一点点好处:我们能够在应对迫切需求时,做得更好。但从长远的角度来看,我们的损失更大:我们会忽视其他需要关注的事项,在生活的其他方面变得不那么有成效。(摘自《稀缺》P17)

这听上去就像是在找借口一样,但其实有点差别。我发觉每个人其实都能够写出好的代码,只是取决于你有没有这样的意识,有没有坚持自己的思考,更重要的是,有没有「跳出需求」,甚至是「跳出工作」之外来思考,就像是要跳出「我们明明知道了很多道理,却依然过不好这一生」的怪圈一样。

结尾


这一段时间都不怎么更新了,不是我变懒了.. 前段时间就陷入了不加班就完成不了工作的状态,一方面是因为事情比较杂.. 另外一方面就是自己效率还不够高.. (悄悄说:虽然很忙,但是总是能抽得出时间玩儿手机 hhhh...)值得反思吧.. 最近也开始有一些觉得越来越难下笔了.. 想写的东西很多,但总怕写不好..

另外,程序员真的是很有意思的职位了,并且也觉得程序员都多少带着点儿自己的骄傲来得,因为每天都在自己的世界玩儿拼图,自己就是世界的造物主,久了,难免有些受影响..(主要体现在沟通上..)

摁.. 总之这是一本很好的书,建议感兴趣的童鞋可以溜一遍。


按照惯例黏一个尾巴:

欢迎转载,转载请注明出处!
独立域名博客:wmyskxz.com
简书ID:@我没有三颗心脏
github:wmyskxz
欢迎关注公众微信号:wmyskxz
分享自己的学习 & 学习资料 & 生活
想要交流的朋友也可以加qq群:3382693

posted @ 2019-09-14 17:41  我没有三颗心脏  阅读(1742)  评论(0编辑  收藏  举报