《编写可读代码的艺术》读书笔记——代码审美

写代码就像读书时候写作文一样,虽然试卷上没有要求字迹要多工整、篇幅要多具备艺术气息,但是不可否认的是,字迹工整、篇幅合理的作文总能拿较高的分数,屡试不爽。而好的代码规范能够提高工作效率,降低查找代码和团队交接的时间。如何让代码看起来很“养眼”呢?确切地说有三条原则:

  • 使用一致的布局
  • 让相似的代码看上去相似
  • 把相关的代码行分组,形成代码块

举个坏掉的栗子:

class StatsKeeper{
public:
// A class for keeping track of a series of doubles
   void Add(double d);    // and methods for quick statistics about them
 private:  int count;    /* how many so   far
*/private:    double Average();
list
  past_items
         ;double maximum;
};

再举一个好的栗子:

// A class for keeping track of a series of doubles
// and methods for quick statistics about them
class StatsKeeper{
  public:
    void Add(double d);
    double Average();

  private:
    list past_items;
    int count;  // how many so far

    double minimum;
    double maximum;
};

嗯,看到这里你应该才发现第一个例子中少写了一个double minimum;,下面是一些具体操作的要点。

  1. 通过换行来保持一致和紧凑
  2. 在需要时使用列对齐
  3. 选一个有意义的顺序,并始终一致地使用
  4. 把声明按块组织起来
  5. 把代码分成段落
  6. 用方法来整理不规则的东西
  7. 个人风格与一致性
1. 通过换行来保持一致和紧凑#####

有的公司编码规范中有明确规定列字数限制,例如Google是80或100,超过规定都必须自动换行。但除此之外,还有一些没有超过的但也期望程序员能选择换行的情况。举个例子:

public class PerformanceTester{
  public static final TcpConnectionSimulator wifi =  new TcpConnectionSimulator(
    500, /* kbps */ 
    80, /*millisecs latency */  
    200, /* jitter */ 
    1 /* packet loss % */ );

  public static final  TcpConnectionSimulator t3_fiber = 
    new TcpConnectionSimulator(
      4500, /* kbps */ 
      10, /*millisecs latency */  
      0, /* jitter */ 
      0 /* packet loss % */ );

  public static final  TcpConnectionSimulator cell = new TcpConnectionSimulator(
    100, /* kbps */ 
    400, /*millisecs latency */  
    250, /* jitter */ 
    5 /* packet loss % */ );
}

虽然都是定义一个TcpConnectionSimulator对象,但t3_fiber乍一看和它的邻居并不一样,这显然违反了“让相似的代码看上去相似”原则。修改过后是这样的:

public class PerformanceTester{
  public static final TcpConnectionSimulator wifi =  
    new TcpConnectionSimulator(
      500, /* kbps */ 
      80, /*millisecs latency */  
      200, /* jitter */ 
      1 /* packet loss % */ );

  public static final  TcpConnectionSimulator t3_fiber = 
    new TcpConnectionSimulator(
      4500, /* kbps */ 
      10, /*millisecs latency */  
      0, /* jitter */ 
      0 /* packet loss % */ );

  public static final  TcpConnectionSimulator cell = 
    new TcpConnectionSimulator(
      100, /* kbps */ 
      400, /*millisecs latency */  
      250, /* jitter */ 
      5 /* packet loss % */ );
}

当然也可以把注释搬到上面去,毕竟同样的注释写了三遍就要考虑“重构”了。

public class PerformanceTester{
   // TcpConnectionSimulator(thoughput, latency, jetter, packet_loss)
   //                         [kbps]     [ms]     [ms]    [percent]
  public static final  TcpConnectionSimulator cell = 
    new TcpConnectionSimulator(500,80, 200, 1);

  public static final  TcpConnectionSimulator cell = 
    new TcpConnectionSimulator(4500, 10, 0, 0);

  public static final  TcpConnectionSimulator cell = 
    new TcpConnectionSimulator(100, 400, 250, 5);
}

2.在需要的时候使用列对齐#####

整齐的边和列让读者可更轻松的浏览文本,有时候可以选择借用“列对齐”来让代码易读。例如:

CherkFullName("Doug Adams" , "Mr. Douglas Adams"  , "");
CherkFullName("Jake Brown" , "Mr. Jake Brown III" , "");
CherkFullName("No Such Guy", ""                   , "no match found");
CherkFullName("John"       , ""                   , "more than one result");

就像Google的代码规范中并不提倡这样的写法一样,笔者也不建议使用这种风格,因为一旦方法中某个参数需要修改,那么你又要花很多时间去排序。但是下面的情况可适当使用:

# Extract POST parameters to local variables
details  = request.POST.get('details');
location = request.POST.get('location');
phone    = equest.POST.get('phone');
email    = request.POST.get('email');
url      = request.POST.get('url');

Wait!第四行是不是少了点什么???


3. 选一个有意义的顺序,始终一致地使用#####

在很多时候,代码的顺序不会影响其正确性,例如上文五个变量的定义可以写成任意顺序。在这种情况下,不要随机排序,把他们按有意义的方式排序会有帮助。以下是一些排序的原则:

  • 让变量的顺序与对应的HTML表单中字段的顺序相匹配。
  • 从“最重要”到“最不重要”排序。
  • 按字母顺序排序

一旦你采用了某种排序规则,就要在代码中始终如一的遵循它。


4.把声明按块组织起来#####

由于大脑会很自然地按照分组和层次结构来思考,因此可以通过按块组织方式来使读者更快速的理解你的代码。
举个例子:

class FrontendServer{
  public:
    FrontendServer();
    void ViewProfile(HttpRequest* request);
    void OpenDatabase(String location, String user);
    void SaveProfile(HttpRequest* request);
    String ExtractQueryParam(HttpRequest* request, String param);
    void ReplyOk(HttpRequest* request, String html);
    void FindFriends(HttpRequest* request);
    void ReplyNotFound(HttpRequest* request, String error);
    void CloseDatabase(String location);
    ~FrontendServer();
}

作为读者,要想读懂代码,恐怕你会选择先把它们分成不同的组,所以写这段代码的程序员不妨先将它们按块组织在一起,并加上适当的注释,这样代码的可读性将大大提高。

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

    // 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);
}

5. 把代码分成段落#####

字面文字分成段落是由于以下几个原因:

  • 它是一种把相似的想法放在一起并与其他想法分开的方法。
  • 它提供了可见的“脚印”,如果没有它,会很容易找不到你读到哪里了。
  • 它便于段落之间的导航。

出于同样的原因,代码也应该分段,并为每一个段落加上一条总结性的注释。

def suggust_new_friends(user, emaii_password):
    # Get the user's friends' email addresses
    friends = user.friends();
    friend_emails = set(f.email for f in frends)

    # Import all email addresses from this user's email account.
    contacts = import_contacts(user.email, email_password)
    contact_emails = set(c.email for c in contracts)

    # Find matching users that they aren't aleady friends with.
    non_friend_emails = contact_email - friend_emails
    suggested_friends = User.object.select(email_in = non_friend_emails)

    # Display these lists on the page
    display['user'] = user
    display['friends'] = friends
    display['suggested_friends'] = suggested_friends

    return render("suggested_friends.html", display)

6. 用方法来整理不规则的东西#####

假设有一个个人数据库,它提供了下面这个函数:

// Turn a partial_name like "Doug Adams " into "Mr. Douglas Adams".
// If not possible, 'error' is filled with an explannation.
String ExpandFullName(DatabaseConnection dc, String patial_name, String* error);

并且提供一系列的例子来测试:

DatabaseConnection database_connection;
string error;
assert(ExpandFullName(database_connection, "Doug Adams", &error) == "Mr. Douglas Adams");
assert(error == "");
assert(ExpandFullName(database_connection, "Jake Brown", &error) == "Mr. Jacob Brown III");
assert(error == "");
assert(ExpandFullName(database_connection, "No Such Guy", &error) == "");
assert(error == "no match found");
assert(database_connection, "John", &error) == "");
assert(error == "more than one result");

这段代码其实看起来并不“养眼”,因为它没有换行,也没有一致的风格。但更大的问题在于有很多重复的串,例如“assert(ExpandFullName(database_connection,…”, 其中还有很多“error”。要想改进这段代码,可以为它们添加一个辅助方法。就像:

CherkFullName("Doug Adams", "Mr. Douglas Adams", "");
CherkFullName("Jake Brown", "Mr. Jake Brown III", "");
CherkFullName("No Such Guy", "", "no match found");
CherkFullName("John", "", "more than one result");

很明显这里有4个测试,每个使用了不同的参数,并把所有的“脏活”都放在CherkFullName()中:

void CherkFullName(String partial_name,
                   String expected_full_name,
                   String expected_error) {
//database_connection is now a class member
String error;
String full_name = ExpandFullName(database_connection, partial_name, &error);
assert(error == expected_error); 
assert(full_name == expected_full_name);
}

7. 个人风格与一致性#####

最后想说的是一些个人的编程风格,例如大括号该放在哪里:

public void function
{
    …
}

还是:

public void function {
    …
}

又比如说"Tabs versus Space" ,笔者的建议是按照团队定好的风格来写,只采取一种,至于是采取哪一种,那就见仁见智了。

你可能感兴趣的:(《编写可读代码的艺术》读书笔记——代码审美)