[读书笔记]: 可读代码的艺术


本文地址:https://www.cnblogs.com/hchengmx/p/14826739.html

本文为 《编写可读代码的艺术》
英译《The Art of Readable Code》的读书笔记。

整本书的关键思想是:

让别人理解 你这段代码 所需的时间 “最小化”

Code should be written to minimize the time it would take for someone else to understand it.

第一部分 表面层次的改进

从“表面层次”改进代码,这一部分不涉及 代码的重构。主要包括:

  1. 选择好的 方法名、函数名;
  2. 写好的注释
  3. 代码的整洁性。

1. 选择准确的变量名

1.1 准确的动词

案例1: def getPage(url) 这个函数名

用get就不明显,是从 数据库中得到一个网页呢?还是从 互联网上得到一个网页?要是从互联网中得到一个页面,可以考虑用 fetchPage() 或者 downloadPage;

案例2: 一段操作数据库结果的代码

results = Database.all_objects.filter("year <= 2011")

这里的filter指代的是 “挑出”,还是减少呢,从代码中其实看不出来的。

一些常用词及其替代词汇

send: deliver、dispatch、announce、distribute、route
find: search、extract、locate、discover
start: launch、create、begin、open
make:create、set up、build、generate、compose、add、new

1.2 准确的变量

案例1:类 BinaryTree 定义如下:

class BinaryTree{
    int size;
}

这里的 Size就很不明显,是期望返回的是 树的高度,节点数,还是树在内存中所占的空间? 对应应该用更专业的词,Height, NumNodes 或者 MemoryBytes。

几点变量命名建议:

  • 推荐用 min 和 max来表示(包含)极限
  • 推荐用 first 和 last 来表示包含的范围
  • 表示 布尔值的时候,加上 is、has、can、should

2. 避免使用 temp / retval / result

不要用 temp 或者 retval 这样的词。因为这种包括不了更多的信息。(temp这个名字只应用于 短期存在 且 临时性 为其存在因素的变量)

3. 避免在迭代器中使用 i/j/k

因为要是用了多重循环,就很难发现错误。

if(clubs[i].members[k] == users[j]) -> if(clubs[ci].members[mi] == users[ui])

4. 为度量变量加上单位

如果变量是一个度量的话(比如 时间长度、字节数),那么最好把名字带上它的单位

var start = (new Date()).getTime() -> var start_ms = (new Date()).getTime();

before after
satrt(int delay) delay -> delay_desc
createCache(int six) size -> size_mb
throttleDownload(float limit) limit -> max_kbps
truncate(text, max_length) maxLength -> max_chars

5. 代码美学

选用列对齐,可以比较整洁,也可以更直接的发现拼写错误

details  = request.POST.get('detail')
location = request.POST.get('location')
phone    = equest.POST.get('phone')
email    = request.POST.get('email')
url      = request.POST.get('url')

把代码分成 "段落"。

class FrontendServer {
    public:
        FrontServer();
        ~FrontServer();

        //Handlers
        void ViewProfile(HttpRequest* request);
        void SaveProfile(HttpRequest* request);
        void FindFriends(HttpRequest* request);

        //Request/Reply Utilities
        string ExtractQueryParam(HttpRequest* request, string param);
        void ReplyOK(HttpRequest* request, string html);
        void ReplyNotFound(HttpRequest* request, string error);

        // Database Helpers
        void OpenDatabase(string location, string user);
        void CloseDatabase(string location);
}

6. 注释

KEY IDEA

Comments should have a high information-to-space ratio

  • 不要添加不必要的注释(从代码就能读出来的含义)

6.1 不要为了注释而注释

// Find the Node in the givin subtree, with the given name, using the given depth
Node* FindNodeInSubtree(Node* subtree, string name, int depth)

添加细节:

// Find a Node with given 'name' or return NULL.
// If depth <=0, only 'subtree' is inspected.
// If depth == N, only 'subtree' and N levels below are inspected.
Node* FindNodeInSubtree(Node* subtree, string name, int depth)

6.2 用注释记录你的想法

标注代码瑕疵

TODO: Stuff I haven’t gotten around to yet
FIXME: Known-broken code here
HACK: Admittedly inelegant solution to a problem
XXX: Danger! major problem here

每个团队都有自己的风格,比如 TODO:只用于重要的问题,也可以用 todo: 或者 mayber-later: 来表示重要的缺陷

6.3 给常量添加注释

// Impose a reasonable limit - no human can read that much anyway.
const int MAX_RSS_SUBSCRIPTION = 1000;

6.4 写出言简意赅的注释

让注释保持紧凑

// The int is the CategoryType
// The first float in the inner pari in the 'score',
// the second is the 'weight'
typedef hash_map<int, pair<float, float>> ScoreMap;

6.5 不要用代词

会导致含义不明。

案例1;
// Insert the data into the cache, but check if it's too big first.

那这里的it指的是数据呢?还是缓存呢?

6.6 举例来说明特别的情况

//...
// Example: Stripe("abba/a/ba", "ab") returns "/a/"
String Stripe(String src, String chars) { ... }

第二部分 简化逻辑

关键思想:把条件、循环以及其他对控制流的改变做得越“自然”越好。运用一种方式使读者不用停下来重读你的代码。

Key Idea: Make all your conditionals, loops, and other changes to control flow as "natural" as possible - written in a way that doesn't make the reader stop and reread your code.

7. 把控制流变得易读

7.1 if

比如我们常用的习惯是 if(length >= 10), while(bytes_actual > bytes_expected)

建议:
比较式的左侧:不断变化的
比较式的右侧:常量

7.2 if else

原则

  • 先处理 正逻辑,再处理负逻辑。比如 用 if(debug) 而不是 if(!debug)

用 三目运算符,替代简单的 if else

time_str += (hour >=12) ? "pm" :"am";

7.3 return

public boolean Contains(String str, String subStr){
    if(str == null || substr == null) return false;
    if(substr.equals("")) return true;
}

7.4 减少嵌套

嵌套会 大大增加读者的思维负担。

if(user_result == SUCCESS) {
    if(premission_result != SUCESS){
        reply.WriteErrors("error reading permissions");
        reply.Done();
        return;
    }
    reply.WriteErrors("");
    else{
        reply.WriteErrors(user_result);
    }
    reply.Done();
}

以上的代码很有可能是 以前的需求是只需要判断一下 "user_result",现在增加了新的需求,还要判断 "premission_result"。

可以改为

if(user_result != SUCCESS){
    reply.WriteError(user_result);
    reply.Done();
    return;
}

if(permission_result != SUCCESS){
    reply.WriteError(permission_result);
    reply.Done();
    return;
}

reply.WriteErrors("");
reply.Done();

8. 拆分超长的表达式

8.1 抽离变量

用变量代替 表达式 里面的东西

final boolean users_owns_document = (request.user.id == document.owner.id);

if(user_owns_document) {
    // user can edit this document...
}

if(!user_owns_document) {
    // document is read-only...
}

8.2 德摩根定律

!(a||b) 等于 !a&&!b,!(a&&b) 等于 !a||!b

后者比前者的可读性更好。

9. 变量与可读性

9.1 减少变量

可以考虑删除的变量:

  1. 临时变量,也就是 tmp 变量;
  2. 控制流变量,也就是
boolean done = false;

while(!done){
    if(...){
        done = true;
        continue;
    }
}

应该考虑改成

while( /* condition*/){
    ...
    if(...){
        break;
    }
}

9.2 减少变量的作用域

要是一个变量,只有一个方法可以用到,就应该把这个变量变为局部变量。

class LargeClass{
    string str_;

    void Method1(){
        str_ = ...;
        Method2();
    }

    void Method1(){
        // Uses str_
    }
} 

就可以改成

class LargeClass{

    void Method1(){
        string str_ = ...;
        Method2();
    }

    void Method1(){
        // Uses str_
    }
} 

要是一个变量用在if里面,可以适当进行简练。

if(Payment* info = database.ReadPaymentInfo()){
    cout << "Use pard: " << info ->amount() <<endl;
}

第三部分 重新组织代码

10. 抽取不相关的子问题

基本原则:

  1. 看看某个函数或代码块,问问你自己:这段代码高层次的目标是什么?
  2. 对于每一行代码,问一下:它是直接为了目标而工作吗?这段代码高层次的目标是什么?
  3. 如果足够的行数在解决不相关的子问题,就应该抽取代码到独立的函数中。

11. 一次只做一件事

假如有一个博客上的投票插件,用户可以给一条评论 按“上” 或 “下”。每条评论的总分为所有投票的和:“up” 代表 +1,“Down”代表分数 -1。

当用户按了一个按钮,会调用以下的JS代码。

vote_changed(old_vote, new_bote);

下面这个函数实现了这个功能,其中用了大量的代码来判断 old_value 和 new_value 的组合情况。

var vote_changed = function(old_vote, new vote){
    vat scroe = get_score();

    if(new_vote != old_vote){
        if(new_vote === 'up'){
            score += (old_vote === 'Down' ? 2:1);
        } else if(new_vote === 'Down'){
            score -= (old_vote === 'Up'? 2:1);
        } else if(new_vote === ''){
            score += (old_vote === 'Up'?-1:1);
        }
    }
    set_score(score);
}

我们可以考虑把这个问题分成两个任务

  1. 把投票解析为 数字值;
  2. 更新分数;
var vote_value = function(vote){
    if(vote === 'Up'){
        return +1;
    }
    if(vote === 'Down'){
        return -1;
    }
    return 0;
}
var vote_changed = function(old_vote, new_vote){
    var score = get_score();

    score -= vote_value(old_vote); //remove the old vote
    score += vote_value(new_vote); //add the new vote

    set_score(score);
}

12. 把想法变成代码

想象对着一个同事,用“自然语言”来描述代码要做什么。

以下代码用来决定 是否授权用户看到这个页面,要是没有,就告诉用户没有授权。

$is_admin = is_admin_request();
if($document){
    if(!$is_admin && ($document['username'] != $_SESSION['username']){
        return not_authorized();
    })else{
        if(!$is_admin){
            return not_authorized();
        }
    }

    //continue rendering the page ...
}

首先从自然语言描述着逻辑:
授权你有两种方式:

  1. 你是管理员;
  2. 你拥有当前文档;

否则,无法授权给你

if(is_admin_request()){
    //authorized
}else if($document && ($(document['username'] == $_SESSION['username']))){
    //authorized
}else{
    return not_authorized();
}

13. 少写代码

  • 不要费力做不需要的feature
  • 创建更多更好的“工具代码”,来减少重复代码;
  • 让你的项目保持分开的子项目状态;
  • 熟悉你周边的库;
posted on 2021-05-29 23:15  hchengmx  阅读(120)  评论(0编辑  收藏  举报