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

前言

这本书的封面写道,“细节之中自有天地,整洁成就卓越代码”,便是本书的精髓所在。对于软件开发,设计不仅存在于界面之中,更是存在于代码之中,而对于一个软件架构来说,任何编码的细节都至关重要,代码的整洁性关乎到后期的可维护性,后人看代码时的可读性,所以我们要对自己写下的每一行代码负责。


第一章 整洁代码

本章阐述了什么是整洁的代码。
  首先是有代码,现在人工智能风行,有传言说以后计算机能自动敲代码,程序员将会被替代。暂且不说这个消息是真是假,即使是真的,那被淘汰的也是“代码猴子”。因为精确的优雅的代码还是需要由高级程序员来构建的。
  那为什么要写整洁的代码呢?如果你被烂代码坑过那你一定会对整洁代码更加有更深的理解和需求。当你面对着一堆杂乱的代码,不知产品提的需求从何做起,稍微一改动一点地方就会牵一发而动全身的时候,你一定想骂一句“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;
}

我们再看上面那个方法就很清晰了,这个方法要得到被标记的单元,从这个游戏的所有单元格里面进行遍历,只要遍历到的单元格被标记了就把它加入已标记的容器里面。

从这个例子中我们可以看出,良好的代码命名能简化我们的工作量,并且令身心愉悦。

所以,良好的代码命名规范主要由以下几点:

  1. 代码的命名要名副其实,顾名思义,能直接了解到该处代码的作用;
  2. 代码的命名应该避免歧义,这个歧义有几种情况,
    a. 减少英文字母o和l的单独使用,因为他们看起来像阿拉伯数字0和1;
    b. 是同一个模块中的命名尽量不出现近义词,例如info和data,不然看的人不知道这两个有什么区别;
    c. 是减少使用双关语,例如add,难以区分是增加还是连接;
  3. 名称的长短应该与作用域相对应,因为作用域越大,该命名的被搜索的可能性就越大,一般来说长命名胜于短命名,可以实现精准搜索;
  4. 类名应该使用名词,方法名应该使用动词;
  5. 使用源自所涉问题领域的命名,例如社区用forum相关,理财用finance相关。

函数

在面向对象的语言中,我们编写一个类,基本是由变量和函数(方法)构成的,变量主要是命名,那函数的编写原则在哪呢?

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    initView();
    findView();
    setView();
    setListener();
}

1.函数要尽可能短小,当一个函数过于长,比如我们一个Activity的onCreate()方法,这时候我们就应该适当的抽取出一些新的函数出来;

  1. 函数应该只做一件事,并做好这件事,例如initView()就应该做初始化布局的工作,把findViewById(R.id.XXX)的工作留给findView()来完成;
  2. 函数的语句应该在同一抽象级上,例如上面onCreate()里面的方法就属于较高抽象级,不要突然给一个控件来一句setColor(),这感觉就有点杂乱了;

Tips:无论是写代码还是,读代码,都应遵循自顶向下的原则;

if(isExists(username)) {
    setNewPassword(username, newPassword);
    ... ...
}
  1. 函数要么只做一件事,要么回答一个问题,不要同时做着两个工作;
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);
    }
}
  1. 使用异常去代替返回错误码,主要有以下几点原因:
    a. 异常的错误处理代码能直接从主路径中分离出来;
    b. 明确错误处理的函数工作责任单一性;
    c. 避免”依赖磁铁“的重新编译和部署的麻烦性;

其他的一些原则:

  1. 函数的参数应该尽可能少,如果参数太多了,可能会增加我们对这个函数的理解和使用难度,这时候我们应该把这些参数抽象成对象;
  2. 抽取方法,消除冗余,减少重复,这实际在我们的开发中经常遇到,可以算是一个经验了,功能先实现,然后完成后慢慢打磨代码,找到重复的地方,抽取出来进行优化,当然也包括code review的时候有人会去检查代码的冗余程度;

注释

首先强调一点:若代码足够有表达力,那么并不需要注释。这也是本书对注释一个最核心的概念,所以:

  1. 注释并不能美化糟糕的代码,能用代码阐述行为的就不要用注释去解释代码;
  2. 好的注释包括:
    a. 法律信息,公司信息,作者等;
    b. 提供信息的注释,例如正则表达式的注释;
    c. 对意图的注释,代码中可能使用了某个int值或者小技巧例如延时操作,需要注释来解释意图;
    d. 警示,例如不推荐使用,但还没重构完成的方法;
    e. TODO注释,标注还没完成的工作,方便定位
    f. API文档的Javadoc;
  3. 坏的注释包括:
    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() {
        ......
    }
}

类应该从一组变量列表开始。

如果有公共静态常量,应该先出现,然后是私有静态变量,以及私有实体变量,很少会有公共变量。
公共函数应跟在变量列表之后,把由某个公共函数调用的私有工具函数紧随在该公共函数后面,这符合了自顶向下原则。

类的设计原则:

  1. 类应该短小
  2. 系统应该由许多短小的类而不是少量巨大的类组成;
  3. 保持内聚性就会得到许多短小的类;
  4. 如果类丧失了内聚性,就拆分它;
  5. 类只应该有一个权责——只有一个修改的理由(单一权责原则,SRP);
  6. 类应该对扩展开发,对修改封闭(开放-闭合原则,OCP);
  7. 类应该依赖于抽象而不是具体细节,通过部件之间的解耦来隔离修改(依赖倒置原则,DIP);

跌进

软件项目的主要成本在于长期维护,所以在我们长期开发和维护代码的过程中

主要有以下几个简单的设计原则:

  1. 运行所有测试,同样编写测试越多,就会越遵循DIP之类的原则,使用依赖注入,接口和抽象等工具尽可能减少耦合;
  2. 重构,在重构过程中,可以应用有关优秀软件设计的一切知识,提升内聚性,降低耦合度;
  3. 不可重复,重复是良好设计系统的大敌。它代表着额外的工作、额外的风险和额外不必要的复杂度;
  4. 提高表达力,选用好名称、保持函数和类尺寸短小、标准命名法,其实说白了就是提高代码的可读性;
  5. 尽可能少的类和方法,有时候一些类和方法是被刻意写出来的,需不需要为此创建新的类和方法需要灵活处理。

总结:

测试一定是处于第一位的,测试在保证功能正确的同时,也是我们进行重构的最基本保证。

为了使代码可测试,我们必须在开发的过程中不断的打磨代码,持续的降低耦合度,提高内聚,使类与函数功能单一、简单。

从这个意义上讲,测试也是重构的持续驱动力。


并发编程

本章只是简单介绍了一些并发的概念以及处理并发时应该注意的一些问题

一些并发编程的认识:

  1. 编写并发程序会在代码上增加额外的开销。;
  2. 正确的并发是非常复杂的,即使对于很简单的问题;
  3. 并发中的缺陷因为不易重现也不容易被发现;
  4. 并发往往需要对设计策略从根本上进行修改;

一些并发编程的原则:

  1. 单一职责:并发已经足够复杂,我们更需要代码分离,分离线程相关代码和非线程相关代码,单一权责,尽可能降低其复杂度;
  2. 限制临界资源的作用域:为临界资源加锁是防止并发的策略,但是必须正确的加锁,如果形成等待环,就导致死锁;
  3. 利用数据副本在线程之间传递数据:避免线程之前操作的并发影响;线程独立,使其在自己的环境中运行,不能其他线程共享数据;
  4. 线程尽可能独立:让线程存在于自己的世界中,不与其他线程共享数据;
  5. 对于临界资源加锁应尽量保持加锁范围尽可能的小。

一些并发编程的基础定义:


并发编程

一些并发编程的执行模型:

1.生产者-消费者模型
一个或多个生产者线程创建某些工作,并置于缓存或者队列中。
一个或者多个消费者线程从队列中获取并完成这些工作。生产者和消费者之间的队列是一种限定资源。

2.读者-作者模型
当存在一个主要为读者线程提供信息源,但只是偶尔被作者线程更新的共享资源,吞吐量就会是个问题。
增加吞吐量,会导致线程饥饿和过时信息的积累。
协调读者线程不去读取正在更新的信息,而作者线程倾向于长期锁定读者线程。

3.宴席哲学家
许多企业级应用中会存在进程竞争资源的情形,如果没有用心设计,这种竞争会遭遇死锁,活锁,吞吐量和效率低等问题。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容