关闭

编写易读代码的艺术——第四章 美学

标签: emailstringdatabaseclassuserstatistics
553人阅读 评论(0) 收藏 举报
分类:

一本杂志的布局包含了很多的思想。例如,段落的长度,列的宽度,文章的顺序,还有封面故事等等。一本好的杂志,可以方便的跳着看,或顺着看。

        

        好的源代码也应该“看得顺眼”。这一章,我们将展示如何更好的使用空格,对齐和排序使你的代码更容易阅读。


下面就是我们使用的3个原则:

  •  使用风格一致的布局,最好是能让读者习惯的样式
  •  让相似的代码看起来也相似
  •  把相关的代码组成一个块

为什么美学重要?


想象一下你的代码有如下类:


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 
*/ public:
        int GetAvg(int k);

private:   double minimum;
list<double>
  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);
  int GetAvg(int k);
 
 private:
  list<double> past_items;
  int count;  // how many so far

  double minimum;
  double maximum;
};

很明显,理解那些有美学愉悦性的代码更容易。你想想看,你编程的大部分时间是花在读代码上!你的代码能被浏览得越快,就越容易被其他人使用。


重新布置换行,使代码一致和简洁


假设你在写一个Java的代码来评估你的程序在不同的网速下的表现。你有一个在构造器带四个参数的TcpConnectionSimulator方法:


1.网速(Kbps)

2.平均响应时间(ms)

3.不稳定响应时间(ms)

4、丢包率(percentage)


你的代码需要3个不同的TcpConnectionSimulator实例:


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(
          45000, /* 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 % */);
};

这段代码需要很多换行来满足80字字母的限制(这是你公司的编码标准)。不幸的是,这使得t3_fiber的定义与旁边的定义看上去不一样了。这段代码的轮廓很奇怪,把人的注意力都无故的吸引到t3_fiber上了。这段代码没有遵循“相似的代码应看起来也相似”得原则。


为了使代码看起来一致,我们可以引进另外的换行符:


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(
          45000, /* 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 % */);
};

这段代码就有一致的样式,更容易快速浏览。但不好的地方时,它占用了太多行。并且注释重复了3遍。


这里有一种够紧凑的方式:


class PerformanceTester {
  // TcpConnectionSimulator(throughput, latency, jitter, packet_loss)
  //                            [Kbps]   [ms]    [ms]    [percent]

  public static final TcpConnectionSimulator wifi =
      new TcpConnectionSimulator(500,    80,     200,    1);

  public static final TcpConnectionSimulator t3_fiber =
      new TcpConnectionSimulator(45000,  10,     0,      0);

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

我们把注释放到了最上面,然后把所有参数都放在同一行。现在,即使注释不在每个数字的旁边,但数据都排在一个紧凑的表里。


用方法清楚不规则


假设你有一个提供如下方法的个人数据库:


// Turn a partial_name like "Doug Adams" into "Mr. Douglas Adams".
// If not possible, 'error' is filled with an explanation.
string ExpandFullName(DatabaseConnection dc, string partial_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(ExpandFullName(database_connection, "John", &error) == "");
assert(error == "more than one result");

这段代码看起来没有美学愉悦感。有些行太长不得不换行。而且代码的轮廓看上去很丑,也没有使用一致的样式。

这个情况下,换行只能做到那么多。一个更大的问题是有很多重复的字符串像“assert(ExpandFullName(database_connection...,”,和"error“等等。为了真正的能改进这段代码,我们需要一个方法,这样代码就能看起来如下:


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

现在,就能很清晰的知道有四个测试,每个测试都有不同的参数。即使所有的”脏活“都在ChenkFullName()里面,这个方法看起来也不会很坏:


void CheckFullName(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);
} 

即使我们的目的是使代码看起来更具美感,这些改变还带来了很多额外额外的好处:

  • 他去掉了很多重复的代码,是代码更紧凑
  • 一眼就能看到每个测试用例的重要部分(名字和错误字符串)。之前,这些字符串与database_connection和error混在一起,很难一眼看出来。
  • 添加新的测试时更容易了。


这个故事的意义在于,使代码“看起来更美”不只是表面上的改进——也肯能帮你更好的组织你的代码。


有用的话使用列对齐


读者更容易浏览对齐的边和列。


有时候,你可以引进“列对齐”使代码更易于阅读。例如,上个例子,ChecnkFullName的参数的空格和对齐可以如下:


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

这个例子中,第二和第三个参数更容易被分辨出来。


以下例子有一大组的变量定义:


# 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')

正如你所发现,第三个变量定义有个拼写错误。这样的错误在这样简洁的排列下很容易被发现


在wget代码库里,可选的命令行选项(超过100个)是这样列出来的:


commands[] = {
  ...
  { "timeout",          NULL,                   cmd_spec_timeout },
  { "timestamping",     &opt.timestamping,      cmd_boolean },
  { "tries",            &opt.ntry,              cmd_number_inf },
  { "useproxy",         &opt.use_proxy,         cmd_boolean },
  { "useragent",        NULL,                   cmd_spec_useragent },
  ...
}

这样使得列表极易地浏览,从一列跳到另一列。


你需要使用列对齐么?


列的边缘会提供“视觉扶手”让代码更容易被扫描。是个“相似的代码应看起来相似”的好例子“。


但有些程序员不喜欢。一个原因是对齐需要更多的时间来建立和维护。另一个原因是修改的时候会差生巨大的差异——一行修改会导致另外的5行都要修改。


我们的建议是你可以试试。我们的经验里,他没有需要程序员想象的那么多工作。如果真的花了太多精力,你只要不那么做就行了。


选一个有意义的顺序,并一直使用它


这是一个许多情况下,顺序并不影响代码正确性的例子。你可以以任意顺序书写下面5个变量的定义:


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

这种情况,把他们按一定顺序排列,而不是随机写,是有帮助的。下面是一些选择:

  • 与HTML里<input>字段的顺序保持一致
  • 把他们按重要性重大到小排列
  • 根据字母顺序排列

不管怎样排列,你必须在你的所有代码里使用这个排序方法。如果你改名排序的方式,就会让人疑惑:


if details:  rec.details  = details
if phone:    rec.phone    = phone     # Hey, where did 'location' go?
if email:    rec.email    = email
if url:      rec.url      = url
if location: rec.location = location  # Why is it down here now?

把声明组织到一块


大脑天生是以分组和分级的方式思考的。所以,你可以用这个方法帮助你的读者更快理解的代码,

例如,这是个前端服务器的C++类,所以得方法定义如下:


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

这段代码没什么不好的地方。但是它的布局不会让读者知道这个类是干嘛的。所有的方法都放在一起,就像与同一件事相关一样。实际上,这些方法可以按逻辑组织成不同的组,像这样:


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

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

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

这个版面更易理解。也更容易阅读即使他增加了许多行,原因是你能很快弄清楚4个不同的部分,需要的话,可以再阅读每部分的细节。


把代码分段


文章被分成不同的段落有很多原因:

  • 这样能把相似的东西放在一起,与其他不同的分开。
  • 这样能提供一个视觉的”脚踏石“——没有段落,你一不小心就不知道看到哪里了。
  • 它方便你能从一段跳到下一段。

由于以上原因,代码也应该分段。例如,没人会喜欢阅读下面那么一大段代码:


# Import the user's email contacts, and match them to users in our system.
# Then display a list of those users that he/she isn't already friends with.
def suggest_new_friends(user, email_password):
  friends = user.friends()
  friends.sort(key=lambda u: u.display_name)
  email_contacts = import_contacts(user.email, email_password)
  emails = [ec.email for ec in email_contacts]
  contact_users = User.objects.filter(email__in=emails).exclude(email=user.email)
  suggested_friends = [cu for cu in contact_users if cu not in friends]
  display['user'] = user
  display['friends'] = friends
  display['suggested_friends'] = suggested_friends
  return render("suggested_friends.html", display)

也许很不明显,但是这个方法有很多不同的独立的步骤。所以把他分成不同的段落是很有用的:


def suggest_new_friends(user, email_password):
  friends = user.friends()
  friends.sort(key=lambda u: u.display_name)

  email_contacts = import_contacts(user.email, email_password)
  emails = [ec.email for ec in email_contacts]

  contact_users = User.objects.filter(email__in=emails).exclude(email=user.email)
  suggested_friends = [cu for cu in contact_users if cu not in friends]

  display['user'] = user
  display['friends'] = friends
  display['suggested_friends'] = suggested_friends

  return render("suggested_friends.html", display)

就像文章一样,把这段代码分段有很多不同的方式,有些程序员会喜欢长一些的,有些喜欢短的。

对于做很多事的段,最好注释总结一下它在做什么:


def suggest_new_friends(user, email_password):
  friends = user.friends()
  friends.sort(key=lambda u: u.display_name)

  # Import all email addresses from this user's email account
  email_contacts = import_contacts(user.email, email_password)
  emails = [ec.email for ec in email_contacts]

  # Find matching users that they aren't already friends with
  contact_users = User.objects.filter(email__in=emails).exclude(email=user.email)
  suggested_friends = [cu for cu in contact_users if cu not in friends]

  display['user'] = user
  display['friends'] = friends
  display['suggested_friends'] = suggested_friends

  return render("suggested_friends.html", display)

当扫描这段代码的时候,这些注释就能代替下面的代码。


个人风格 VS. 一致性


很多美学选择完全取决于个人风格。例如,对于一个类的括号,可以:


class Logger {
  ...
};

也可以:


class Logger
{
  ...
};

一旦选择其中一种,他不会影响代码的阅读性,但是,两种风格同时被使用,可读性是会受影响的。


我们参加的许多项目,感到代码使用了”错误“的风格,但我们仍然遵循原来的项目惯例因为我们知道一致性更重要


总结


所有人都喜欢阅读具体美学愉悦性的代码。通过一致,有意义的方式”格式化“你的代码,它就会更易快速的阅读。

如下是我们讨论的一些技巧:

  • 如果一块代码做相似的事情,那么就给他们相似的样子。
  • 如果”让你的代码好看“导致更有意义的改进,那很好!
  • 把代码按”列“排成一线,能让你的代码更易浏览
  • 如果在代码中一处提到A,B,C,就不要在另外地方说成B,C,A。选一种有意义的排序并坚持使用它。
  • 使用空行把大段的代码分成不同的逻辑”段落“


0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:27071次
    • 积分:511
    • 等级:
    • 排名:千里之外
    • 原创:15篇
    • 转载:6篇
    • 译文:8篇
    • 评论:2条
    文章分类
    最新评论