測試程式碼質量的唯一方式:別人看你程式碼時說 f * k 的次數。
程式碼質量與其整潔度成正比。乾淨的程式碼,既在質量上較為可靠,也為後期維護、升級奠定了良好基礎。
本文並不是程式碼風格指南,而是關於程式碼的可讀性
、複用性
、擴充套件性
探討。
變數
用有意義且常用的單詞命名變數
Bad:
1 2 |
const yyyymmdstr = moment().format('YYYY/MM/DD'); |
Good:
1 2 |
const currentDate = moment().format('YYYY/MM/DD'); |
保持統一
可能同一個專案對於獲取使用者資訊,會有三個不一樣的命名。應該保持統一,如果你不知道該如何取名,可以去 codelf 搜尋,看別人是怎麼取名的。
Bad:
1 2 3 4 |
getUserInfo(); getClientData(); getCustomerRecord(); |
Good:
1 2 |
getUser() |
每個常量都該命名
可以用 buddy.js 或者 ESLint 檢測程式碼中未命名的常量。
Bad:
1 2 3 |
// 三個月之後你還能知道 86400000 是什麼嗎? setTimeout(blastOff, 86400000); |
Good:
1 2 3 4 |
const MILLISECOND_IN_A_DAY = 86400000; setTimeout(blastOff, MILLISECOND_IN_A_DAY); |
可描述
通過一個變數生成了一個新變數,也需要為這個新變數命名,也就是說每個變數當你看到他第一眼你就知道他是幹什麼的。
Bad:
1 2 3 4 |
const ADDRESS = 'One Infinite Loop, Cupertino 95014'; const CITY_ZIP_CODE_REGEX = /^[^,\]+[,\s]+(.+?)s*(d{5})?$/; saveCityZipCode(ADDRESS.match(CITY_ZIP_CODE_REGEX)[1], ADDRESS.match(CITY_ZIP_CODE_REGEX)[2]); |
Good:
1 2 3 4 5 |
const ADDRESS = 'One Infinite Loop, Cupertino 95014'; const CITY_ZIP_CODE_REGEX = /^[^,\]+[,\s]+(.+?)s*(d{5})?$/; const [, city, zipCode] = ADDRESS.match(CITY_ZIP_CODE_REGEX) || []; saveCityZipCode(city, zipCode); |
直接了當
Bad:
1 2 3 4 5 6 7 8 9 10 11 |
const locations = ['Austin', 'New York', 'San Francisco']; locations.forEach((l) => { doStuff(); doSomeOtherStuff(); // ... // ... // ... // 需要看其他程式碼才能確定 'l' 是幹什麼的。 dispatch(l); }); |
Good:
1 2 3 4 5 6 7 8 9 10 |
const locations = ['Austin', 'New York', 'San Francisco']; locations.forEach((location) => { doStuff(); doSomeOtherStuff(); // ... // ... // ... dispatch(location); }); |
避免無意義的字首
如果建立了一個物件 car,就沒有必要把它的顏色命名為 carColor。
Bad:
1 2 3 4 5 6 7 8 9 10 |
const car = { carMake: 'Honda', carModel: 'Accord', carColor: 'Blue' }; function paintCar(car) { car.carColor = 'Red'; } |
Good:
1 2 3 4 5 6 7 8 9 10 |
const car = { make: 'Honda', model: 'Accord', color: 'Blue' }; function paintCar(car) { car.color = 'Red'; } |
使用預設值
Bad:
1 2 3 4 5 |
function createMicrobrewery(name) { const breweryName = name || 'Hipster Brew Co.'; // ... } |
Good:
1 2 3 4 |
function createMicrobrewery(name = 'Hipster Brew Co.') { // ... } |
函式
引數越少越好
如果引數超過兩個,使用 ES2015/ES6
的解構語法,不用考慮引數的順序。
Bad:
1 2 3 4 |
function createMenu(title, body, buttonText, cancellable) { // ... } |
Good:
1 2 3 4 5 6 7 8 9 10 11 |
function createMenu({ title, body, buttonText, cancellable }) { // ... } createMenu({ title: 'Foo', body: 'Bar', buttonText: 'Baz', cancellable: true }); |
只做一件事情
這是一條在軟體工程領域流傳久遠的規則。嚴格遵守這條規則會讓你的程式碼可讀性更好,也更容易重構。如果違反這個規則,那麼程式碼會很難被測試或者重用。
Bad:
1 2 3 4 5 6 7 8 9 |
function emailClients(clients) { clients.forEach((client) => { const clientRecord = database.lookup(client); if (clientRecord.isActive()) { email(client); } }); } |
Good:
1 2 3 4 5 6 7 8 9 10 |
function emailActiveClients(clients) { clients .filter(isActiveClient) .forEach(email); } function isActiveClient(client) { const clientRecord = database.lookup(client); return clientRecord.isActive(); } |
顧名思義
看函式名就應該知道它是幹啥的。
Bad:
1 2 3 4 5 6 7 8 9 |
function addToDate(date, month) { // ... } const date = new Date(); // 很難知道是把什麼加到日期中 addToDate(date, 1); |
Good:
1 2 3 4 5 6 7 |
function addMonthToDate(month, date) { // ... } const date = new Date(); addMonthToDate(1, date); |
只需要一層抽象層
如果函式巢狀過多會導致很難複用以及測試。
Bad:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
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... }); } |
Good:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
function parseBetterJSAlternative(code) { const tokens = tokenize(code); const ast = lexer(tokens); 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; } |
刪除重複程式碼
很多時候雖然是同一個功能,但由於一兩個不同點,讓你不得不寫兩個幾乎相同的函式。
要想優化重複程式碼需要有較強的抽象能力,錯誤的抽象還不如重複程式碼。所以在抽象過程中必須要遵循 SOLID
原則(SOLID
是什麼?稍後會詳細介紹)。
Bad:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
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); }); } |
Good:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
function showEmployeeList(employees) { employees.forEach(employee => { const expectedSalary = employee.calculateExpectedSalary(); const experience = employee.getExperience(); const data = { expectedSalary, experience, }; switch(employee.type) { case 'develop': data.githubLink = employee.getGithubLink(); break case 'manager': data.portfolio = employee.getMBAProjects(); break } render(data); }) } |
物件設定預設屬性
Bad:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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); |
Good:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
const menuConfig = { title: 'Order', // 'body' key 缺失 buttonText: 'Send', cancellable: true }; function createMenu(config) { config = Object.assign({ title: 'Foo', body: 'Bar', buttonText: 'Baz', cancellable: true }, config); // config 就變成了: {title: "Order", body: "Bar", buttonText: "Send", cancellable: true} // ... } createMenu(menuConfig); |
不要傳 flag 引數
通過 flag 的 true 或 false,來判斷執行邏輯,違反了一個函式幹一件事的原則。
Bad:
1 2 3 4 5 6 7 8 |
function createFile(name, temp) { if (temp) { fs.create(`./temp/${name}`); } else { fs.create(name); } } |
Good:
1 2 3 4 5 6 7 |
function createFile(name) { fs.create(name); } function createFileTemplate(name) { createFile(`./temp/${name}`) } |
避免副作用(第一部分)
函式接收一個值返回一個新值,除此之外的行為我們都稱之為副作用,比如修改全域性變數、對檔案進行 IO 操作等。
當函式確實需要副作用時,比如對檔案進行 IO 操作時,請不要用多個函式/類進行檔案操作,有且僅用一個函式/類來處理。也就是說副作用需要在唯一的地方處理。
副作用的三大天坑:隨意修改可變資料型別、隨意分享沒有資料結構的狀態、沒有在統一地方處理副作用。
Bad:
1 2 3 4 5 6 7 8 9 10 11 12 |
// 全域性變數被一個函式引用 // 現在這個變數從字串變成了陣列,如果有其他的函式引用,會發生無法預見的錯誤。 var name = 'Ryan McDermott'; function splitIntoFirstAndLastName() { name = name.split(' '); } splitIntoFirstAndLastName(); console.log(name); // ['Ryan', 'McDermott']; |
Good:
1 2 3 4 5 6 7 8 9 10 |
var name = 'Ryan McDermott'; var newName = splitIntoFirstAndLastName(name) function splitIntoFirstAndLastName(name) { return name.split(' '); } console.log(name); // 'Ryan McDermott'; console.log(newName); // ['Ryan', 'McDermott']; |
避免副作用(第二部分)
在 JavaScript 中,基本型別通過賦值傳遞,物件和陣列通過引用傳遞。以引用傳遞為例:
假如我們寫一個購物車,通過 addItemToCart()
方法新增商品到購物車,修改 購物車陣列
。此時呼叫 purchase()
方法購買,由於引用傳遞,獲取的 購物車陣列
正好是最新的資料。
看起來沒問題對不對?
如果當使用者點選購買時,網路出現故障, purchase()
方法一直在重複呼叫,與此同時使用者又新增了新的商品,這時網路又恢復了。那麼 purchase()
方法獲取到 購物車陣列
就是錯誤的。
為了避免這種問題,我們需要在每次新增商品時,克隆 購物車陣列
並返回新的陣列。
Bad:
1 2 3 4 |
const addItemToCart = (cart, item) => { cart.push({ item, date: Date.now() }); }; |
Good:
1 2 3 4 |
const addItemToCart = (cart, item) => { return [...cart, {item, date: Date.now()}] }; |
不要寫全域性方法
在 JavaScript 中,永遠不要汙染全域性,會在生產環境中產生難以預料的 bug。舉個例子,比如你在 Array.prototype
上新增一個 diff
方法來判斷兩個陣列的不同。而你同事也打算做類似的事情,不過他的 diff
方法是用來判斷兩個陣列首位元素的不同。很明顯你們方法會產生衝突,遇到這類問題我們可以用 ES2015/ES6 的語法來對 Array
進行擴充套件。
Bad:
1 2 3 4 5 |
Array.prototype.diff = function diff(comparisonArray) { const hash = new Set(comparisonArray); return this.filter(elem => !hash.has(elem)); }; |
Good:
1 2 3 4 5 6 7 |
class SuperArray extends Array { diff(comparisonArray) { const hash = new Set(comparisonArray); return this.filter(elem => !hash.has(elem)); } } |
比起命令式我更喜歡函數語言程式設計
函式式變程式設計可以讓程式碼的邏輯更清晰更優雅,方便測試。
Bad:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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; } |
Good:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
const programmerOutput = [ { name: 'Uncle Bobby', linesOfCode: 500 }, { name: 'Suzie Q', linesOfCode: 1500 }, { name: 'Jimmy Gosling', linesOfCode: 150 }, { name: 'Gracie Hopper', linesOfCode: 1000 } ]; let totalOutput = programmerOutput .map(output => output.linesOfCode) .reduce((totalLines, lines) => totalLines + lines, 0) |
封裝條件語句
Bad:
1 2 3 4 |
if (fsm.state === 'fetching' && isEmpty(listNode)) { // ... } |
Good:
1 2 3 4 5 6 7 8 |
function shouldShowSpinner(fsm, listNode) { return fsm.state === 'fetching' && isEmpty(listNode); } if (shouldShowSpinner(fsmInstance, listNodeInstance)) { // ... } |
儘量別用“非”條件句
Bad:
1 2 3 4 5 6 7 8 |
function isDOMNodeNotPresent(node) { // ... } if (!isDOMNodeNotPresent(node)) { // ... } |
Good:
1 2 3 4 5 6 7 8 |
function isDOMNodePresent(node) { // ... } if (isDOMNodePresent(node)) { // ... } |
避免使用條件語句
Q:不用條件語句寫程式碼是不可能的。
A:絕大多數場景可以用多型替代。
Q:用多型可行,但為什麼就不能用條件語句了呢?
A:為了讓程式碼更簡潔易讀,如果你的函式中出現了條件判斷,那麼說明你的函式不止幹了一件事情,違反了函式單一原則。
Bad:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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(); } } } |
Good:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
class Airplane { // ... } // 波音777 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(); } } |
避免型別檢查(第一部分)
JavaScript 是無型別的,意味著你可以傳任意型別引數,這種自由度很容易讓人困擾,不自覺的就會去檢查型別。仔細想想是你真的需要檢查型別還是你的 API 設計有問題?
Bad:
1 2 3 4 5 6 7 8 |
function travelToTexas(vehicle) { if (vehicle instanceof Bicycle) { vehicle.pedal(this.currentLocation, new Location('texas')); } else if (vehicle instanceof Car) { vehicle.drive(this.currentLocation, new Location('texas')); } } |
Good:
1 2 3 4 |
function travelToTexas(vehicle) { vehicle.move(this.currentLocation, new Location('texas')); } |
避免型別檢查(第二部分)
如果你需要做靜態型別檢查,比如字串、整數等,推薦使用 TypeScript,不然你的程式碼會變得又臭又長。
Bad:
1 2 3 4 5 6 7 8 9 |
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'); } |
Good:
1 2 3 4 |
function combine(val1, val2) { return val1 + val2; } |
不要過度優化
現代瀏覽器已經在底層做了很多優化,過去的很多優化方案都是無效的,會浪費你的時間,想知道現代瀏覽器優化了哪些內容,請點這裡。
Bad:
1 2 3 4 5 |
// 在老的瀏覽器中,由於 `list.length` 沒有做快取,每次迭代都會去計算,造成不必要開銷。 // 現代瀏覽器已對此做了優化。 for (let i = 0, len = list.length; i < len; i++) { // ... } |
Good:
1 2 3 |
for (let i = 0; i < list.length; i++) { // ... } |
刪除棄用程式碼
很多時候有些程式碼已經沒有用了,但擔心以後會用,捨不得刪。
如果你忘了這件事,這些程式碼就永遠存在那裡了。
放心刪吧,你可以在程式碼庫歷史版本中找他它。
Bad:
1 2 3 4 5 6 7 8 9 10 11 |
function oldRequestModule(url) { // ... } function newRequestModule(url) { // ... } const req = newRequestModule; inventoryTracker('apples', req, 'www.inventory-awesome.io'); |
Good:
1 2 3 4 5 6 7 |
function newRequestModule(url) { // ... } const req = newRequestModule; inventoryTracker('apples', req, 'www.inventory-awesome.io'); |
物件和資料結構
用 get
、set
方法運算元據
這樣做可以帶來很多好處,比如在運算元據時打日誌,方便跟蹤錯誤;在 set
的時候很容易對資料進行校驗…
Bad:
1 2 3 4 5 6 7 8 9 10 11 12 |
function makeBankAccount() { // ... return { balance: 0, // ... }; } const account = makeBankAccount(); account.balance = 100; |
Good:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
function makeBankAccount() { // 私有變數 let balance = 0; function getBalance() { return balance; } function setBalance(amount) { // ... 在更新 balance 前,對 amount 進行校驗 balance = amount; } return { // ... getBalance, setBalance, }; } const account = makeBankAccount(); account.setBalance(100); |
使用私有變數
可以用閉包來建立私有變數
Bad:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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 |
Good:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function makeEmployee(name) { return { getName() { return name; }, }; } const employee = makeEmployee('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 |
類
使用 class
在 ES2015/ES6 之前,沒有類的語法,只能用建構函式的方式模擬類,可讀性非常差。
Bad:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
// 動物 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() {}; |
Good:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// 動物 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() {}; } |
鏈式呼叫
這種模式相當有用,可以在很多庫中發現它的身影,比如 jQuery、Lodash 等。它讓你的程式碼簡潔優雅。實現起來也非常簡單,在類的方法最後返回 this 可以了。
Bad:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
class Car { constructor(make, model, color) { this.make = make; this.model = model; this.color = color; } 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('Ford','F-150','red'); car.setColor('pink'); car.save(); |
Good:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
class Car { constructor(make, model, color) { this.make = make; this.model = model; this.color = color; } setMake(make) { this.make = make; return this; } setModel(model) { this.model = model; return this; } setColor(color) { this.color = color; return this; } save() { console.log(this.make, this.model, this.color); return this; } } const car = new Car('Ford','F-150','red') .setColor('pink'); .save(); |
不要濫用繼承
很多時候繼承被濫用,導致可讀性很差,要搞清楚兩個類之間的關係,繼承表達的一個屬於關係,而不是包含關係,比如 Human->Animal vs. User->UserDetails
Bad:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class Employee { constructor(name, email) { this.name = name; this.email = email; } // ... } // TaxData(稅收資訊)並不是屬於 Employee(僱員),而是包含關係。 class EmployeeTaxData extends Employee { constructor(ssn, salary) { super(); this.ssn = ssn; this.salary = salary; } // ... } |
Good:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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
SOLID 是幾個單詞首字母組合而來,分別表示 單一功能原則
、開閉原則
、里氏替換原則
、介面隔離原則
以及依賴反轉原則
。
單一功能原則
如果一個類乾的事情太多太雜,會導致後期很難維護。我們應該釐清職責,各司其職減少相互之間依賴。
Bad:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class UserSettings { constructor(user) { this.user = user; } changeSettings(settings) { if (this.verifyCredentials()) { // ... } } verifyCredentials() { // ... } } |
Good:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class UserAuth { constructor(user) { this.user = user; } verifyCredentials() { // ... } } class UserSetting { constructor(user) { this.user = user; this.auth = new UserAuth(this.user); } changeSettings(settings) { if (this.auth.verifyCredentials()) { // ... } } } } |
開閉原則
“開”指的就是類、模組、函式都應該具有可擴充套件性,“閉”指的是它們不應該被修改。也就是說你可以新增功能但不能去修改原始碼。
Bad:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
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) => { // 傳遞 response 並 return }); } else if (this.adapter.name === 'httpNodeAdapter') { return makeHttpCall(url).then((response) => { // 傳遞 response 並 return }); } } } function makeAjaxCall(url) { // 處理 request 並 return promise } function makeHttpCall(url) { // 處理 request 並 return promise } |
Good:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
class AjaxAdapter extends Adapter { constructor() { super(); this.name = 'ajaxAdapter'; } request(url) { // 處理 request 並 return promise } } class NodeAdapter extends Adapter { constructor() { super(); this.name = 'nodeAdapter'; } request(url) { // 處理 request 並 return promise } } class HttpRequester { constructor(adapter) { this.adapter = adapter; } fetch(url) { return this.adapter.request(url).then((response) => { // 傳遞 response 並 return }); } } |
里氏替換原則
名字很唬人,其實道理很簡單,就是子類不要去重寫父類的方法。
Bad:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
// 長方形 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(); rectangle.render(area); }); } const rectangles = [new Rectangle(), new Rectangle(), new Square()]; renderLargeRectangles(rectangles); |
Good:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
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); |
介面隔離原則
JavaScript 幾乎沒有介面的概念,所以這條原則很少被使用。官方定義是“客戶端不應該依賴它不需要的介面”,也就是介面最小化,把介面解耦。
Bad:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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. // ... }); |
Good:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
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() {} } }); |
依賴反轉原則
說就兩點:
- 高層次模組不能依賴低層次模組,它們依賴於抽象介面。
- 抽象介面不能依賴具體實現,具體實現依賴抽象介面。
總結下來就兩個字,解耦。
Bad:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
// 庫存查詢 class InventoryRequester { constructor() { this.REQ_METHODS = ['HTTP']; } requestItem(item) { // ... } } // 庫存跟蹤 class InventoryTracker { constructor(items) { this.items = items; // 這裡依賴一個特殊的請求類,其實我們只是需要一個請求方法。 this.requester = new InventoryRequester(); } requestItems() { this.items.forEach((item) => { this.requester.requestItem(item); }); } } const inventoryTracker = new InventoryTracker(['apples', 'bananas']); inventoryTracker.requestItems(); |
Good:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
// 庫存跟蹤 class InventoryTracker { constructor(items, requester) { this.items = items; this.requester = requester; } requestItems() { this.items.forEach((item) => { this.requester.requestItem(item); }); } } // HTTP 請求 class InventoryRequesterHTTP { constructor() { this.REQ_METHODS = ['HTTP']; } requestItem(item) { // ... } } // webSocket 請求 class InventoryRequesterWS { constructor() { this.REQ_METHODS = ['WS']; } requestItem(item) { // ... } } // 通過依賴注入的方式將請求模組解耦,這樣我們就可以很輕易的替換成 webSocket 請求。 const inventoryTracker = new InventoryTracker(['apples', 'bananas'], new InventoryRequesterHTTP()); inventoryTracker.requestItems(); |
測試
隨著專案變得越來越龐大,時間線拉長,有的老程式碼可能半年都沒碰過,如果此時上線,你有信心這部分程式碼能正常工作嗎?測試的覆蓋率和你的信心是成正比的。
PS: 如果你發現你的程式碼很難被測試,那麼你應該優化你的程式碼了。
單一化
Bad:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import assert from 'assert'; describe('MakeMomentJSGreatAgain', () => { it('handles date boundaries', () => { let date; date = new MakeMomentJSGreatAgain('1/1/2015'); date.addDays(30); assert.equal('1/31/2015', date); 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); }); }); |
Good:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import assert from 'assert'; describe('MakeMomentJSGreatAgain', () => { it('handles 30-day months', () => { const date = new MakeMomentJSGreatAgain('1/1/2015'); date.addDays(30); assert.equal('1/31/2015', date); }); 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 替代回撥吧。
Bad:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import { get } from 'request'; import { writeFile } from 'fs'; get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', (requestErr, response) => { if (requestErr) { console.error(requestErr); } else { writeFile('article.html', response.body, (writeErr) => { if (writeErr) { console.error(writeErr); } else { console.log('File written'); } }); } }); |
Good:
1 2 3 4 5 6 7 8 9 10 11 |
get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin') .then((response) => { return writeFile('article.html', response); }) .then(() => { console.log('File written'); }) .catch((err) => { console.error(err); }); |
Async/Await 比起 Promises 更簡潔
Bad:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import { get } from 'request-promise'; import { writeFile } from 'fs-promise'; get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin') .then((response) => { return writeFile('article.html', response); }) .then(() => { console.log('File written'); }) .catch((err) => { console.error(err); }); |
Good:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import { get } from 'request-promise'; import { writeFile } from 'fs-promise'; async function getCleanCodeArticle() { try { const response = await get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin'); await writeFile('article.html', response); console.log('File written'); } catch(err) { console.error(err); } } |
錯誤處理
不要忽略拋異常
Bad:
1 2 3 4 5 6 |
try { functionThatMightThrow(); } catch (error) { console.log(error); } |
Good:
1 2 3 4 5 6 7 8 9 10 11 12 |
try { functionThatMightThrow(); } catch (error) { // 這一種選擇,比起 console.log 更直觀 console.error(error); // 也可以在介面上提醒使用者 notifyUserOfError(error); // 也可以把異常傳回伺服器 reportErrorToService(error); // 其他的自定義方法 } |
不要忘了在 Promises 拋異常
Bad:
1 2 3 4 5 6 7 8 |
getdata() .then((data) => { functionThatMightThrow(data); }) .catch((error) => { console.log(error); }); |
Good:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
getdata() .then((data) => { functionThatMightThrow(data); }) .catch((error) => { // 這一種選擇,比起 console.log 更直觀 console.error(error); // 也可以在介面上提醒使用者 notifyUserOfError(error); // 也可以把異常傳回伺服器 reportErrorToService(error); // 其他的自定義方法 }); |
程式碼風格
程式碼風格是主觀的,爭論哪種好哪種不好是在浪費生命。市面上有很多自動處理程式碼風格的工具,選一個喜歡就行了,我們來討論幾個非自動處理的部分。
常量大寫
Bad:
1 2 3 4 5 6 7 8 9 10 11 12 |
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 {} |
Good:
1 2 3 4 5 6 7 8 9 10 11 12 |
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 {} |
先宣告後呼叫
就像我們看報紙文章一樣,從上到下看,所以為了方便閱讀把函式宣告寫在函式呼叫前面。
Bad:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
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(employee); review.perfReview(); |
Good:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
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(); |
註釋
只有業務邏輯需要註釋
程式碼註釋不是越多越好。
Bad:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
function hashIt(data) { // 這是初始值 let hash = 0; // 陣列的長度 const length = data.length; // 迴圈陣列 for (let i = 0; i < length; i++) { // 獲取字元程式碼 const char = data.charCodeAt(i); // 修改 hash hash = ((hash << 5) - hash) + char; // 轉換為32位整數 hash &= hash; } } |
Good:
1 2 3 4 5 6 7 8 9 10 11 12 |
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; // 轉換為32位整數 hash &= hash; } } |
刪掉註釋的程式碼
git 存在的意義就是儲存你的舊程式碼,所以註釋的程式碼趕緊刪掉吧。
Bad:
1 2 3 4 5 |
doStuff(); // doOtherStuff(); // doSomeMoreStuff(); // doSoMuchStuff(); |
Good:
1 2 |
doStuff(); |
不要記日記
記住你有 git!,git log
可以幫你幹這事。
Bad:
1 2 3 4 5 6 7 8 9 10 |
/** * 2016-12-20: 刪除了 xxx * 2016-10-01: 改進了 xxx * 2016-02-03: 刪除了第12行的型別檢查 * 2015-03-14: 增加了一個合併的方法 */ function combine(a, b) { return a + b; } |
Good:
1 2 3 4 |
function combine(a, b) { return a + b; } |
註釋不需要高亮
註釋高亮,並不能起到提示的作用,反而會干擾你閱讀程式碼。
Bad:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
//////////////////////////////////////////////////////////////////////////////// // Scope Model Instantiation //////////////////////////////////////////////////////////////////////////////// $scope.model = { menu: 'foo', nav: 'bar' }; //////////////////////////////////////////////////////////////////////////////// // Action setup //////////////////////////////////////////////////////////////////////////////// const actions = function() { // ... }; |
Good:
1 2 3 4 5 6 7 8 9 |
$scope.model = { menu: 'foo', nav: 'bar' }; const actions = function() { // ... }; |
感謝閱讀!
ref
翻譯自 ryanmcdermott 的 《clean-code-javascript》,本文對原文進行了一些修改。