30 August 2014

本文主要摘自 《编写可读代码的艺术》(The Art of Readable Code)

###代码即文档

可读性基本原理

你所写的代码应当让别人理解它所需的时间最小化

####避免空洞的词语选择更专业的词语

get() 很抽象,应该具体指明 get 什么,从哪里get,当然,你如果把这两个作为get 的参数,也是一个好的命名。

size() 就不如 Height(), NumNodes(), MemoryBytes() 等更具象的词

尽量少使用 tmp, ret,i,j, iter 等空泛的词语,取而代之的是具象的,tmp 具体指什么xxx_tmp,ret 具体返回什么,比如xxx_ret, 循环中具体是循环什么 xxx_i,如果可能尽量指明。

对比如下代码

for(int i = 0; i < club.size(), i++)
    for(int j = 0; j < club[i].members.size(), j++)
        for(int k = 0; k < users.size(), k++)
            //这样的bug 识别度较下面要难得多
            //if (club[i].members[k] == user[j]) 
            if (club[i].members[j] == user[k])
                count << "user[" << j << "] is in club[" << i << i "]" << endl;

for(int ci = 0; ci < club.size(), ci++)
    for(int mi = 0; mi < club[ci].members.size(), mi++)
        for(int ui = 0; ui < users.size(), ui++)
            if (club[ci].members[mi] == user[ui])
                count << "user[" << j << "] is in club[" << i << i "]" << endl;

当然如果你只有一个for循环,那么i,j,k,iter等都是可以接受的。

####让名字携带更多的信息

比如 string id = "af2d4" //是十六进制的字母,显然 hex_id 更合适

如下
start(int delay)       start(int delay_secs)
sleep(int time)        sleep(int ms)
data                   data_urlenc
  • 如果变量涉及单位,末尾加上单位是一个不错的选择。比如 delay_ms, content_MB
  • 如果是 bool 值,增加 is, can, has,should 等为变量前缀,会使得变量意义非常明确, 如 is_password, is_logical等等
  • 有一套规范来指导不同类型的变量。比如类名,方法名,属性,私有属性,常量,变量,宏,静态变量等等。
  • 经常思考,这个变量别人会理解偏差么?是提高你命名的最有效方法。

    比如 stop(), pause(), kill(), clip(), truncate(), inherit()

    推荐用 begin end 表示不包含最后元素的范围 first last 表示包含最后的元素范围

  • 体味命名后面的意义

    比如 size() countElement() 显然,前面要较后面感觉轻量好多,前面好像数据类型中有length属性, 你可以O(1)得到长度,而后面,显然是经过遍历O(n)计算后的结果

####合理的布局

合理布局

	一般比较好的布局
    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(
                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 % */);
            }

	更简洁、精致的布局
    public 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");

	重构后

	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");

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

 列对齐

 	大多数人觉得列对齐不好,是因为,维护复杂,不过建议自己尝试下,也许只是看起来而已。
 	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");

	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},
	...
	};

代码按块组织

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



	# 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()
		friend_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 = contact_emails - friend_emails
		suggested_friends = User.objects.select(email__in=non_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()
		friend_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 = contact_emails - friend_emails
		suggested_friends = User.objects.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)

一致性比风格更重要

比如
	if () {
	}

	if ()
	{
	}

还有
	void
	func()
	{}

	void func()
	{}

等等仅仅是风格问题,但一致性是最重要的。

####注释

我假定你的代码已经是基本能够自解释的了。
  • 不要因为代码命名不好而注释
  • 给代码有歧义、陷阱、瑕疵写注释。如TODO, FIXME(无法运行的代码),HACK(对一个问题不得不采用粗暴的办法),XXX(危险)
  • 给关键的常量注释。不是所有的常量需要注释,如果一些参数不建议随意修改,那么应该注释指明,如 node_core_ratio = 2; //当 rate为2时性能最好。
  • 对一个接口、文件的总结性进行注释,这样让读者很容易立刻接口或文件主要做了什么。
  • 对非常规的做法注释,这可以让维护者了解作者采用这样做法的考虑。再好的代码只能告诉自己是怎么样的,但无法告诉维护 代码的人,为什么这样做。
  • 给代码的特殊情况写注释,但这不是必须的。比如,

    split(path) //指明,你对 “/a/b/c/” 和 “/a/b/c” 的处理的区别。

####变量

渐少变量就会减少代码行数,也就使得代码更加简洁。但一味追求减少变量定义并不是可取的。如下的原则可以指导我们更好地判读:

  • 没有影响代码的可读性
  • 减少冗余变量。
  • 减小变量的作用域。
  • 尽量渐少变量被再次赋值。

    var setFirstEmptyInput = function (new_value) { var found = false; var i = 1; var elem = document.getElementById(‘input’ + i); while (elem !== null) { if (elem.value === ‘’) { found = true; break; } i++; elem = document.getElementById(‘input’ + i); } if (found) elem.value = new_value; return elem; };

    var setFirstEmptyInput = function (new_value) { for (var i = 1; true; i++) { var elem = document.getElementById(‘input’ + i); if (elem === null) return null; // Search Failed. No empty input found. if (elem.value === ‘’) { elem.value = new_value; return elem; } } };

  • 什么是冗余的变量? 没有拆分任何表达式 没有更好澄清变量的意义 只用过一次

    now = datetime.datetime.now() root_message.last_view_time = now

    root_message.last_view_time = datetime.datetime.now()

####代码重构

  • 为了关注高层实现,如果代码在解决一个子问题,就应该在一个独立的接口中完成。
  • 每个一个独立的接口是可测试的
  • 某些代码可以独立为基础的工具或库
  • 把一个接口的实现过于分散有时候又是对可读性的伤害,必须学会平衡。
  • 一个接口只做一件事。

这并不是鼓励分散接口,除非这个独立的功能代码可能被重用或只一个完整的子功能。 如果通过简单的空行能够实现就应该用空行,