《代码整洁之道》· 读书笔记
前言
这本书的封面写道,“细节之中自有天地,整洁成就卓越代码”,便是本书的精髓所在。对于软件开发,设计不仅存在于界面之中,更是存在于代码之中,而对于一个软件架构来说,任何编码的细节都至关重要,代码的整洁性关乎到后期的可维护性,后人看代码时的可读性,所以我们要对自己写下的每一行代码负责。
第一章 整洁代码
本章阐述了什么是整洁的代码。
首先是有代码,现在人工智能风行,有传言说以后计算机能自动敲代码,程序员将会被替代。暂且不说这个消息是真是假,即使是真的,那被淘汰的也是“代码猴子”。因为精确的优雅的代码还是需要由高级程序员来构建的。
那为什么要写整洁的代码呢?如果你被烂代码坑过那你一定会对整洁代码更加有更深的理解和需求。当你面对着一堆杂乱的代码,不知产品提的需求从何做起,稍微一改动一点地方就会牵一发而动全身的时候,你一定想骂一句“WTF”了。所以,写整洁的代码是程序员的一种职业素养和基本功,不仅是对自己负责,也是对以后开发这块代码的人负责,当别人一看就懂的时候,肯定会拉上去看一下这个腻害author是谁的哈哈哈。
最后,划重点啦!!!什么是整洁的代码呢?书中引用了很多大神的观点,我总结了一些,基本就是:
1. 代码逻辑直截了当,模块应该尽量小,减少依赖关系,也就是我们经常说的一个类一个责任的原则,让人一看就懂,不引人猜测,以免诱导别人做出错误的判断;
2. 增强对错误的处理,例如内存泄露,竞态条件等情况,使代码能通过所有测试;
3. 没有重复的代码,重复的地方尽量抽取成一个函数,明确该函数的功能,提高代码的表达力。
第二章 有意义的命名
命名这件事贯穿于我们的代码之中,小到变量,函数,大到类名,包名,都要用到命名,所以命名这件事看似很小,实则对代码的可读性起到重要的作用。
int d;
int day;
int elapsedTimeInDays;
如果该变量是代表流逝的天数,
用elapsedTimeInDays则最佳,名副其实;
用day,程序员结合下文的代码也许可以估摸出来是什么意思,也许估摸不出来;
用d,这个估计看的人就要懵逼了。
引用项目中的一个命名:private static final int DEFAULT_PRESSED_COLOR_ALPHA,这个命名一看就能明白这个变量代表按钮默认按压时颜色透明度的变化程度。
现在来看方法的命名:
public List<int[]> getThem() {
List<int[]> list1 = new ArrayList<int[]>();
for (int[] x : theList) {
if (x[0] == 4) {
list1.add(x);
}
}
return list1;
}
单看这个方法,我会问几个问题:
1.getThem()想得到什么?
2.其次theList和list1代表什么?
3.最后为什么要有if(x[0] == 4)的逻辑判断?
public List<Cell> getFlaggedCells() {
List<Cell> flaggedCells = new ArrayList<>();
for (Cell cell : gameBoard) {
if (cell.isFlagged()) {
flaggedCells.add(cell);
}
}
return flaggedCells;
}
我们再看上面那个方法就很清晰了,这个方法要得到被标记的单元,从这个游戏的所有单元格里面进行遍历,只要遍历到的单元格被标记了就把它加入已标记的容器里面。
从这个例子中我们可以看出,良好的代码命名能简化我们的工作量,并且令身心愉悦。
所以,良好的代码命名规范主要由以下几点:
- 代码的命名要名副其实,顾名思义,能直接了解到该处代码的作用;
- 代码的命名应该避免歧义,这个歧义有几种情况,
a. 减少英文字母o和l的单独使用,因为他们看起来像阿拉伯数字0和1;
b. 是同一个模块中的命名尽量不出现近义词,例如info和data,不然看的人不知道这两个有什么区别;
c. 是减少使用双关语,例如add,难以区分是增加还是连接; - 名称的长短应该与作用域相对应,因为作用域越大,该命名的被搜索的可能性就越大,一般来说长命名胜于短命名,可以实现精准搜索;
- 类名应该使用名词,方法名应该使用动词;
- 使用源自所涉问题领域的命名,例如社区用forum相关,理财用finance相关。
函数
在面向对象的语言中,我们编写一个类,基本是由变量和函数(方法)构成的,变量主要是命名,那函数的编写原则在哪呢?
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initView();
findView();
setView();
setListener();
}
1.函数要尽可能短小,当一个函数过于长,比如我们一个Activity的onCreate()方法,这时候我们就应该适当的抽取出一些新的函数出来;
- 函数应该只做一件事,并做好这件事,例如initView()就应该做初始化布局的工作,把findViewById(R.id.XXX)的工作留给findView()来完成;
- 函数的语句应该在同一抽象级上,例如上面onCreate()里面的方法就属于较高抽象级,不要突然给一个控件来一句setColor(),这感觉就有点杂乱了;
Tips:无论是写代码还是,读代码,都应遵循自顶向下的原则;
if(isExists(username)) {
setNewPassword(username, newPassword);
... ...
}
- 函数要么只做一件事,要么回答一个问题,不要同时做着两个工作;
try {
JSONObject headValueJO = new JSONObject();
headValueJO.put("version","1.0");
headValueJO.put("bizcode", "1007");
HttpManagerHelper.getInstance().postRequest(financeWalletInfoUrl, nameValuePairs);//请求发送通知
} catch (JSONException e) {
DebugUtil.exception(TAG, e);
} catch (Exception e) {
DebugUtil.exception(TAG, e);
}
}
- 使用异常去代替返回错误码,主要有以下几点原因:
a. 异常的错误处理代码能直接从主路径中分离出来;
b. 明确错误处理的函数工作责任单一性;
c. 避免”依赖磁铁“的重新编译和部署的麻烦性;
其他的一些原则:
- 函数的参数应该尽可能少,如果参数太多了,可能会增加我们对这个函数的理解和使用难度,这时候我们应该把这些参数抽象成对象;
- 抽取方法,消除冗余,减少重复,这实际在我们的开发中经常遇到,可以算是一个经验了,功能先实现,然后完成后慢慢打磨代码,找到重复的地方,抽取出来进行优化,当然也包括code review的时候有人会去检查代码的冗余程度;
注释
首先强调一点:若代码足够有表达力,那么并不需要注释。这也是本书对注释一个最核心的概念,所以:
- 注释并不能美化糟糕的代码,能用代码阐述行为的就不要用注释去解释代码;
- 好的注释包括:
a. 法律信息,公司信息,作者等;
b. 提供信息的注释,例如正则表达式的注释;
c. 对意图的注释,代码中可能使用了某个int值或者小技巧例如延时操作,需要注释来解释意图;
d. 警示,例如不推荐使用,但还没重构完成的方法;
e. TODO注释,标注还没完成的工作,方便定位
f. API文档的Javadoc; - 坏的注释包括:
a. 喃喃自语的注释,只有自己看的懂的注释;
b. 多余的注释,通过代码已经能清晰明白含义就不需要注释了;
c. 误导性的注释,注释不够精确,和代码含义不一致的注释(有可能是历史遗留原因造成的);
d. 注释掉的代码,这样的代码别人看了也不敢删,无用的代码就会积少成多;
举个例子解释一下好和坏的注释:
// private static final int DEFAULT_PRESSED_COLOR_ALPHA = 51; // 按钮按压时颜色的透明度变化
private static final int DEFAULT_PRESSED_COLOR_ALPHA = 51; // 透明度为20%
上面的例子首先是一个int值为51的静态变量:
最开始的注释为“按钮按压时颜色透明度的变化”,其实这就是一个多余的注释,因为从变量名已经能很清楚的解释这个变量名的含义;
所以我们更改注释为“透明度为20%”,就能很好的解释int值为“51”的意图;
当代码使我们自己写的,又没用到的时候就直接删除掉,不要像上面一样注释掉,以防成为陈年老醋。
格式
团队应该一致同意采用一套简单的格式规则,可以运用工具将这些规则自动化的工具。
代码格式关乎沟通,而沟通是专业开发者的头等大事。
或许你认为“让代码能工作”才是专业开发者的第一优先级,你今天编写的功能,极有可能在下一版本中被修改,但代码的可读性却会对以后可能发生的修改行为产生深远影响。
原始代码修改之后很久,其代码风格和可读性仍会影响到可维护性和扩展性。即便代码已不复存在,你的风格和律条仍存活下来。
每个人有不同的代码风格这无可厚非,但是一些被普遍认可的代码格式有:
//设置底部图标
int normalColor = SkinManager.getInstance().getSkinColor(Skinable.KEY_MAIN_BOTTOM_TEXT_COLOR);
int pressedColor = DrawableUtil.changeColorAlpha(bottomColor, DEFAULT_PRESSED_COLOR_ALPHA);
ColorStateList colorStateList = DrawableUtil.createColorStateList(bottomColor, pressedColor);
setNavBtnIcon(mNavYearTransBtn, R.drawable.nav_year_trans, Skinable.BOTTOM_NAV_ICON_1, colorStateList);
setNavBtnIcon(mNavAccountBtn, R.drawable.nav_account, Skinable.BOTTOM_NAV_ICON_2, colorStateList);
setNavBtnIcon(mNavReportBtn, R.drawable.nav_report, Skinable.BOTTOM_NAV_ICON_3, colorStateList);
setNavBtnIcon(mNavBudgetBtn, R.drawable.nav_finance, Skinable.BOTTOM_NAV_ICON_4, colorStateList);
setNavBtnIcon(mNavSettingBtn, R.drawable.nav_setting, Skinable.BOTTOM_NAV_ICON_5, colorStateList);
mNavYearTransBtn.setTextColor(colorStateList);
mNavAccountBtn.setTextColor(colorStateList);
mNavReportBtn.setTextColor(colorStateList);
mNavBudgetBtn.setTextColor(colorStateList);
mNavSettingBtn.setTextColor(colorStateList);
1.垂直方向上的间隔与靠近,空白行隔开了不同概念的代码块,靠近的代码行则暗示了它们之间的紧密关系。这有助于对代码的理解;
2.水平方向上的空白与靠近, 空格字符可以把相关性较弱的事物分隔开,比如参数之间;
3.利用缩进模式来看清楚自己在哪个范围工作,这样可以快速跳过与当前关注的情形无关的范围;
4.团队规则,一套读起来不错的代码系统,需要一直和顺畅的风格;
对象与数据结构
简单介绍一下几个主要概念:
对象:把数据隐藏于抽象之后,暴露操作数据的函数;
数据结构:暴露其数据,没有提供有意义的函数;
过程式编程:便于在不改动既有数据结构的前提下添加新函数;
面向对象编程:便于在不改动既有函数的前提下添加新类;
德墨忒尔律:模块不应该了解他所操作对象的内部情况。对象隐藏数据,暴露操作,这意味着对象不应该通过存取器暴露其内部结构。
火车失事:连串调用是肮脏的风格,方法不应调用由任何函数返回的对象的方法。只和朋友谈话,不和陌生人谈话。(同一个对象的话例外,例如RxJava的链式调用)
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
所以上述代码最好切分下面这样的代码,也可以方便出现空指针异常的时候及时找出null值出现的位置。
Options opts = ctxt.getOptions();
File scratchDir = opts.getScratchDir();
final String outputDir = scratchDir.getAbsolutePath();
类
类的组成:
public class Example {
public 公共静态常量;
private 私有静态变量;
private 私有实体变量;
public 公共函数1() {
......
调用私有工具函数1();
}
private 私有工具函数1() {
......
}
public 公共函数2() {
......
调用私有工具函数2();
}
private 私有工具函数2() {
......
}
}
类应该从一组变量列表开始。
如果有公共静态常量,应该先出现,然后是私有静态变量,以及私有实体变量,很少会有公共变量。
公共函数应跟在变量列表之后,把由某个公共函数调用的私有工具函数紧随在该公共函数后面,这符合了自顶向下原则。
类的设计原则:
- 类应该短小
- 系统应该由许多短小的类而不是少量巨大的类组成;
- 保持内聚性就会得到许多短小的类;
- 如果类丧失了内聚性,就拆分它;
- 类只应该有一个权责——只有一个修改的理由(单一权责原则,SRP);
- 类应该对扩展开发,对修改封闭(开放-闭合原则,OCP);
- 类应该依赖于抽象而不是具体细节,通过部件之间的解耦来隔离修改(依赖倒置原则,DIP);
跌进
软件项目的主要成本在于长期维护,所以在我们长期开发和维护代码的过程中
主要有以下几个简单的设计原则:
- 运行所有测试,同样编写测试越多,就会越遵循DIP之类的原则,使用依赖注入,接口和抽象等工具尽可能减少耦合;
- 重构,在重构过程中,可以应用有关优秀软件设计的一切知识,提升内聚性,降低耦合度;
- 不可重复,重复是良好设计系统的大敌。它代表着额外的工作、额外的风险和额外不必要的复杂度;
- 提高表达力,选用好名称、保持函数和类尺寸短小、标准命名法,其实说白了就是提高代码的可读性;
- 尽可能少的类和方法,有时候一些类和方法是被刻意写出来的,需不需要为此创建新的类和方法需要灵活处理。
总结:
测试一定是处于第一位的,测试在保证功能正确的同时,也是我们进行重构的最基本保证。
为了使代码可测试,我们必须在开发的过程中不断的打磨代码,持续的降低耦合度,提高内聚,使类与函数功能单一、简单。
从这个意义上讲,测试也是重构的持续驱动力。
并发编程
本章只是简单介绍了一些并发的概念以及处理并发时应该注意的一些问题
一些并发编程的认识:
- 编写并发程序会在代码上增加额外的开销。;
- 正确的并发是非常复杂的,即使对于很简单的问题;
- 并发中的缺陷因为不易重现也不容易被发现;
- 并发往往需要对设计策略从根本上进行修改;
一些并发编程的原则:
- 单一职责:并发已经足够复杂,我们更需要代码分离,分离线程相关代码和非线程相关代码,单一权责,尽可能降低其复杂度;
- 限制临界资源的作用域:为临界资源加锁是防止并发的策略,但是必须正确的加锁,如果形成等待环,就导致死锁;
- 利用数据副本在线程之间传递数据:避免线程之前操作的并发影响;线程独立,使其在自己的环境中运行,不能其他线程共享数据;
- 线程尽可能独立:让线程存在于自己的世界中,不与其他线程共享数据;
- 对于临界资源加锁应尽量保持加锁范围尽可能的小。
一些并发编程的基础定义:
并发编程
一些并发编程的执行模型:
1.生产者-消费者模型
一个或多个生产者线程创建某些工作,并置于缓存或者队列中。
一个或者多个消费者线程从队列中获取并完成这些工作。生产者和消费者之间的队列是一种限定资源。
2.读者-作者模型
当存在一个主要为读者线程提供信息源,但只是偶尔被作者线程更新的共享资源,吞吐量就会是个问题。
增加吞吐量,会导致线程饥饿和过时信息的积累。
协调读者线程不去读取正在更新的信息,而作者线程倾向于长期锁定读者线程。
3.宴席哲学家
许多企业级应用中会存在进程竞争资源的情形,如果没有用心设计,这种竞争会遭遇死锁,活锁,吞吐量和效率低等问题。