yuxino / blog

🎬 Life's a Movie
17 stars 2 forks source link

怎样才算是美观的代码 #58

Closed yuxino closed 3 years ago

yuxino commented 6 years ago

好的源代码应当"看上去养眼"。确切的说,有三条原则:

为什么我们要关注代码的审美

假设你不得不用这个类:

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:
                double Average();
    private:   double minium;
    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); 
        double Average();

    private:
        list<double> past_items;  
        int count;          //how many so far
        double minium;
        double maximum;
};

很明显,使用从审美角度来讲让人愉悦的代码更容易让人理解。试想一下,你编程的时间大部分时间都花在看代码上! 浏览代码的速度越快,人们就越容易使用它。

重新安排换行来保持一致和紧凑

假设你在写Java代码来评估你的程序在不同的网络连接速度下的行为。你有一个TcpConnectionSimulator类,它的构造函数有4个参数:

  1. 网络连接的速度(Kbps)
  2. 平均延时(ms)
  3. 延时的"抖动"(ms)
  4. 丢包率(ms)

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

public class PerfomanceTester{
    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很突兀。这违反了"相似的代码看上去很相似"这条原则。

为了让代码看上去更一致,我们可以引入更多换行(同时还可以让注释对其)

public class PerfomanceTester{
    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遍。

下面是写这个类的更紧凑的方法。

public class PerfomanceTester{
    // TcpConnectionSimulator(throughput , latency , jitter , packet_lose)
    //                           [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(ExpandFullName(database_connection,"Jake Brown",&error)
    == "Mr. Jacob Brown III");
assert(error == "");
assert(ExpandFullName(database_connection,"No Such Huy",&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. Jacob Brown III","");
CheckFullName("No such Guy","","no match found");
CheckFullName("John","","more than one result");

现在,很明显这里有4个测试,每个使用了不同的参数。尽管所有的"脏活"都放在了CheckFullName()中,这个函数也没有那么糟糕:

void CheckFullName(string partial_name,
                   string expected_full_name,
                   string expected_error) {
     // database_connnection 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); }

尽管我们的目的仅仅是让代码更有美感,但这个动词同时有几个附带的效果:

这个故事想要传达的寓意是让代码"看上去漂亮"通常会带来不限于表面层次的改进,它可能会帮你把代码的结构组的更好。

在需要时使用列对齐

整齐的边和列能让读者更轻松地浏览文本。

有时你可以借用"列对齐"的方法让代码易读。例如,在前一部分中,你可以用空白来把CheckFullName()的参数排成:

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

在这段代码中,很容易区分出ChecckFullName()的第二个和第三个参数。下面是一个简单的例子,它有一大组变量定义:

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

你可能注意到了,第三个定义有个拼写错误(把request写成了equest)。当所有的内容都这么整齐的排列起来时,这样的错误就很明显。

在wget数据库中,可用的命令行选项(有一百多项)这样列出:

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

这种方式使行这个列表很容易快读和从一列跳到另一列。

应该使用列对齐吗

列对齐的好处是"让相似的代码看起来相似"。但是不可避免的在建立和维护的时候工作量很大。如果再不花费非常多的功夫的时候,可以尝试。如果工作量很大。那么可以不这么做。

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

在很多的情况下,代码的顺序不会影响程序的正确性。例如,下面的5个变量定义可以写成任意的排序:

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

在这种情况下,不要随机地排序,把它们按有意义的方式排列会有帮助。下面是一些想法:

无论使用什么顺序,你在代码中应当始终使用这一顺序。如果后面改变了这个顺序。那会让人很困惑。

按声明按块组织起来

我们的大脑很自然地会按照分组和层次结构来思考,因此你可以通过这样的组织方式来帮助读者快速地理解你的代码。

例如下面是一个签单服务器类C++类,这里有它所有方法的声明:

class ForntendServer{
    public:
      ForntendServer();
      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 ForntendServer{
    public:
      ForntendServer();
      ~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);
}

这个版本容易理解多了。他还更容易读,尽管代码行数更多了。原因是你可以快速的找出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()
    firend_emails = set(f.email for f in friends)
    contacts = import_contacts(user.email, email_password)
    contact_emails = set(c.email for c in contacts)
    non_friend_emails = set(contact_email - friend_emails)
    suggested_friends = User.objects.select(email_in = none_friend_emails)
    display['user'] = user
    display['friends'] = friends
    display['suggested_friends'] = suggested_friends
    return render("suggested_friends.html",display)

可能看上去并不明显,但这个函数会经过数个不同的步骤。因此,把这些行代码分成段落会特别有用:

def suggest_new_friends(user, email_password):
    # Get the user's friends' email addresses.
    friends = user.friends()
    firend_emails = set(f.email for f in friends)

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

    # Find matching users that they aren't already friends with.
    non_friend_emails = set(contact_email - friend_emails)
    suggested_friends = User.objects.select(email_in = none_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)

请注意,我们还给每个段落加了一条总结性的注释。这也会帮助读者浏览代码。

个人风格与一致性

有相当一部分审美选择可以归结为个人风格。例如,类定义的大括号应该放在哪里:

class Logger{
    ...
};

还是:

class Logger
{
    ...
};

选择一种风格而非另一种,不会真的影响到代码的可读性。但如果把两种风格混在一起,就会对可读性有影响了。

在日常的开发里应该遵循团队的风格。