[譯] 程式碼整潔的 JavaScript
程式碼整潔的 JavaScript
目錄
簡介
將源自 Robert C. Martin 的 Clean Code 的軟體工程原則適配到 JavaScript 。 這不是一個程式碼風格指南, 它是一個使用 JavaScript 來生產 可讀的, 可重用的, 以及可重構的軟體的指南。
這裡的每一項原則都不是必須遵守的, 甚至只有更少的能夠被廣泛認可。 這些僅僅是指南而已, 但是卻是 Clean Code 作者多年經驗的結晶。
我們的軟體工程行業只有短短的 50 年, 依然有很多要我們去學習。 當軟體架構與建築架構一樣古老時, 也許我們將會有硬性的規則去遵守。 而現在, 讓這些指南做為你和你的團隊生產的 JavaScript 程式碼的 質量的標準。
還有一件事: 知道這些指南並不能馬上讓你成為一個更加出色的軟體開發者, 並且使用它們工作多年也並 不意味著你不再會犯錯誤。 每一段程式碼最開始都是草稿, 像溼粘土一樣被打造成最終的形態。 最後當我們 和搭檔們一起審查程式碼時清除那些不完善之處, 不要因為最初需要改善的草稿程式碼而自責, 而是對那些代 碼下手。
變數
使用有意義並且可讀的變數名稱
不好的:
const yyyymmdstr = moment().format('YYYY/MM/DD');
好的:
const currentDate = moment().format('YYYY/MM/DD');
為相同型別的變數使用相同的詞彙
不好的:
getUserInfo(); getClientData(); getCustomerRecord();
好的:
getUser();
使用可搜尋的名稱
我們要閱讀的程式碼比要寫的程式碼多得多, 所以我們寫出的程式碼的可讀性和可搜尋性是很重要的。 使用沒有 意義的變數名將會導致我們的程式難於理解, 將會傷害我們的讀者, 所以請使用可搜尋的變數名。 類似 buddy.js 和 ESLint 的工具可以幫助我們找到未命名的常量。
不好的:
// 艹, 86400000 是什麼鬼? setTimeout(blastOff, 86400000);
好的:
// 將它們宣告為全域性常量 `const` 。 const MILLISECONDS_IN_A_DAY = 86400000; setTimeout(blastOff, MILLISECONDS_IN_A_DAY);
使用解釋性的變數
不好的:
const address = 'One Infinite Loop, Cupertino 95014'; const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/; saveCityZipCode(address.match(cityZipCodeRegex)[1], address.match(cityZipCodeRegex)[2]);
好的:
const address = 'One Infinite Loop, Cupertino 95014'; const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/; const [, city, zipCode] = address.match(cityZipCodeRegex) || []; saveCityZipCode(city, zipCode);
避免心理對映
顯示比隱式更好
不好的:
const locations = ['Austin', 'New York', 'San Francisco']; locations.forEach((l) => { doStuff(); doSomeOtherStuff(); // ... // ... // ... // 等等, `l` 是啥? dispatch(l); });
好的:
const locations = ['Austin', 'New York', 'San Francisco']; locations.forEach((location) => { doStuff(); doSomeOtherStuff(); // ... // ... // ... dispatch(location); });
不新增不必要的上下文
如果你的類名/物件名有意義, 不要在變數名上再重複。
不好的:
const Car = { carMake: 'Honda', carModel: 'Accord', carColor: 'Blue' }; function paintCar(car) { car.carColor = 'Red'; }
好的:
const Car = { make: 'Honda', model: 'Accord', color: 'Blue' }; function paintCar(car) { car.color = 'Red'; }
使用預設變數替代短路運算或條件
不好的:
function createMicrobrewery(name) { const breweryName = name || 'Hipster Brew Co.'; // ... }
好的:
function createMicrobrewery(breweryName = 'Hipster Brew Co.') { // ... }
函式
函式引數 (兩個以下最理想)
限制函式引數的個數是非常重要的, 因為這樣將使你的函式容易進行測試。 一旦超過三個引數將會導致組 合爆炸, 因為你不得不編寫大量針對每個引數的測試用例。
沒有引數是最理想的, 一個或者兩個引數也是可以的, 三個引數應該避免, 超過三個應該被重構。 通常, 如果你有一個超過兩個函式的引數, 那就意味著你的函式嘗試做太多的事情。 如果不是, 多數情況下一個 更高階物件可能會滿足需求。
由於 JavaScript 允許我們不定義型別/模板就可以建立物件, 當你發現你自己需要大量的引數時, 你 可以使用一個物件。
不好的:
function createMenu(title, body, buttonText, cancellable) { // ... }
好的:
const menuConfig = { title: 'Foo', body: 'Bar', buttonText: 'Baz', cancellable: true }; function createMenu(config) { // ... }
函式應當只做一件事情
這是軟體工程中最重要的一條規則, 當函式需要做更多的事情時, 它們將會更難進行編寫、 測試和推理。 當你能將一個函式隔離到只有一個動作, 他們將能夠被容易的進行重構並且你的程式碼將會更容易閱讀。 如 果你嚴格遵守本指南中的這一條, 你將會領先於許多開發者。
不好的:
function emailClients(clients) { clients.forEach((client) => { const clientRecord = database.lookup(client); if (clientRecord.isActive()) { email(client); } }); }
好的:
function emailClients(clients) { clients .filter(isClientActive) .forEach(email); } function isClientActive(client) { const clientRecord = database.lookup(client); return clientRecord.isActive(); }
函式名稱應該說明它要做什麼
不好的:
function addToDate(date, month) { // ... } const date = new Date(); // 很難從函式名看出加了什麼 addToDate(date, 1);
好的:
function addMonthToDate(month, date) { // ... } const date = new Date(); addMonthToDate(1, date);
函式應該只有一個抽象級別
當在你的函式中有多於一個抽象級別時, 你的函式通常做了太多事情。 拆分函式將會提升重用性和測試性。
不好的:
function parseBetterJSAlternative(code) { const REGEXES = [ // ... ]; const statements = code.split(' '); const tokens = []; REGEXES.forEach((REGEX) => { statements.forEach((statement) => { // ... }); }); const ast = []; tokens.forEach((token) => { // lex... }); ast.forEach((node) => { // parse... }); }
好的:
function tokenize(code) { const REGEXES = [ // ... ]; const statements = code.split(' '); const tokens = []; REGEXES.forEach((REGEX) => { statements.forEach((statement) => { tokens.push( /* ... */ ); }); }); return tokens; } function lexer(tokens) { const ast = []; tokens.forEach((token) => { ast.push( /* ... */ ); }); return ast; } function parseBetterJSAlternative(code) { const tokens = tokenize(code); const ast = lexer(tokens); ast.forEach((node) => { // parse... }); }
移除冗餘程式碼
竭盡你的全力去避免冗餘程式碼。 冗餘程式碼是不好的, 因為它意味著當你需要修改一些邏輯時會有多個地方 需要修改。
想象一下你在經營一家餐館, 你需要記錄所有的庫存西紅柿, 洋蔥, 大蒜, 各種香料等等。 如果你有多 個記錄列表, 當你用西紅柿做一道菜時你得更新多個列表。 如果你只有一個列表, 就只有一個地方需要更 新!
你有冗餘程式碼通常是因為你有兩個或多個稍微不同的東西, 它們共享大部分, 但是它們的不同之處迫使你使 用兩個或更多獨立的函式來處理大部分相同的東西。 移除冗餘程式碼意味著建立一個可以處理這些不同之處的 抽象的函式/模組/類。
讓這個抽象正確是關鍵的, 這是為什麼要你遵循 Classes 那一章的 SOLID 的原因。 不好的抽象比冗 餘程式碼更差, 所以要謹慎行事。 既然已經這麼說了, 如果你能夠做出一個好的抽象, 才去做。 不要重複 你自己, 否則你會發現當你要修改一個東西時時刻需要修改多個地方。
不好的:
function showDeveloperList(developers) { developers.forEach((developer) => { const expectedSalary = developer.calculateExpectedSalary(); const experience = developer.getExperience(); const githubLink = developer.getGithubLink(); const data = { expectedSalary, experience, githubLink }; render(data); }); } function showManagerList(managers) { managers.forEach((manager) => { const expectedSalary = manager.calculateExpectedSalary(); const experience = manager.getExperience(); const portfolio = manager.getMBAProjects(); const data = { expectedSalary, experience, portfolio }; render(data); }); }
好的:
function showList(employees) { employees.forEach((employee) => { const expectedSalary = employee.calculateExpectedSalary(); const experience = employee.getExperience(); let portfolio = employee.getGithubLink(); if (employee.type === 'manager') { portfolio = employee.getMBAProjects(); } const data = { expectedSalary, experience, portfolio }; render(data); }); }
使用 Object.assign 設定預設物件
不好的:
const 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);
好的:
const 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 now equals: {title: "Order", body: "Bar", buttonText: "Send", cancellable: true} // ... } createMenu(menuConfig);
不要使用標記位做為函式引數
標記位是告訴你的使用者這個函式做了不只一件事情。 函式應該只做一件事情。 如果你的函式因為一個布林值 出現不同的程式碼路徑, 請拆分它們。
不好的:
function createFile(name, temp) { if (temp) { fs.create(`./temp/${name}`); } else { fs.create(name); } }
好的:
function createFile(name) { fs.create(name); } function createTempFile(name) { createFile(`./temp/${name}`); }
避免副作用
如果一個函式做了除接受一個值然後返回一個值或多個值之外的任何事情, 它將會產生副作用, 它可能是 寫入一個檔案, 修改一個全域性變數, 或者意外的把你所有的錢連線到一個陌生人那裡。
現在在你的程式中確實偶爾需要副作用, 就像上面的程式碼, 你也許需要寫入到一個檔案, 你需要做的是集 中化你要做的事情, 不要讓多個函式或者類寫入一個特定的檔案, 用一個服務來實現它, 一個並且只有一 個。
重點是避免這些常見的易犯的錯誤, 比如在物件之間共享狀態而不使用任何結構, 使用任何地方都可以寫入 的可變的資料型別, 沒有集中化導致副作用。 如果你能做到這些, 那麼你將會比其它的碼農大軍更加幸福。
不好的:
// Global variable referenced by following function. // 全域性變數被下面的函式引用 // If we had another function that used this name, now it'd be an array and it // could break it. // 如果我們有另一個函式使用這個 name , 現在它應該是一個數組, 這可能會出現錯誤。 let name = 'Ryan McDermott'; function splitIntoFirstAndLastName() { name = name.split(' '); } splitIntoFirstAndLastName(); console.log(name); // ['Ryan', 'McDermott'];
好的:
function splitIntoFirstAndLastName(name) { return name.split(' '); } const name = 'Ryan McDermott'; const newName = splitIntoFirstAndLastName(name); console.log(name); // 'Ryan McDermott'; console.log(newName); // ['Ryan', 'McDermott'];
不要寫入全域性函式
汙染全域性在 JavaScript 中是一個不好的做法, 因為你可能會和另外一個類庫衝突, 你的 API 的使用者 可能不夠聰明, 直到他們得到在生產環境得到一個異常。 讓我們來考慮這樣一個例子: 假設你要擴充套件 JavaScript 的 原生 Array
, 新增一個可以顯示兩個陣列的不同之處的 diff
方法, 你可以在 Array.prototype
中寫一個新的方法, 但是它可能會和嘗試做相同事情的其它類庫發生衝突。 如果有 另外一個類庫僅僅使用 diff
方法來查詢陣列的第一個元素和最後一個元素之間的不同之處呢? 這就是 為什麼使用 ES2015/ES6 的類是一個更好的做法的原因, 只要簡單的擴充套件全域性的 Array
即可。
不好的:
Array.prototype.diff = function diff(comparisonArray) { const hash = new Set(comparisonArray); return this.filter(elem => !hash.has(elem)); };
好的:
class SuperArray extends Array { diff(comparisonArray) { const hash = new Set(comparisonArray); return this.filter(elem => !hash.has(elem)); } }
函數語言程式設計優於指令式程式設計
JavaScript 不是 Haskell 那種方式的函式式語言, 但是它有它的函式式風格。 函式式語言更加簡潔 並且更容易進行測試, 當你可以使用函數語言程式設計風格時請盡情使用。
不好的:
const programmerOutput = [ { name: 'Uncle Bobby', linesOfCode: 500 }, { name: 'Suzie Q', linesOfCode: 1500 }, { name: 'Jimmy Gosling', linesOfCode: 150 }, { name: 'Gracie Hopper', linesOfCode: 1000 } ]; let totalOutput = 0; for (let 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 } ]; const 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 this.getMaxAltitude() - this.getPassengerCount(); case 'Air Force One': return this.getMaxAltitude(); case 'Cessna': return this.getMaxAltitude() - this.getFuelExpenditure(); } } }
好的:
class Airplane { // ... } class Boeing777 extends Airplane { // ... getCruisingAltitude() { return this.getMaxAltitude() - this.getPassengerCount(); } } class AirForceOne extends Airplane { // ... getCruisingAltitude() { return this.getMaxAltitude(); } } class Cessna extends Airplane { // ... getCruisingAltitude() { return this.getMaxAltitude() - this.getFuelExpenditure(); } }
避免型別檢查 (part 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')); }
避免型別檢查 (part 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; } throw new Error('Must be of type String or Number'); }
好的:
function combine(val1, val2) { return val1 + val2; }
不要過度優化
現代化瀏覽器執行時在幕後做大量的優化, 在大多數的時間, 做優化就是在浪費你的時間。 這些是好的 資源 , 用來 檢視那些地方需要優化。 為這些而優化, 直到他們被修正。
不好的:
// On old browsers, each iteration with uncached `list.length` would be costly // because of `list.length` recomputation. In modern browsers, this is optimized. // 在舊的瀏覽器上, 每次迴圈 `list.length` 都沒有被快取, 會導致不必要的開銷, 因為要重新計 // 算 `list.length` 。 在現代化瀏覽器上, 這個已經被優化了。 for (let i = 0, len = list.length; i < len; i++) { // ... }
好的:
for (let i = 0; i < list.length; i++) { // ... }
移除殭屍程式碼
僵死程式碼和冗餘程式碼同樣糟糕。 沒有理由在程式碼庫中儲存它。 如果它不會被呼叫, 就刪掉它。 當你需要 它時, 它依然儲存在版本歷史記錄中。
不好的:
function oldRequestModule(url) { // ... } function newRequestModule(url) { // ... } const req = newRequestModule; inventoryTracker('apples', req, 'www.inventory-awesome.io');
好的:
function newRequestModule(url) { // ... } const req = newRequestModule; inventoryTracker('apples', req, 'www.inventory-awesome.io');
物件和資料結構
使用 getters 和 setters
JavaScript 沒有介面或型別, 所以堅持這個模式是非常困難的, 因為我們沒有 public
和 private
關鍵字。 正因為如此, 使用 getters 和 setters 來訪問物件上的資料比簡單的在一個物件上查詢屬性 要好得多。 “為什麼?” 你可能會問, 好吧, 原因請看下面的列表:
set
不好的:
class BankAccount { constructor() { this.balance = 1000; } } const bankAccount = new BankAccount(); // Buy shoes... bankAccount.balance -= 100;
好的:
class BankAccount { constructor(balance = 1000) { this._balance = balance; } // It doesn't have to be prefixed with `get` or `set` to be a getter/setter set balance(amount) { if (verifyIfAmountCanBeSetted(amount)) { this._balance = amount; } } get balance() { return this._balance; } verifyIfAmountCanBeSetted(val) { // ... } } const bankAccount = new BankAccount(); // Buy shoes... bankAccount.balance -= shoesPrice; // Get balance let balance = bankAccount.balance;
讓物件擁有私有成員
這個可以通過閉包來實現(針對 ES5 或更低)。
不好的:
const Employee = function(name) { this.name = name; }; Employee.prototype.getName = function getName() { return this.name; }; const 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
好的:
const Employee = function (name) { this.getName = function getName() { return name; }; }; const 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
類
ES2015/ES6 類優先與 ES5 純函式
很難為經典的 ES5 類建立可讀的的繼承、 構造和方法定義。 如果你需要繼承(並且感到奇怪為啥你不需 要), 則優先用 ES2015/ES6的類。 不過, 短小的函式優先於類, 直到你發現你需要更大並且更復雜的 物件。
不好的:
const Animal = function(age) { if (!(this instanceof Animal)) { throw new Error('Instantiate Animal with `new`'); } this.age = age; }; Animal.prototype.move = function move() {}; const 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 liveBirth() {}; const 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 speak() {};
好的:
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.make = make; } setModel(model) { this.model = model; } setColor(color) { this.color = color; } save() { console.log(this.make, this.model, this.color); } } const 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.make = make; // NOTE: Returning this for chaining return this; } setModel(model) { this.model = model; // NOTE: Returning this for chaining return this; } setColor(color) { this.color = color; // NOTE: Returning this for chaining return this; } save() { console.log(this.make, this.model, this.color); // NOTE: Returning this for chaining return this; } } const car = new Car() .setColor('pink') .setMake('Ford') .setModel('F-150') .save();
組合優先於繼承
正如 設計模式四人幫 所述, 如果可能, 你應該優先使用組合而不是繼承。 有許多好的理由去使用繼承, 也有許多好的理由去使用組合。這個格言 的重點是, 如果你本能的觀點是繼承, 那麼請想一下組合能否更好的為你的問題建模。 很多情況下它真的 可以。
那麼你也許會這樣想, “我什麼時候改使用繼承?” 這取決於你手上的問題, 不過這兒有一個像樣的列表說 明什麼時候繼承比組合更好用:
- 你的繼承表示"是一個"的關係而不是"有一個"的關係(人類->動物 vs 使用者->使用者詳情);
- 你可以重用來自基類的程式碼(人可以像所有動物一樣行動);
- 你想通過基類對子類進行全域性的修改(改變所有動物行動時的熱量消耗);
不好的:
class Employee { constructor(name, email) { this.name = name; this.email = email; } // ... } // 不好是因為僱員“有”稅率資料, EmployeeTaxData 不是一個 Employee 型別。 class EmployeeTaxData extends Employee { constructor(ssn, salary) { super(); this.ssn = ssn; this.salary = salary; } // ... }
好的:
class EmployeeTaxData { constructor(ssn, salary) { 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); } // ... }
SOLID
單一職責原則 (SRP)
正如程式碼整潔之道所述, “永遠不要有超過一個理由來修改一個類”。 給一個類塞滿許多功能, 就像你在航 班上只能帶一個行李箱一樣, 這樣做的問題你的類不會有理想的內聚性, 將會有太多的理由來對它進行修改。 最小化需要修改一個類的次數時很重要的, 因為如果一個類擁有太多的功能, 一旦你修改它的一小部分, 將會很難弄清楚會對程式碼庫中的其它模組造成什麼影響。
不好的:
class UserSettings { constructor(user) { this.user = user; } changeSettings(settings) { if (this.verifyCredentials()) { // ... } } verifyCredentials() { // ... } }
好的:
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 說過, “軟體實體 (類, 模組, 函式等) 應該為擴充套件開放, 但是為修改關閉。” 這 是什麼意思呢? 這個原則基本上說明了你應該允許使用者新增功能而不必修改現有的程式碼。
不好的:
class AjaxAdapter extends Adapter { constructor() { super(); this.name = 'ajaxAdapter'; } } class NodeAdapter extends Adapter { constructor() { super(); this.name = 'nodeAdapter'; } } class HttpRequester { constructor(adapter) { this.adapter = adapter; } fetch(url) { if (this.adapter.name === 'ajaxAdapter') { return makeAjaxCall(url).then((response) => { // transform response and return }); } else if (this.adapter.name === 'httpNodeAdapter') { return makeHttpCall(url).then((response) => { // transform response and return }); } } } function makeAjaxCall(url) { // request and return promise } function makeHttpCall(url) { // request and return promise }
好的:
class AjaxAdapter extends Adapter { constructor() { super(); this.name = 'ajaxAdapter'; } request(url) { // request and return promise } } class NodeAdapter extends Adapter { constructor() { super(); this.name = 'nodeAdapter'; } request(url) { // request and return promise } } class HttpRequester { constructor(adapter) { this.adapter = adapter; } fetch(url) { return this.adapter.request(url).then((response) => { // transform response and return }); } }
里氏代換原則 (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 { 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); const area = rectangle.getArea(); // BAD: Will return 25 for Square. Should be 20. rectangle.render(area); }); } const rectangles = [new Rectangle(), new Rectangle(), new Square()]; renderLargeRectangles(rectangles);
好的:
class Shape { setColor(color) { // ... } render(area) { // ... } } class Rectangle extends Shape { constructor(width, height) { super(); this.width = width; this.height = height; } getArea() { return this.width * this.height; } } class Square extends Shape { constructor(length) { super(); this.length = length; } getArea() { return this.length * this.length; } } function renderLargeShapes(shapes) { shapes.forEach((shape) => { const area = shape.getArea(); shape.render(area); }); } const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)]; renderLargeShapes(shapes);
介面隔離原則 (ISP)
JavaScript 沒有介面, 所以這個原則不想其它語言那麼嚴格。 不過, 對於 JavaScript 這種缺少類 型的語言來說, 它依然是重要並且有意義的。
介面隔離原則說的是 “客戶端不應該強制依賴他們不需要的介面。” 在 JavaScript 這種弱型別語言中, 介面是隱式的契約。
在 JavaScript 中能比較好的說明這個原則的是一個類需要一個巨大的配置物件。 不需要客戶端去設定大 量的選項是有益的, 因為多數情況下他們不需要全部的設定。 讓它們變成可選的有助於防止出現一個“胖接 口”。
不好的:
class DOMTraverser { constructor(settings) { this.settings = settings; this.setup(); } setup() { this.rootNode = this.settings.rootNode; this.animationModule.setup(); } traverse() { // ... } } const $ = new DOMTraverser({ rootNode: document.getElementsByTagName('body'), animationModule() {} // Most of the time, we won't need to animate when traversing. // ... });
好的:
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() { // ... } } const $ = new DOMTraverser({ rootNode: document.getElementsByTagName('body'), options: { animationModule() {} } });
依賴反轉原則 (DIP)
這個原則闡述了兩個重要的事情:
- 高階模組不應該依賴於低階模組, 兩者都應該依賴與抽象;
- 抽象不應當依賴於具體實現, 具體實現應當依賴於抽象。
這個一開始會很難理解, 但是如果你使用過 Angular.js , 你應該已經看到過通過依賴注入來實現的這 個原則, 雖然他們不是相同的概念, 依賴反轉原則讓高階模組遠離低階模組的細節和建立, 可以通過 DI 來實現。 這樣做的巨大益處是降低模組間的耦合。 耦合是一個非常糟糕的開發模式, 因為會導致程式碼難於 重構。
如上所述, JavaScript 沒有介面, 所以被依賴的抽象是隱式契約。 也就是說, 一個物件/類的方法和 屬性直接暴露給另外一個物件/類。 在下面的例子中, 任何一個 Request 模組的隱式契約 InventoryTracker
將有一個 requestItems
方法。
不好的:
class InventoryRequester { constructor() { this.REQ_METHODS = ['HTTP']; } requestItem(item) { // ... } } class InventoryTracker { constructor(items) { this.items = items; // 不好的: 我們已經建立了一個對請求的具體實現的依賴, 我們只有一個 requestItems 方法依 // 賴一個請求方法 'request' this.requester = new InventoryRequester(); } requestItems() { this.items.forEach((item) => { this.requester.requestItem(item); }); } } const 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) { // ... } } // 通過外部建立依賴項並將它們注入, 我們可以輕鬆的用一個嶄新的使用 WebSockets 的請求模組進行 // 替換。 const inventoryTracker = new InventoryTracker(['apples', 'bananas'], new InventoryRequesterV2()); inventoryTracker.requestItems();
測試
測試比釋出更加重要。 如果你沒有測試或者測試不夠充分, 每次釋出時你就不能確認沒有破壞任何事情。 測試的量由你的團隊決定, 但是擁有 100% 的覆蓋率(包括所有的語句和分支)是你為什麼能達到高度自信 和內心的平靜。 這意味著需要一個額外的偉大的測試框架, 也需要一個好的 覆蓋率工具 。
沒有理由不寫測試。 這裡有 大量的優秀的 JS 測試框架 , 選一個適合你的團隊的即可。 當為團隊選擇了測試框架之後, 接下來的目標是為生產的每一個新的功能/模 塊編寫測試。 如果你傾向於測試驅動開發(TDD), 那就太棒了, 但是要點是確認你在上線任何功能或者重 構一個現有功能之前, 達到了需要的目標覆蓋率。
一個測試一個概念
不好的:
const assert = require('assert'); describe('MakeMomentJSGreatAgain', () => { it('handles date boundaries', () => { 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', () => { it('handles 30-day months', () => { const date = new MakeMomentJSGreatAgain('1/1/2015'); date.addDays(30); date.shouldEqual('1/31/2015'); }); it('handles leap year', () => { const date = new MakeMomentJSGreatAgain('2/1/2016'); date.addDays(28); assert.equal('02/29/2016', date); }); it('handles non-leap year', () => { const date = new MakeMomentJSGreatAgain('2/1/2015'); date.addDays(28); assert.equal('03/01/2015', date); }); });
併發
使用 Promises, 不要使用回撥
回撥不夠簡潔, 因為他們會產生過多的巢狀。 在 ES2015/ES6 中, Promises 已經是內建的全域性型別 了,使用它們吧!
不好的:
require('request').get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', (requestErr, response) => { if (requestErr) { console.error(requestErr); } else { require('fs').writeFile('article.html', response.body, (writeErr) => { if (writeErr) { console.error(writeErr); } else { console.log('File written'); } }); } });
好的:
require('request-promise').get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin') .then((response) => { return require('fs-promise').writeFile('article.html', response); }) .then(() => { console.log('File written'); }) .catch((err) => { console.error(err); });
Async/Await 比 Promises 更加簡潔
Promises 是回撥的一個非常簡潔的替代品, 但是 ES2017/ES8 帶來的 async 和 await 提供了一個 更加簡潔的解決方案。 你需要的只是一個字首為 async
關鍵字的函式, 接下來就可以不需要 then
函式鏈來編寫邏輯了。 如果你能使用 ES2017/ES8 的高階功能的話, 今天就使用它吧!
不好的:
require('request-promise').get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin') .then((response) => { return require('fs-promise').writeFile('article.html', response); }) .then(() => { console.log('File written'); }) .catch((err) => { console.error(err); });
好的:
async function getCleanCodeArticle() { try { const response = await require('request-promise').get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin'); await require('fs-promise').writeFile('article.html', response); console.log('File written'); } catch(err) { console.error(err); } }
錯誤處理
丟擲錯誤是一件好事情! 他們意味著當你的程式有錯時執行時可以成功確認, 並且通過停止執行當前堆疊 上的函式來讓你知道, 結束當前程序(在 Node 中), 在控制檯中用一個堆疊跟蹤提示你。
不要忽略捕捉到的錯誤
對捕捉到的錯誤不做任何處理不能給你修復錯誤或者響應錯誤的能力。 向控制檯記錄錯誤 ( console.log
) 也不怎麼好, 因為往往會丟失在海量的控制檯輸出中。 如果你把任意一段程式碼用 try/catch
包裝那就 意味著你想到這裡可能會錯, 因此你應該有個修復計劃, 或者當錯誤發生時有一個程式碼路徑。
不好的:
try { functionThatMightThrow(); } catch (error) { console.log(error); }
好的:
try { functionThatMightThrow(); } catch (error) { // One option (more noisy than console.log): console.error(error); // Another option: notifyUserOfError(error); // Another option: reportErrorToService(error); // OR do all three! }
不要忽略被拒絕的 promise
與你不應忽略來自 try/catch
的錯誤的原因相同。
不好的:
getdata() .then((data) => { functionThatMightThrow(data); }) .catch((error) => { console.log(error); });
好的:
getdata() .then((data) => { functionThatMightThrow(data); }) .catch((error) => { // One option (more noisy than console.log): console.error(error); // Another option: notifyUserOfError(error); // Another option: reportErrorToService(error); // OR do all three! });
格式化
格式化是主觀的。 就像其它規則一樣, 沒有必須讓你遵守的硬性規則。 重點是不要因為格式去爭論, 這 裡有 大量的工具 來自動格式化, 使用其中的一個即可! 因 為做為工程師去爭論格式化就是在浪費時間和金錢。
針對自動格式化工具不能涵蓋的問題(縮排、 製表符還是空格、 雙引號還是單引號等), 這裡有一些指南。
使用一致的大小寫
JavaScript 是無型別的, 所以大小寫告訴你關於你的變數、 函式等的很多事情。 這些規則是主觀的, 所以你的團隊可以選擇他們想要的。 重點是, 不管你們選擇了什麼, 要保持一致。
不好的:
const DAYS_IN_WEEK = 7; const daysInMonth = 30; const songs = ['Back In Black', 'Stairway to Heaven', 'Hey Jude']; const Artists = ['ACDC', 'Led Zeppelin', 'The Beatles']; function eraseDatabase() {} function restore_database() {} class animal {} class Alpaca {}
好的:
const DAYS_IN_WEEK = 7; const DAYS_IN_MONTH = 30; const songs = ['Back In Black', 'Stairway to Heaven', 'Hey Jude']; const 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'); } lookupManager() { return db.lookup(this.employee, 'manager'); } getPeerReviews() { const peers = this.lookupPeers(); // ... } perfReview() { this.getPeerReviews(); this.getManagerReview(); this.getSelfReview(); } getManagerReview() { const manager = this.lookupManager(); } getSelfReview() { // ... } } const review = new PerformanceReview(user); review.perfReview();
好的:
class PerformanceReview { constructor(employee) { this.employee = employee; } perfReview() { this.getPeerReviews(); this.getManagerReview(); this.getSelfReview(); } getPeerReviews() { const peers = this.lookupPeers(); // ... } lookupPeers() { return db.lookup(this.employee, 'peers'); } getManagerReview() { const manager = this.lookupManager(); } lookupManager() { return db.lookup(this.employee, 'manager'); } getSelfReview() { // ... } } const review = new PerformanceReview(employee); review.perfReview();
註釋
僅僅對包含複雜業務邏輯的東西進行註釋
註釋是程式碼的辯解, 不是要求。 多數情況下, 好的程式碼就是文件。
不好的:
function hashIt(data) { // The hash let hash = 0; // Length of string const length = data.length; // Loop through every character in data for (let i = 0; i < length; i++) { // Get character code. const char = data.charCodeAt(i); // Make the hash hash = ((hash << 5) - hash) + char; // Convert to 32-bit integer hash &= hash; } }
好的:
function hashIt(data) { let hash = 0; const length = data.length; for (let i = 0; i < length; i++) { const char = data.charCodeAt(i); hash = ((hash << 5) - hash) + char; // Convert to 32-bit integer 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 //////////////////////////////////////////////////////////////////////////////// $scope.model = { menu: 'foo', nav: 'bar' }; //////////////////////////////////////////////////////////////////////////////// // Action setup //////////////////////////////////////////////////////////////////////////////// const actions = function() { // ... };
好的:
$scope.model = { menu: 'foo', nav: 'bar' }; const actions = function() { // ... };
Translation
This is also available in other languages:
- Brazilian Portuguese : fesnt/clean-code-javascript
- Chinese :
- German : marcbruederlin/clean-code-javascript
- Korean : qkraudghgh/clean-code-javascript-ko
- Russian :
- Vietnamese : hienvd/clean-code-javascript/