如何重新组织代码提高可读性? (函数层面, part 3)
1. 抽取与主要问题无关的代码
2. 重新组织代码使得一次只做一件事
3. 首先描述功能,然后再实现功能,这样更清楚明了
如何抽出问题无关的子问题? (chap 10)
0. 无关问题的思考
- 看到一个函数或一个代码块, 问自己, "这段代码的高层作用是什么(high-level gloal)"
- 对于每一行代码, 思考"它是直接解决这个目标吗",还是"解决一个子问题来达到目标的解决"
- 如果是解决子问题,并且代码的行数也足够多,那么就可以抽取出一个独立的函数
// Return which element of 'array' is closest to the given latitude/longitude. // Models the Earth as a perfect sphere. var findClosestLocation = function (lat, lng, array) { var closest; var closest_dist = Number.MAX_VALUE; for (var i = 0; i < array.length; i += 1) { // Convert both points to radians. var lat_rad = radians(lat); var lng_rad = radians(lng); var lat2_rad = radians(array[i].latitude); var lng2_rad = radians(array[i].longitude); // Use the "Spherical Law of Cosines" formula. var dist = Math.acos(Math.sin(lat_rad) * Math.sin(lat2_rad) + Math.cos(lat_rad) * Math.cos(lat2_rad) * Math.cos(lng2_rad - lng_rad)); if (dist < closest_dist) { closest = array[i]; closest_dist = dist; } } return closest; }; // Good : clear, easy to test spherical_distance in isolation var spherical_distance = function (lat1, lng1, lat2, lng2) { var lat1_rad = radians(lat1); var lng1_rad = radians(lng1); var lat2_rad = radians(lat2); var lng2_rad = radians(lng2); // Use the "Spherical Law of Cosines" formula. return Math.acos(Math.sin(lat1_rad) * Math.sin(lat2_rad) + Math.cos(lat1_rad) * Math.cos(lat2_rad) * Math.cos(lng2_rad - lng1_rad)); }; var findClosestLocation = function (lat, lng, array) { var closest; var closest_dist = Number.MAX_VALUE; for (var i = 0; i < array.length; i += 1) { var dist = spherical_distance(lat, lng, array[i].latitude, array[i].longitude); if (dist < closest_dist) { closest = array[i]; closest_dist = dist; } } return closest; };
* 纯粹的Utility代码 (比如操作字符串,hash表,读写文件)
这类代码最好封装到一个独立的Util类或者函数,(ReadFileToString())
* 其他通用的代码
ajax_post({ url: 'http://example.com/submit', data: data, on_success: function (response_data) { var str = "{ "; for (var key in response_data) { str += " " + key + " = " + response_data[key] + " "; } alert(str + "}"); }); } // Continue handling 'response_data' ... // Good : 函数名更容易定位,更容易维护 var format_pretty = function (obj) { var str = "{ "; for (var key in obj) { str += " " + key + " = " + obj[key] + " "; } return str + "}"; }; // Better : 功能更强大 var format_pretty = function (obj, indent) { // Handle null, undefined, strings, and non-objects. if (obj === null) return "null"; if (obj === undefined) return "undefined"; if (typeof obj === "string") return '"' + obj + '"'; if (typeof obj !== "object") return String(obj); if (indent === undefined) indent = ""; // Handle (non-null) objects. var str = "{ "; for (var key in obj) { str += indent + " " + key + " = "; str += format_pretty(obj[key], indent + " ") + " "; } return str + indent + "}"; };
* 创造更多通用功能的代码
像之前的ReadFileToString(),format_pretty()都可以跨工程使用。
* 工程特有的功能 (即使如此,依然可以考虑创建在工程内部使用的util类/函数)
* 简化现有的接口 (用到某个第三方的库,它的接口不好用,可以考虑对它封装一下)
- Simplifying an existing interface
- Reshaping an interface to your needs
- Don't take things too far (不要将一个函数实现细分成太多的小函数,要控制粒度)
一次只做一件事情(chap 11, one task at a time)
0. 一个代码片段只做一件事情 (空行风格的代码段)
示例1
// Bad : two things doing together (parseValue, computeScore) var vote_changed = function (old_vote, new_vote) { var score = get_score(); if (new_vote !== old_vote) { if (new_vote === 'Up') { score += (old_vote === 'Down' ? 2 : 1); } else if (new_vote === 'Down') { score -= (old_vote === 'Up' ? 2 : 1); } else if (new_vote === '') { score += (old_vote === 'Up' ? -1 : 1); } } set_score(score); }; // Good : clear to understand var vote_value = function (vote) { if (vote === 'Up') { return +1; } if (vote === 'Down') { return -1; } return 0; }; var vote_changed = function (old_vote, new_vote) { var score = get_score(); score -= vote_value(old_vote); // remove the old vote score += vote_value(new_vote); // add the new vote set_score(score); };
示例2
// Bad : 不方便扩展 var place = location_info["LocalityName"]; // e.g. "Santa Monica" if (!place) { place = location_info["SubAdministrativeAreaName"]; // e.g. "Los Angeles" } if (!place) { place = location_info["AdministrativeAreaName"]; // e.g. "California" } if (!place) { place = "Middle-of-Nowhere"; } if (location_info["CountryName"]) { place += ", " + location_info["CountryName"]; // e.g. "USA" } else { place += ", Planet Earth"; } return place; // Good var town = location_info["LocalityName"]; // e.g. "Santa Monica" var city = location_info["SubAdministrativeAreaName"]; // e.g. "Los Angeles" var state = location_info["AdministrativeAreaName"]; // e.g. "CA" var country = location_info["CountryName"]; // e.g. "USA" // Start with the default, and keep overwriting with the most specific value. var second_half = "Planet Earth"; if (country) { second_half = country; } if (state && country === "USA") { second_half = state; } var first_half = "Middle-of-Nowhere"; if (state && country !== "USA") { first_half = state; } if (city) { first_half = city; } if (town) { first_half = town; } return first_half + ", " + second_half; // Better : 利用js语法的a||b||c的特性 var first_half, second_half; if (country === "USA") { first_half = town || city || "Middle-of-Nowhere"; second_half = state || "USA"; } else { first_half = town || city || state || "Middle-of-Nowhere"; second_half = country || "Planet Earth"; } return first_half + ", " + second_half;