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

翻译 2012年03月23日 17:37:01

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

        

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


下面就是我们使用的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。选一种有意义的排序并坚持使用它。
  • 使用空行把大段的代码分成不同的逻辑”段落“


Android艺术开发探索第四章——View的工作原理(上)

Android艺术开发探索第四章——View的工作原理(上) 这章就比较好玩了,主要介绍一下View的工作原理,还有自定义View的实现方法,在Android中,View是一个很重要的角色,简单来...
  • qq_26787115
  • qq_26787115
  • 2016年11月28日 22:24
  • 3685

《编写可读代码的艺术》读书笔记(二)

第一部分介绍了“表面层次的改进”,一次一行,在没有很大风险也不需要花很大代价的情况下改进代码的可读性。接下来,第二部分将讨论“简化循环和逻辑”这个主题,相对第一部分,第二部分的技巧方法通常都需要对代码...
  • e5Max
  • e5Max
  • 2013年10月03日 23:14
  • 2099

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

发现以前看《编写可读代码的艺术》做的思维导图,今天有空就放上来了。 个人觉得这边还是挺值得看的,尤其是新人,可以很好的遵循里面的一些编码规范,养成好习惯。 就算是老鸟,也是可以参考总结一下的,我是...
  • sujun10
  • sujun10
  • 2017年01月21日 18:36
  • 661

编写代码的艺术

一 . 为了统一公司软件开发设计过程的编程规范   二 . 使网站开发人员能很方便的理解每个目录 , 变量,控件,类,方法的意义   三 . 为了保证编写出的程序都符合相同的规范, 保证一致性、统一性...
  • dfshsdr
  • dfshsdr
  • 2016年10月09日 19:12
  • 617

读书报告之《修改代码的艺术》 (I)

《修改代码的艺术》,英文名《Working Effectively with Legacy Code》,中文翻译的文笔上绝对谈不上“艺术”二字,愧对艺术二字(当然译者不是这个意思)。书中第三部分不论是...
  • crylearner
  • crylearner
  • 2014年09月05日 00:20
  • 3116

代码重构的艺术

最近在看一些软件工程和构建方面的书籍。俗话说,书读百遍,其义自见,看得多了,自然也就有一些知识可以拿得出手分享一二了。由于我在写代码的过程中往往很追求代码的优雅和简洁,因此我着重阅读了代码重构方面的知...
  • kelekexiao123
  • kelekexiao123
  • 2016年10月16日 20:41
  • 704

ThoughtWorks(中国)程序员读书雷达

软件业的特点是变化。若要提高软件开发的技能,就必须跟上技术发展的步伐。埋首醉心于项目开发与实战,固然能够锤炼自己的开发技巧,却难免受限于经验与学识。世界上并不存在速成的终南捷径,但阅读好的技术书籍,尤...
  • aristolto
  • aristolto
  • 2016年02月25日 16:55
  • 505

Spring in Action(第四章 面向切面编程)学习笔记

1.在软件开发中,分布于应用中多处的功能被称为横切关注点。这些横切关注点从概念上是与应用的 业务逻辑相分离的,将这些横切关注点与业务逻辑相分离正式面向切面编程(AOP)所解决的。依赖注入 有助于应...
  • dfb198998
  • dfb198998
  • 2016年08月27日 10:55
  • 375

编写可读代码的艺术

本文转载自 BeiYuu博客 原文链接:Click me 正文如下: 编写可读代码的艺术 2013-03-21 这是《The Art of Readable Code》的...
  • u014272528
  • u014272528
  • 2014年03月28日 09:07
  • 936

编写可读性代码的艺术

译者序 在做IT的公司里,尤其是软件开发部门,一般不会要求工程师衣着正式。在我工作过的一些环境相对宽松的公司里,很多程序员的衣着连得体都算不上(搞笑的T恤、短裤、拖鞋或者干脆不穿鞋)。我想,我本人也...
  • tiewen
  • tiewen
  • 2014年02月17日 17:13
  • 6800
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:编写易读代码的艺术——第四章 美学
举报原因:
原因补充:

(最多只允许输入30个字)