摘要:概述在代碼整潔之道中提到的軟件工程原則,同樣適用于。我們在軟件工程方面的技術發展剛剛超過年,我們仍然在學習很多東西。不好好一個函數只做一件事目前這是軟件工程中最重要的原則。
概述
Robert C. Martin 在《代碼整潔之道》 中提到的軟件工程原則,同樣適用于 JavaScript。這不是一個風格參考。它指導如何用 JavaScript 編寫可讀、可復用、可重構的軟件。
并不是每一個原則都必須嚴格遵循,甚至很少得到大家的認同。它們僅用于參考,不過要知道這些原則都是_《代碼整潔之道》_的作者們累積多年的集體經驗。
我們在軟件工程方面的技術發展剛剛超過 50 年,我們仍然在學習很多東西。當軟件架構和架構本身一樣古老的時候,我們應該遵循更為嚴格規則?,F在,對于你和你的團隊編寫的 JavaScript 代碼,不妨依據這些準則來進行質量評估。
還有一件事:知道這些不會馬上讓你成為更好的軟件開發者,在工作中常年使用這些準則不能讓你避免錯誤。每一段代碼都從最初的草圖開始到最終成型,就像為濕粘土塑形一樣。最后,當我們與同行一起審查的時候,再把不完美的地方消除掉。不要因為初稿需要改善而否定自己,需要要否定的只是那些代碼!
變量 使用有準確意義的變量名不好:
var yyyymmdstr = moment().format("YYYY/MM/DD");
好:
var yearMonthDay = moment().format("YYYY/MM/DD");在變量的值不會改變時使用 ES6 的常量
在不好的示例中,變量可以被改變。如果你申明一個常量,它會在整個程序中始終保持不變。
不好:
var FIRST_US_PRESIDENT = "George Washington";
好:
const FIRST_US_PRESIDENT = "George Washington";對同一類型的變量使用相同的詞匯
不好:
getUserInfo(); getClientData(); getCustomerRecord();
好:
getUser();使用可檢索的名稱
我們閱讀的代碼永遠比寫的折。寫可讀性強、易于檢索的的代碼非常重要。在程序中使用_無_明確意義的變量名會難以理解,對讀者造成傷害。所以,把名稱定義成可檢索的。
不好:
// 見鬼,525600 是個啥? for (var i = 0; i < 525600; i++) { runCronJob(); }
好:
// 用 `var` 申明為大寫的全局變量 var MINUTES_IN_A_YEAR = 525600; for (var i = 0; i < MINUTES_IN_A_YEAR; i++) { runCronJob(); }使用解釋性的變量
不好:
const cityStateRegex = /^(.+)[,s]+(.+?)s*(d{5})?$/; saveCityState(cityStateRegex.match(cityStateRegex)[1], cityStateRegex.match(cityStateRegex)[2]);
好:
const cityStateRegex = /^(.+)[,s]+(.+?)s*(d{5})?$/; const match = cityStateRegex.match(cityStateRegex) const city = match[1]; const state = match[2]; saveCityState(city, state);避免暗示
顯式優于隱式。
不好:
var locations = ["Austin", "New York", "San Francisco"]; locations.forEach((l) => { doStuff(); doSomeOtherStuff(); ... ... ... // 等等,`l` 又是什么? dispatch(l); });
好:
var locations = ["Austin", "New York", "San Francisco"]; locations.forEach((location) => { doStuff(); doSomeOtherStuff(); ... ... ... dispatch(location); });不要添加沒必要的上下文
如果你的類名稱/對象名稱已經說明了它們是什么,不要在(屬性)變量名里重復。
不好:
var Car = { carMake: "Honda", carModel: "Accord", carColor: "Blue" }; function paintCar(car) { car.carColor = "Red"; }
好:
var Car = { make: "Honda", model: "Accord", color: "Blue" }; function paintCar(car) { car.color = "Red"; }短路語法比條件語句更清晰
不好:
function createMicrobrewery(name) { var breweryName; if (name) { breweryName = name; } else { breweryName = "Hipster Brew Co."; } }
好:
function createMicrobrewery(name) { var breweryName = name || "Hipster Brew Co." }函數 函數參數 (理論上少于等于2個)
限制函數參數的數量極為重要,它會讓你更容易測試函數。超過3個參數會導致組合膨脹,以致于你必須根據不同的參數對大量不同的情況進行測試。
理想情況下是沒有參數。有一個或者兩個參數也還好,三個就應該避免了。多于那個數量就應該考慮合并。通常情況下,如果你有多于2個參數,你的函數會嘗試做太多事情。如果不是這樣,大多數時候可以使用一個高階對象作為參數使用。
既然 JavaScript 允許我們在運行時隨意創建對象,而不需要預先定義樣板,那么你在需要很多參數的時候就可以使用一個對象來處理。
不好:
function createMenu(title, body, buttonText, cancellable) { ... }
好:
var menuConfig = { title: "Foo", body: "Bar", buttonText: "Baz", cancellable: true } function createMenu(menuConfig) { ... }一個函數只做一件事
目前這是軟件工程中最重要的原則。如果函數做了較多的事情,它就難以組合、測試和推測。當你讓函數只做一件事情的時候,它們就很容易重構,而且代碼讀起來也會清晰得多。你只需要遵循本指南的這一條,就能領先于其他很多開發者。
不好:
function emailClients(clients) { clients.forEach(client => { let clientRecord = database.lookup(client); if (clientRecord.isActive()) { email(client); } }); }
好:
function emailClients(clients) { clients.forEach(client => { emailClientIfNeeded(client); }); } function emailClientIfNeeded(client) { if (isClientActive(client)) { email(client); } } function isClientActive(client) { let clientRecord = database.lookup(client); return clientRecord.isActive(); }函數名稱要說明它做的事
不好:
function dateAdd(date, month) { // ... } let date = new Date(); // 很難從函數名了解到加了什么 dateAdd(date, 1);
好:
function dateAddMonth(date, month) { // ... } let date = new Date(); dateAddMonth(date, 1);函數應該只抽象一個層次
如果你有多個層次的抽象,那么你的函數通常做了太多事情,此時應該拆分函數使其易于復用和易于測試。
不好:
function parseBetterJSAlternative(code) { let REGEXES = [ // ... ]; let statements = code.split(" "); let tokens; REGEXES.forEach((REGEX) => { statements.forEach((statement) => { // ... }) }); let ast; tokens.forEach((token) => { // lex... }); ast.forEach((node) => { // parse... }) }
好:
function tokenize(code) { let REGEXES = [ // ... ]; let statements = code.split(" "); let tokens; REGEXES.forEach((REGEX) => { statements.forEach((statement) => { // ... }) }); return tokens; } function lexer(tokens) { let ast; tokens.forEach((token) => { // lex... }); return ast; } function parseBetterJSAlternative(code) { let tokens = tokenize(code); let ast = lexer(tokens); ast.forEach((node) => { // parse... }) }刪除重復代碼
任何情況下,都不要有重復的代碼。沒有任何原因,它很可能是阻礙你成為專業開發者的最糟糕的一件事。重復代碼意味著你要修改某些邏輯的時候要修改不止一個地方的代碼。JavaScript 是弱類型語句,所以它很容易寫通用性強的函數。記得利用這一點!
不好:
function showDeveloperList(developers) { developers.forEach(developers => { var expectedSalary = developer.calculateExpectedSalary(); var experience = developer.getExperience(); var githubLink = developer.getGithubLink(); var data = { expectedSalary: expectedSalary, experience: experience, githubLink: githubLink }; render(data); }); } function showManagerList(managers) { managers.forEach(manager => { var expectedSalary = manager.calculateExpectedSalary(); var experience = manager.getExperience(); var portfolio = manager.getMBAProjects(); var data = { expectedSalary: expectedSalary, experience: experience, portfolio: portfolio }; render(data); }); }
好:
function showList(employees) { employees.forEach(employee => { var expectedSalary = employee.calculateExpectedSalary(); var experience = employee.getExperience(); var portfolio; if (employee.type === "manager") { portfolio = employee.getMBAProjects(); } else { portfolio = employee.getGithubLink(); } var data = { expectedSalary: expectedSalary, experience: experience, portfolio: portfolio }; render(data); }); }使用默認參數代替短路表達式
不好:
function writeForumComment(subject, body) { subject = subject || "No Subject"; body = body || "No text"; }
好:
function writeForumComment(subject = "No subject", body = "No text") { ... }用 Object.assign 設置默認對象
不好:
var menuConfig = { title: null, body: "Bar", buttonText: null, cancellable: true } function createMenu(config) { config.title = config.title || "Foo" config.body = config.body || "Bar" config.buttonText = config.buttonText || "Baz" config.cancellable = config.cancellable === undefined ? config.cancellable : true; } createMenu(menuConfig);
好:
var menuConfig = { title: "Order", // User did not include "body" key buttonText: "Send", cancellable: true } function createMenu(config) { config = Object.assign({ title: "Foo", body: "Bar", buttonText: "Baz", cancellable: true }, config); // 現在 config 等于: {title: "Foo", body: "Bar", buttonText: "Baz", cancellable: true} // ... } createMenu(menuConfig);不要把標記用作函數參數
標記告訴你的用戶這個函數做的事情不止一件。但是函數應該只做一件事。如果你的函數中會根據某個布爾參數產生不同的分支,那就拆分這個函數。
不好:
function createFile(name, temp) { if (temp) { fs.create("./temp/" + name); } else { fs.create(name); } }
好:
function createTempFile(name) { fs.create("./temp/" + name); } function createFile(name) { fs.create(name); }避免副作用
如果一個函數不是獲取一個輸入的值并返回其它值,它就有可能產生副作用。這些副作用可能是寫入文件、修改一些全局變量,或者意外地把你所有錢轉給一個陌生人。
現在你確實需要在程序中有副作用。像前面提到的那樣,你可能需要寫入文件?,F在你需要做的事情是搞清楚在哪里集中完成這件事情。不要使用幾個函數或類來完成寫入某個特定文件的工作。采用一個,就一個服務來完成。
關鍵點是避免覺的陷阱,比如在沒有結構的對象間共享狀態,使用可以被任意修改的易變的數據類型,沒有集中處理發生的副作用等。如果你能做到,你就能比其他大多數程序員更愉快。
不好:
// 下面的函數使用了全局變量。 // 如果有另一個函數在使用 name,現在可能會因為 name 變成了數組而不能正常運行。 var name = "Ryan McDermott"; function splitIntoFirstAndLastName() { name = name.split(" "); } splitIntoFirstAndLastName(); console.log(name); // ["Ryan", "McDermott"];
好:
function splitIntoFirstAndLastName(name) { return name.split(" "); } var name = "Ryan McDermott" var newName = splitIntoFirstAndLastName(name); console.log(name); // "Ryan McDermott"; console.log(newName); // ["Ryan", "McDermott"];不要寫入全局函數
JavaScript 中全局污染是一件糟糕的事情,因為它可能和另外庫發生沖突,然而使用你 API 的用戶卻不會知道——直到他們在生產中遇到一個異常。來思考一個例子:你想擴展 JavaScript 的原生 Array,使之擁有一個 diff 方法,用來展示兩數據之前的區別,這時你會怎么做?你可以給 Array.prototype 添加一個新的函數,但它可能會與其它想做同樣事情的庫發生沖突。如果那個庫實現的 diff 只是比如數組中第一個元素和最后一個元素的異同會發生什么事情呢?這就是為什么最好是使用 ES6 的類語法從全局的 Array 派生一個類來做這件事。
不好:
Array.prototype.diff = function(comparisonArray) { var values = []; var hash = {}; for (var i of comparisonArray) { hash[i] = true; } for (var i of this) { if (!hash[i]) { values.push(i); } } return values; }
好:
class SuperArray extends Array { constructor(...args) { super(...args); } diff(comparisonArray) { var values = []; var hash = {}; for (var i of comparisonArray) { hash[i] = true; } for (var i of this) { if (!hash[i]) { values.push(i); } } return values; } }喜歡上命令式編程之上的函數式編程
如果 Haskell 是 IPA 那么 JavaScript 就是 O"Douls。就是說,與 Haskell 不同,JavaScript 不是函數式編程語言,不過它仍然有一點函數式的意味。函數式語言更整潔也更容易測試,所以你最好能喜歡上這種編程風格。
不好:
const programmerOutput = [ { name: "Uncle Bobby", linesOfCode: 500 }, { name: "Suzie Q", linesOfCode: 1500 }, { name: "Jimmy Gosling", linesOfCode: 150 }, { name: "Gracie Hopper", linesOfCode: 1000 } ]; var totalOutput = 0; for (var i = 0; i < programmerOutput.length; i++) { totalOutput += programmerOutput[i].linesOfCode; }
好:
const programmerOutput = [ { name: "Uncle Bobby", linesOfCode: 500 }, { name: "Suzie Q", linesOfCode: 1500 }, { name: "Jimmy Gosling", linesOfCode: 150 }, { name: "Gracie Hopper", linesOfCode: 1000 } ]; var totalOutput = programmerOutput .map((programmer) => programmer.linesOfCode) .reduce((acc, linesOfCode) => acc + linesOfCode, 0);封裝條件
不好:
if (fsm.state === "fetching" && isEmpty(listNode)) { /// ... }
好:
function shouldShowSpinner(fsm, listNode) { return fsm.state === "fetching" && isEmpty(listNode); } if (shouldShowSpinner(fsmInstance, listNodeInstance)) { // ... }避免否定條件
不好:
function isDOMNodeNotPresent(node) { // ... } if (!isDOMNodeNotPresent(node)) { // ... }
好:
function isDOMNodePresent(node) { // ... } if (isDOMNodePresent(node)) { // ... }避免條件
這似乎是個不可能完成的任務。大多數人第一次聽到這個的時候會說,“沒有 if 語句我該怎么辦?”回答是在多數情況下都可以使用多態來實現相同的任務。第二個問題通常是,“那太好了,不過我為什么要這么做呢?”答案在于我們之前了解過整潔的概念:一個函數應該只做一件事情。如果你的類和函數有 if 語句,就意味著你的函數做了更多的事。記住,只做一件事。
不好:
class Airplane { //... getCruisingAltitude() { switch (this.type) { case "777": return getMaxAltitude() - getPassengerCount(); case "Air Force One": return getMaxAltitude(); case "Cessna": return getMaxAltitude() - getFuelExpenditure(); } } }
好:
class Airplane { //... } class Boeing777 extends Airplane { //... getCruisingAltitude() { return getMaxAltitude() - getPassengerCount(); } } class AirForceOne extends Airplane { //... getCruisingAltitude() { return getMaxAltitude(); } } class Cessna extends Airplane { //... getCruisingAltitude() { return getMaxAltitude() - getFuelExpenditure(); } }避免類型檢查(第1部分)
JavaScript 是無類型的,也就是說函數可以獲取任意類型的參數。有時候你會覺得這種自由是種折磨,因而會不由自主地在函數中使用類型檢查。有很多種方法可以避免類型檢查。首先要考慮的就是 API 的一致性。
不好:
function travelToTexas(vehicle) { if (vehicle instanceof Bicycle) { vehicle.peddle(this.currentLocation, new Location("texas")); } else if (vehicle instanceof Car) { vehicle.drive(this.currentLocation, new Location("texas")); } }
好:
function travelToTexas(vehicle) { vehicle.move(this.currentLocation, new Location("texas")); }避免類型檢查(第2部分)
如果你在處理基本類型的數據,比如字符串,整數和數組,又不能使用多態,這時你會覺得需要使用類型檢查,那么可以考慮 TypeScript。這是普通 JavaScript 的完美替代品,它在標準的 JavaScript 語法之上提供了靜態類型。普通 JavaScript 手工檢查類型的問題在于這樣會寫很多廢話,而人為的“類型安全”并不能彌補損失的可讀性。讓你的 JavaScript 保持整潔,寫很好的測試,并保持良好的代碼審查。否則讓 TypeScript (我說過,這是很好的替代品)來做所有事情。
不好:
function combine(val1, val2) { if (typeof val1 == "number" && typeof val2 == "number" || typeof val1 == "string" && typeof val2 == "string") { return val1 + val2; } else { throw new Error("Must be of type String or Number"); } }
好:
function combine(val1, val2) { return val1 + val2; }不要過度優化
現在瀏覽器在運行時悄悄地做了很多優化工作。很多時候你的優化都是在浪費時間。這里有很好的資源 可以看看哪些優化比較缺乏。把它們作為目標,直到他們能固定下來的時候。
不好:
// 在舊瀏覽器中,每次循環的成本都比較高,因為每次都會重算 `len`。 // 現在瀏覽器中,這已經被優化了。 for (var i = 0, len = list.length; i < len; i++) { // ... }
好:
for (var i = 0; i < list.length; i++) { // ... }刪除不用的代碼
不用的代碼和重復的代碼一樣糟糕。在代碼庫中保留無用的代碼是毫無道理的事情。如果某段代碼用不到,那就刪掉它!如果你以后需要它,仍然可以從代碼庫的歷史版本中找出來。
不好:
function oldRequestModule(url) { // ... } function newRequestModule(url) { // ... } var req = newRequestModule; inventoryTracker("apples", req, "www.inventory-awesome.io");
好:
function newRequestModule(url) { // ... } var req = newRequestModule; inventoryTracker("apples", req, "www.inventory-awesome.io");對象和數據結構 使用 getter 和 setter
JavaScript 沒有接口或者類型,也沒有像 public 和 private 這樣的關鍵字,所以很難應用設計模式。實事上,在對象上使用 getter 和 setter 訪問數據遠好于直接查找對象屬性。“為什么?”你可能會這樣問。那好,下面列出了原因:
你想在獲取對象屬性的時候做更多的事,不必在代碼中尋找所有訪問的代碼來逐個修改。
在進行 set 的時候可以進行額外的數據檢驗。
封裝內部表現。
在獲取或設置的時候易于添加日志和錯誤處理。
繼承當前類,可以重寫默認功能。
可以對對象屬性進行懶加載,比如說從服務器獲取屬性的數據。
不好:
class BankAccount { constructor() { this.balance = 1000; } } let bankAccount = new BankAccount(); // 買鞋... bankAccount.balance = bankAccount.balance - 100;
好:
class BankAccount { constructor() { this.balance = 1000; } // It doesn"t have to be prefixed with `get` or `set` to be a getter/setter withdraw(amount) { if (verifyAmountCanBeDeducted(amount)) { this.balance -= amount; } } } let bankAccount = new BankAccount(); // 買鞋... bankAccount.withdraw(100);讓對象擁有私有成員
這可以通過閉包實現(ES5以之前的版本)。
不好:
var Employee = function(name) { this.name = name; } Employee.prototype.getName = function() { return this.name; } var employee = new Employee("John Doe"); console.log("Employee name: " + employee.getName()); // Employee name: John Doe delete employee.name; console.log("Employee name: " + employee.getName()); // Employee name: undefined
好:
var Employee = (function() { function Employee(name) { this.getName = function() { return name; }; } return Employee; }()); var employee = new Employee("John Doe"); console.log("Employee name: " + employee.getName()); // Employee name: John Doe delete employee.name; console.log("Employee name: " + employee.getName()); // Employee name: John Doe類 單一職責原則 (SRP)
正如《代碼整潔之道》所說,“不應該有超過一個原因來改變類”。往一個類里塞進許多功能是件誘人的事情,就像在坐飛機的時候只帶一個手提箱一樣。這帶來的問題是,你的類不會在概念上有凝聚力,會有很多因素造成對它的改變。讓你的類需要改變的次數最少是件非常重要的事情。這是因為如果一個類里塞入了太多功能,你只修改它的一部分,可能會讓人難以理解它為何會影響代碼庫中其它相關模塊。
不好:
class UserSettings { constructor(user) { this.user = user; } changeSettings(settings) { if (this.verifyCredentials(user)) { // ... } } verifyCredentials(user) { // ... } }
好:
class UserAuth { constructor(user) { this.user = user; } verifyCredentials() { // ... } } class UserSettings { constructor(user) { this.user = user; this.auth = new UserAuth(user) } changeSettings(settings) { if (this.auth.verifyCredentials()) { // ... } } }開放封裝原則(OCP)
正如 Bertrand Meyer 所說,“軟件實體(類、模塊、函數等)應該對擴展開放,對修改封閉。”這是什么意思呢?這個原則基本上規定了你應該允許用戶擴展你的模塊,但不需要打開 .js 源代碼文件來進行編輯。
不好:
class AjaxRequester { constructor() { // 如果我們需要另一個 HTTP 方法,比如 DELETE,該怎么辦? // 我們必須打開這個文件然后手工把它加進去 this.HTTP_METHODS = ["POST", "PUT", "GET"]; } get(url) { // ... } }
好:
class AjaxRequester { constructor() { this.HTTP_METHODS = ["POST", "PUT", "GET"]; } get(url) { // ... } addHTTPMethod(method) { this.HTTP_METHODS.push(method); } }里氏替換原則(LSP)
這是一個嚇人的術語,但描述的卻是個簡單的概念。它的正式定義為“如果 S 是 T 的子類,那所有 T 類型的對象都可以替換為 S 類型的對象(即 S 類型的對象可以替代 T 類型的對象),這個替換不會改變程序的任何性質(正確性、任務執行等)。”這確實是個嚇人的定義。
對此最好的解釋是,如果你有父類和子類,那么父類和子類可以交替使用而不會造成不正確的結果。這可能仍然讓人感到疑惑,那么讓我們看看經典的正方形和矩形的例子。在數學上,正方形也是矩形,但是如果你在模型中通過繼承使用 “is-a” 關系,你很快就會陷入困境。
不好:
class Rectangle { constructor() { this.width = 0; this.height = 0; } setColor(color) { // ... } render(area) { // ... } setWidth(width) { this.width = width; } setHeight(height) { this.height = height; } getArea() { return this.width * this.height; } } class Square extends Rectangle { constructor() { super(); } setWidth(width) { this.width = width; this.height = width; } setHeight(height) { this.width = height; this.height = height; } } function renderLargeRectangles(rectangles) { rectangles.forEach((rectangle) => { rectangle.setWidth(4); rectangle.setHeight(5); let area = rectangle.getArea(); // 不好:這里對正方形會返回 25,但應該是 20. rectangle.render(area); }) } let rectangles = [new Rectangle(), new Rectangle(), new Square()]; renderLargeRectangles(rectangles);
好:
class Shape { constructor() {} setColor(color) { // ... } render(area) { // ... } } class Rectangle extends Shape { constructor() { super(); this.width = 0; this.height = 0; } setWidth(width) { this.width = width; } setHeight(height) { this.height = height; } getArea() { return this.width * this.height; } } class Square extends Shape { constructor() { super(); this.length = 0; } setLength(length) { this.length = length; } getArea() { return this.length * this.length; } } function renderLargeShapes(shapes) { shapes.forEach((shape) => { switch (shape.constructor.name) { case "Square": shape.setLength(5); case "Rectangle": shape.setWidth(4); shape.setHeight(5); } let area = shape.getArea(); shape.render(area); }) } let shapes = [new Rectangle(), new Rectangle(), new Square()]; renderLargeShapes(shapes);接口隔離原則(ISP)
JavaScript 中沒有接口,所以實行這個原則不能像其它語言那樣嚴格。然而即使對 JavaScript 的弱類型系統來說,它仍然是重要的相關。
ISP 指出,“客戶不應該依賴于那些他們不使用的接口?!?由于 Duck Typing 理論,接口在 JavaScript 中是個隱性契約。
在 JavaScript 中有一個很好的例子來演示這個原則,即一個擁有巨大設置對象的類。比較好的做法是不要求客戶設置大量的選項,因為多數時候他們不需要所有設置。讓這些選項成為可選的有助于防止“胖接口”。
不好:
class DOMTraverser { constructor(settings) { this.settings = settings; this.setup(); } setup() { this.rootNode = this.settings.rootNode; this.animationModule.setup(); } traverse() { // ... } } let $ = new DOMTraverser({ rootNode: document.getElementsByTagName("body"), animationModule: function() {} // 多數時候我們不需要動畫 // ... });
好:
class DOMTraverser { constructor(settings) { this.settings = settings; this.options = settings.options; this.setup(); } setup() { this.rootNode = this.settings.rootNode; this.setupOptions(); } setupOptions() { if (this.options.animationModule) { // ... } } traverse() { // ... } } let $ = new DOMTraverser({ rootNode: document.getElementsByTagName("body"), options: { animationModule: function() {} } });依賴倒置原則(DIP)
這個原則說明了兩個基本問題:
1. 上層模塊不應該依賴下層模塊,兩者都應該依賴抽象。
2. 抽象不應該依賴于具體實現,具體實現應該依賴于抽象。
這一開始可能很難理解,但是如果你使用 Angular.js,你已經看到了對這個原則的一種實現形式:依賴注入(DI)。雖然它們不是完全相同的概念,DIP 阻止上層模塊去了解下層模塊的細節并設置它們。它可以通過 DI 來實現。這帶來的巨大好處降低了模塊間的耦合。耦合是種非常不好的開發模式,因為它讓代碼難以重構。
前提已經提到,JavaScript 沒有接口,因此抽象依賴于隱性契約。也就是說,一個對象/類會把方法和屬性暴露給另一個對象/類。在下面的例子中,隱性契約是任何用于 InventoryTracker 的 Request 模塊都應該擁有 requestItems 方法。
不好:
class InventoryTracker { constructor(items) { this.items = items; // 不好:我們創建了一個依賴于特定請求的實現。 // 我們應該只依賴請求方法:`request` 的 requestItems this.requester = new InventoryRequester(); } requestItems() { this.items.forEach((item) => { this.requester.requestItem(item); }); } } class InventoryRequester { constructor() { this.REQ_METHODS = ["HTTP"]; } requestItem(item) { // ... } } let inventoryTracker = new InventoryTracker(["apples", "bananas"]); inventoryTracker.requestItems();
好:
class InventoryTracker { constructor(items, requester) { this.items = items; this.requester = requester; } requestItems() { this.items.forEach((item) => { this.requester.requestItem(item); }); } } class InventoryRequesterV1 { constructor() { this.REQ_METHODS = ["HTTP"]; } requestItem(item) { // ... } } class InventoryRequesterV2 { constructor() { this.REQ_METHODS = ["WS"]; } requestItem(item) { // ... } } // 通過構建外部依賴并注入它們,我們很容易把請求模塊替換成 // 一個使用 WebSocket 的新模塊。 let inventoryTracker = new InventoryTracker(["apples", "bananas"], new InventoryRequesterV2()); inventoryTracker.requestItems();多用 ES6 類語法,少用 ES5 構造函數語法
在經典的 ES5 的類定義中,很難找到易讀的繼承、構造、方法定義等。如果你需要繼承(你會發現做不到),那就應該使用類語法。不過,應該盡可能使用小函數而不是類,直到你需要更大更復雜的對象。
不好:
var Animal = function(age) { if (!(this instanceof Animal)) { throw new Error("Instantiate Animal with `new`"); } this.age = age; }; Animal.prototype.move = function() {}; var Mammal = function(age, furColor) { if (!(this instanceof Mammal)) { throw new Error("Instantiate Mammal with `new`"); } Animal.call(this, age); this.furColor = furColor; }; Mammal.prototype = Object.create(Animal.prototype); Mammal.prototype.constructor = Mammal; Mammal.prototype.liveBirth = function() {}; var Human = function(age, furColor, languageSpoken) { if (!(this instanceof Human)) { throw new Error("Instantiate Human with `new`"); } Mammal.call(this, age, furColor); this.languageSpoken = languageSpoken; }; Human.prototype = Object.create(Mammal.prototype); Human.prototype.constructor = Human; Human.prototype.speak = function() {};
好:
class Animal { constructor(age) { this.age = age; } move() {} } class Mammal extends Animal { constructor(age, furColor) { super(age); this.furColor = furColor; } liveBirth() {} } class Human extends Mammal { constructor(age, furColor, languageSpoken) { super(age, furColor); this.languageSpoken = languageSpoken; } speak() {} }使用方法鏈
在這里我的意見與《代碼整潔之道》的觀點不同。有人認為方法鏈不整潔,而且違反了得墨忒耳定律。也許他們是對的,但這個模式在 JavaScript 中非常有用,你可以很多庫中看到,比如 jQuery 和 Lodash。它讓代碼變得既簡潔又有表現力。在類中,只需要在每個函數結束前返回 this,就實現了鏈式調用的類方法。
不好:
class Car { constructor() { this.make = "Honda"; this.model = "Accord"; this.color = "white"; } setMake(make) { this.name = name; } setModel(model) { this.model = model; } setColor(color) { this.color = color; } save() { console.log(this.make, this.model, this.color); } } let car = new Car(); car.setColor("pink"); car.setMake("Ford"); car.setModel("F-150") car.save();
好:
class Car { constructor() { this.make = "Honda"; this.model = "Accord"; this.color = "white"; } setMake(make) { this.name = name; // NOTE: 返回 this 以實現鏈式調用 return this; } setModel(model) { this.model = model; // NOTE: 返回 this 以實現鏈式調用 return this; } setColor(color) { this.color = color; // NOTE: 返回 this 以實現鏈式調用 return this; } save() { console.log(this.make, this.model, this.color); } } let car = new Car() .setColor("pink") .setMake("Ford") .setModel("F-150") .save();多用組合,少用繼承
大家都知道 GoF 的設計模式,其中提到應該多用組合而不是繼承。對于繼承和組合,都有大量的理由在支撐,但這個準則的要點在于,你的想法本能地會想到繼承,但這時候不防多思考一下用組合是否能更好的處理問題——某些時候,的確能。
你可能會考慮:“我什么時候該用繼承?”這取決于你遇到的問題。這里有一個不錯的清單說明了什么時候用繼承比用組合更合適:
你的繼承是一個“is-a”關系,而不是“has-a”關系(Animal->Human 對比 User->UserDetails)。
可以從基礎復用代碼 (人可以像所有動物一樣移動)。
你想通過修改基礎來實現對所有子類的全局性更改。(改變動物移動時的熱量消耗)。
不好:
class Employee { constructor(name, email) { this.name = name; this.email = email; } // ... } // 這樣不好,因為 Employees "擁有" 稅務數據。EmployeeTaxData 不是屬于 Employee 的一個類型 class EmployeeTaxData extends Employee { constructor(ssn, salary) { super(); this.ssn = ssn; this.salary = salary; } // ... }
好:
class Employee { constructor(name, email) { this.name = name; this.email = email; } setTaxData(ssn, salary) { this.taxData = new EmployeeTaxData(ssn, salary); } // ... } class EmployeeTaxData { constructor(ssn, salary) { this.ssn = ssn; this.salary = salary; } // ... }測試
測試比生產更重要。如果你不進行測試,或者測試的量不夠,那你就不能肯定你寫的代碼不會造成破壞。測試數量依靠你的開發團隊來決定,但 100% 覆蓋率(所有語句和分支)能讓你擁有巨大的信心,也能使程序員們安心。也就是說,你需要一個不錯的測試框架,還需要一個好的覆蓋檢查工具.
沒有什么理由可以讓你不寫測試。這里有 大量不錯的 JS 測試框架,可以去找個你們團隊喜歡的來用。如果你找一個適合在你的團隊中使用的工作,就把為每個新產生的特性/方法添加測試作為目標。如果你喜歡測試驅動開發(TDD)的方法,非常好,但要注意在讓你的測試覆蓋所有特性,或者重構過的代碼。
每次測試一個概念不好:
const assert = require("assert"); describe("MakeMomentJSGreatAgain", function() { it("handles date boundaries", function() { let date; date = new MakeMomentJSGreatAgain("1/1/2015"); date.addDays(30); date.shouldEqual("1/31/2015"); date = new MakeMomentJSGreatAgain("2/1/2016"); date.addDays(28); assert.equal("02/29/2016", date); date = new MakeMomentJSGreatAgain("2/1/2015"); date.addDays(28); assert.equal("03/01/2015", date); }); });
好:
const assert = require("assert"); describe("MakeMomentJSGreatAgain", function() { it("handles 30-day months", function() { let date = new MakeMomentJSGreatAgain("1/1/2015"); date.addDays(30); date.shouldEqual("1/31/2015"); }); it("handles leap year", function() { let date = new MakeMomentJSGreatAgain("2/1/2016"); date.addDays(28); assert.equal("02/29/2016", date); }); it("handles non-leap year", function() { let date = new MakeMomentJSGreatAgain("2/1/2015"); date.addDays(28); assert.equal("03/01/2015", date); }); });Concurrency 使用 Promise 而不是回調
回調并不整潔,它會導致過多的嵌套。ES6 的 Promise 是個內置的全局類型。使用它!
不好:
require("request").get("https://en.wikipedia.org/wiki/Robert_Cecil_Martin", function(err, response) { if (err) { console.error(err); } else { require("fs").writeFile("article.html", response.body, function(err) { if (err) { console.error(err); } else { console.log("File written"); } }) } })
好:
require("request-promise").get("https://en.wikipedia.org/wiki/Robert_Cecil_Martin") .then(function(response) { return require("fs-promise").writeFile("article.html", response); }) .then(function() { console.log("File written"); }) .catch(function(err) { console.error(err); })async/await 比 Promise 還整潔
與回調相當,Promise 已經相當整潔了,但 ES7 帶來了更整潔的解決方案 —— async 和 await。你要做的事情就是在一個函數前加上 async 關鍵字,然后寫下命令形式的邏輯,而不再需要 then 鏈?,F在可以使用這個 ES7 特性帶來的便利!
不好:
require("request-promise").get("https://en.wikipedia.org/wiki/Robert_Cecil_Martin") .then(function(response) { return require("fs-promise").writeFile("article.html", response); }) .then(function() { console.log("File written"); }) .catch(function(err) { console.error(err); })
好:
async function getCleanCodeArticle() { try { var request = await require("request-promise") var response = await request.get("https://en.wikipedia.org/wiki/Robert_Cecil_Martin"); var fileHandle = await require("fs-promise"); await fileHandle.writeFile("article.html", response); console.log("File written"); } catch(err) { console.log(err); } }錯誤處理
拋出錯誤是件好事!這表示運行時已經成功檢測到程序出錯了,它停止當前調用框上的函數執行,并中止進程(在 Node 中),最后在控制臺通知你,并輸出棧跟蹤信息。
不要忽略捕捉到的錯誤捕捉到錯誤卻什么也不錯,你就失去了糾正錯誤的機會。多數情況下把錯誤記錄到控制臺(console.log)也不比忽略它好多少,因為在少量的控制臺信息中很難發現這一條。如果嘗試在 try/catch 中封裝代碼,就意味著你知道這里可能發生錯,你應該在錯誤發生的時候有應對的計劃、或者處理辦法。
不好:
try { functionThatMightThrow(); } catch (error) { console.log(error); }
好:
try { functionThatMightThrow(); } catch (error) { // 選擇之一(比 console.log 更鬧心): console.error(error); // 另一個選擇: notifyUserOfError(error); // 另一個選擇: reportErrorToService(error); // 或者所有上述三種選擇! }不要忽視被拒絕的Promise
這一條與不要忽略從 try/catch 捕捉到的錯誤有相同的原因。
不好:
getdata() .then(data => { functionThatMightThrow(data); }) .catch(error => { console.log(error); });
好:
getdata() .then(data => { functionThatMightThrow(data); }) .catch(error => { // 選擇之一(比 console.log 更鬧心): console.error(error); // 另一個選擇: notifyUserOfError(error); // 另一個選擇: reportErrorToService(error); // 或者所有上述三種選擇! });格式
格式是個很主觀的東西,像這里提到的許多規則一,你不必完全遵循。要點不在于爭論格式。大量工具 可以自動處理優化格式。用一個!讓工程師爭論格式問題簡直就是在浪費時間和金錢。
對于那些不能自動處理的格式(可以自動處理的包括縮進、Tab或空格、雙引號或單引用等),就看看這里的指導。
使用一致的大小寫JavaScript 是無類型的,所以大小寫可以幫助你了解變量、函數等。這些規則具有較強的主觀性,所以你的團隊應該選擇需要的。重點不在于你選擇了什么,而在于要始終保持一致。
不好:
var DAYS_IN_WEEK = 7; var daysInMonth = 30; var songs = ["Back In Black", "Stairway to Heaven", "Hey Jude"]; var Artists = ["ACDC", "Led Zeppelin", "The Beatles"]; function eraseDatabase() {} function restore_database() {} class animal {} class Alpaca {}
好:
var DAYS_IN_WEEK = 7; var DAYS_IN_MONTH = 30; var songs = ["Back In Black", "Stairway to Heaven", "Hey Jude"]; var artists = ["ACDC", "Led Zeppelin", "The Beatles"]; function eraseDatabase() {} function restoreDatabase() {} class Animal {} class Alpaca {}函數調用者和被調用者應該盡可能放在一起
如果一個函數調用另一個函數,那應該讓他們在源文件中的位置非常接近。理想情況下應該把調用者放在被調用者的正上方,這會讓你的代碼更易讀,因為我們都習慣從上往下讀代碼,就像讀報紙那樣。
不好:
class PerformanceReview { constructor(employee) { this.employee = employee; } lookupPeers() { return db.lookup(this.employee, "peers"); } lookupMananger() { return db.lookup(this.employee, "manager"); } getPeerReviews() { let peers = this.lookupPeers(); // ... } perfReview() { getPeerReviews(); getManagerReview(); getSelfReview(); } getManagerReview() { let manager = this.lookupManager(); } getSelfReview() { // ... } } let review = new PerformanceReview(user); review.perfReview();
好:
class PerformanceReview { constructor(employee) { this.employee = employee; } perfReview() { getPeerReviews(); getManagerReview(); getSelfReview(); } getPeerReviews() { let peers = this.lookupPeers(); // ... } lookupPeers() { return db.lookup(this.employee, "peers"); } getManagerReview() { let manager = this.lookupManager(); } lookupMananger() { return db.lookup(this.employee, "manager"); } getSelfReview() { // ... } } let review = new PerformanceReview(employee); review.perfReview();Comments 只注釋業務邏輯復雜的內容
注釋是用來解釋代碼的,而不是必須的。好的代碼應該 _自注釋_。
不好:
function hashIt(data) { // Hash 碼 var hash = 0; // 字符串長度 var length = data.length; // 遍歷數據中所有字符 for (var i = 0; i < length; i++) { // 獲取字符編碼 var char = data.charCodeAt(i); // 生成 Hash hash = ((hash << 5) - hash) + char; // 轉換為32位整數 hash = hash & hash; } }
好:
function hashIt(data) { var hash = 0; var length = data.length; for (var i = 0; i < length; i++) { var char = data.charCodeAt(i); hash = ((hash << 5) - hash) + char; // 轉換為32位整數 hash = hash & hash; } }不要把注釋掉的代碼留在代碼庫中
版本控制存在的原因就是保存你的歷史代碼。
不好:
doStuff(); // doOtherStuff(); // doSomeMoreStuff(); // doSoMuchStuff();
好:
doStuff();不需要日志式的注釋
記住,使用版本控制!沒用的代碼、注釋掉的代碼,尤其是日志式的注釋。用 git log 來獲取歷史信息!
不好:
/** * 2016-12-20: Removed monads, didn"t understand them (RM) * 2016-10-01: Improved using special monads (JP) * 2016-02-03: Removed type-checking (LI) * 2015-03-14: Added combine with type-checking (JR) */ function combine(a, b) { return a + b; }
好:
function combine(a, b) { return a + b; }避免位置標記
位置標記通常只會添加垃圾信息。通過對函數或變量名以及適當的縮進就能為代碼帶來良好的可視化結構。
不好:
//////////////////////////////////////////////////////////////////////////////// // Scope Model Instantiation //////////////////////////////////////////////////////////////////////////////// let $scope.model = { menu: "foo", nav: "bar" }; //////////////////////////////////////////////////////////////////////////////// // Action setup //////////////////////////////////////////////////////////////////////////////// let actions = function() { // ... }
好:
let $scope.model = { menu: "foo", nav: "bar" }; let actions = function() { // ... }避免在源文件中添加版權注釋
這是代碼文件樹頂層的 LICENSE 文件應該干的事情。
不好:
/* The MIT License (MIT) Copyright (c) 2016 Ryan McDermott Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE */ function calculateBill() { // ... }
好:
function calculateBill() { // ... }
本文轉載自:眾成翻譯
譯者:邊城
鏈接:http://www.zcfy.cc/article/2273
原文:https://github.com/ryanmcdermott/clean-code-javascript/blob/master/README.md
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/81134.html
摘要:代碼整潔之道整潔的代碼不僅僅是讓人看起來舒服,更重要的是遵循一些規范能夠讓你的代碼更容易維護,同時降低幾率。另外這不是強制的代碼規范,就像原文中說的,。里式替換原則父類和子類應該可以被交換使用而不會出錯。注釋好的代碼是自解釋的。 JavaScript代碼整潔之道 整潔的代碼不僅僅是讓人看起來舒服,更重要的是遵循一些規范能夠讓你的代碼更容易維護,同時降低bug幾率。 原文clean-c...
摘要:在代碼整潔之道,提出一種軟件質量,可持續開發不僅在于項目架構設計,還與代碼質量密切相關,代碼的整潔度和質量成正比,一份整潔的代碼在質量上是可靠的,為團隊開發,后期維護,重構奠定了良好的基礎。 現在的軟件系統開發難度主要在于其復雜度和規模,客戶需求也不再像Winston Royce瀑布模型期望那樣在系統編碼前完成所有的設計滿足用戶軟件需求。在這個信息爆炸技術日新月異的時代,需求總是在不停...
摘要:我們這里再介紹一下,朱重八家族的名字,都很有特點。取這樣的名字不是因為朱家是搞數學的,而是因為在元朝,老百姓如果不能上學和當官就沒有名字,只能以父母年齡相加或者出生的日期命名。所以說命名不僅僅是一種科學,更是一種藝術。 在小朱元璋出生一個月后,父母為他取了一個名字(元時慣例):朱重八,這個名字也可以叫做朱八八。我們這里再介紹一下,朱重八家族的名字,都很有特點。朱重八高祖名字:朱百六;朱...
摘要:我們這里再介紹一下,朱重八家族的名字,都很有特點。取這樣的名字不是因為朱家是搞數學的,而是因為在元朝,老百姓如果不能上學和當官就沒有名字,只能以父母年齡相加或者出生的日期命名。所以說命名不僅僅是一種科學,更是一種藝術。 在小朱元璋出生一個月后,父母為他取了一個名字(元時慣例):朱重八,這個名字也可以叫做朱八八。我們這里再介紹一下,朱重八家族的名字,都很有特點。朱重八高祖名字:朱百六;朱...
閱讀 2398·2021-11-23 09:51
閱讀 1209·2021-11-22 13:54
閱讀 3422·2021-09-24 10:31
閱讀 1066·2021-08-16 10:46
閱讀 3619·2019-08-30 15:54
閱讀 700·2019-08-30 15:54
閱讀 2886·2019-08-29 17:17
閱讀 3154·2019-08-29 15:08