就算不讀 Clean Code,你也可以把程式碼寫好

前言

這是一篇有點特別的文章,為了寫這一篇我花了約近一年多的時間,而這一篇主要是分享我當助教這段時間所看到的各種 Code 寫法,當然不會原封不動地貼上學生寫的範例,而是會修正過範例,避免學生看到後有不好的影響,因為這一篇僅僅只是分享「你還可以怎麼寫會更好」。

為什麼要寫這篇文章?

這一篇並不會像 Clean Code 那樣教你怎麼寫、怎麼命名,而是主要分享自己的當助教期間所看到的各種 Code 寫法,然後說明為什麼這樣寫不好,當然這一篇也會盡可能避免說明到一些太過複雜的概念,主軸比較偏向於「你還可以怎麼寫會更好」。

當然每個人都有自己的 Code Style,所以分享這一篇並不代表你就一定要依照我的作法去做,而是讓你知道「為什麼我會這樣子建議」以及「為什麼不要這樣寫」。

當然由於我是 Vue 開發者,所以你可能會看到一些關於 Vue 的建議。

如果沒有什麼太大問題的話,就準備好好認識自己的程式碼是不是我舉例的其中一種吧!

照慣例,一樣要補個迷因圖:

求你了

有意義的函式與變數命名

一開始我們先來看一個我最常見的奇怪狀況:

1
2
3
function xxxx() {
// do something
}

有注意到問題嗎?函式名稱就真的叫做 xxxx,或許你並不會這樣寫甚至許多人也不會這樣寫,但是我在當助教的時候確實看到過這種毫無意義的命名,更不用說變數這樣子命名:

1
2
const bbbb = 0;
const bbbbArray = [];

我絕對不會說上面還是我業界的朋友提供給我的,甚至是下方這種也很常見:

1
2
3
const aaaa = function () {
// do something
}

這種奇怪且毫無意義的命名都是非常常見的。

那麼該怎麼命名會比較好呢?其實就是看裡面的程式碼(邏輯)是在做什麼為主,由於前面內容都是寫「do something」很難看出該怎麼命名,因此這邊我補上一下內容:

1
2
3
4
5
6
7
8
let data = [];

function xxxx() {
axios.get('/products')
.then((res) => {
data = res.data;
})
}

以上面範例來說是一個與遠端進行溝通的 AJAX 函式,雖然我們可以透過閱讀內容略知一二,但以目前函式名稱來講,我們無法知道這函式是在做什麼,而必須閱讀完整程式碼才能知道用途。

因此為了避免這種狀況我們可以透過有意義的命名來讓其他人知道這個函式是在做什麼,舉例來講可以改成這樣:

1
2
3
4
5
6
7
8
let data = [];

function getProducts() {
axios.get('/products')
.then((res) => {
data = res.data;
})
}

那麼這樣子當你看到該函式名稱時,就可以知道這個函式是在做「取得產品」的事情,因此有意義的命名是非常重要的,若你保持著「xxxx」則會讓開發者還要花時間去閱讀函式內做什麼,這樣子並不是一個好習慣。

除了函式命名為什麼要有意義之外,變數命名也是一樣的,例如前面的範例雖然資料是放在 data 中:

1
2
3
4
5
6
7
8
let data = [];

function getProducts() {
axios.get('/products')
.then((res) => {
data = res.data;
})
}

雖然看是沒有什麼問題,但是你依然無法知道 data 是放什麼資料且是什麼資料,因此我們可以這樣命名:

1
2
3
4
5
6
7
8
let products = [];

function getProducts() {
axios.get('/products')
.then((res) => {
products = res.data;
})
}

明確有意義的命名可以讓開發者知道你的變數是預計準備放什麼資料的,而不是讓開發者去通靈猜你到底要放什麼或者執行看看才知道。

變數跟函式命名上基本上原則很簡單,寧可長、好閱讀也不要短而難理解。

避免曖昧的命名

除了上面提到的「有意義的命名」之外,還有一個問題是「曖昧的命名」,這個曖昧的命明我比較常在 Vue 的最終作業中看到:

1
2
3
<template>
<a href="#" @click.prevent="click('點我')">這是一個超連結</a>
</template>

我們可以看到 click 與 Vue 的 click 事件非常曖昧,這種命名不但很曖昧之外也會讓開發者第一眼誤以為你是在使用 Vue 的 click 事件,因此非常的容易讓人混淆。

如果你還真不知道該命名什麼,那麼還不如命名為 handleClick,這樣子就可以讓開發者知道這是一個處理點擊事件的函式:

1
2
3
<template>
<a href="#" @click.prevent="handleClick('點我')">這是一個超連結</a>
</template>

請盡可能避免使用曖昧的命名,你應該不會想要讓開發者看著你的程式碼一直罵 F**K。

題外話,我也曾經看過一種很神奇的寫法,然後問我為什麼每次打開網頁都會壞掉,因為它這樣子寫:

1
2
3
4
const click = () => {
console.log('Click');
return click();
}

這樣子寫的話,每次呼叫 click 函式時,就會無限的呼叫 click 函式,因此網頁就會爆掉,所以在命名上會建議多花個幾秒去思考一下,這樣子才不會出現這種問題。

避免漢語拼音

我曾經看過一位同學的函式跟變數命名都是使用漢語拼音方式:

1
2
3
4
5
6
7
8
9
10
11
const sanminTitle = () => {
console.log('三民區');
}

const llingyaTitle = () => {
console.log('苓雅區');
}

const bigTreeTitle = () => {
console.log('大樹區');
}

這種命名方式不太建議的原因是因為,如果你要將專案交給國外開發者時,國外開發者大概會一臉問號,除此之外大部分的開發者還是會比較傾向使用英文的方式來命名,因此這邊還是會建議使用英文作為命名。

(如果你英文不好,可以請 Google 翻譯幫你翻譯。)

減少使用 var

在 JavaScript 中,我們可以使用 varletconst 來宣告變數,但是實戰開發上我們會盡可能使用 ES6 letconst 來宣告一個變數,其原因主要是出在 var 的作用域是函式(Function),而 letconst 的作用域是區塊(Block)

舉例來講,如果我們使用 var 來宣告一個變數,然後幾百行之後在裡面又重新宣告一次同名的變數,這時候你會發現你的變數值會被覆蓋掉,因為 var 作用域是函式:

1
2
3
4
5
6
7
8
9
10
11
12
13
var a = 1;

// 這邊是一些程式碼
// 這邊是一些程式碼
// 這邊是一些程式碼
// 這邊是一些程式碼
// 這邊是一些程式碼

if (true) {
var a = 2;
}

console.log(a); // 2

而如果我們使用 letconst 來宣告變數,則不會有這樣的問題,因為只要有 {} 都是 letconst 的限制作用域:

1
2
3
4
5
6
7
8
9
10
11
12
13
let a = 1;

// 這邊是一些程式碼
// 這邊是一些程式碼
// 這邊是一些程式碼
// 這邊是一些程式碼
// 這邊是一些程式碼

if (true) {
let a = 2;
}

console.log(a); // 1

因此我們在實戰開發上,盡可能使用 letconst 來宣告變數,避免出現作用域的問題。

儘管現今已經大力提倡使用 ES6 的 letconst,但還是有人會使用 var 來宣告變數,所以這邊才特別提出來,而這也是我批改作業時常見的錯誤。

請不要 varlet 同時使用

這是一個很有趣的事情,如同前面所講的 JavaScript 同時具備 varletconst 三種變數宣告方式,但是我們會盡可能避免使用 varlet 同時使用,這樣將會導致開發者很容易混淆變數作用域,例如:

1
2
3
4
5
6
7
var a = 1;

if (true) {
let a = 2;
}

console.log(a); // 1

當然我也有看過另一種寫法,也就是 let 宣告在 Global Scope,然後在 if 裡面使用 var 宣告變數,例如:

1
2
3
4
5
let a = 1;

if (true) {
var a = 2;
}

雖然以目前的瀏覽器已經有阻擋這樣的寫法(印象以前可以通過。),但如果你嘗試貼入到瀏覽器 Console 內可能會看到以下錯誤(不同瀏覽器可能會有不同的錯誤訊息):

Chrome:Uncaught SyntaxError: Identifier 'a' has already been declared
Firefox:Uncaught SyntaxError: redeclaration of let a

因此為了避免這樣的問題,我們還是會盡可能避免使用 varlet 同時使用,這也是為了避免你被同事請出去喝咖啡。

善加利用 ES6 的解構賦值

這邊先來看一個簡單的範例:

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
const user = {
name: 'Ray',
age: 18,
address: 'Taipei',
email: '[email protected]',
phone: '0912345678',
isDeveloper: true,
isEnabled: true,
category: 'developer',
isMale: true,
isFemale: true,
loginType: 'google',
avatar: 'https://example.com/avatar.png',
createdAt: '2019-01-01 00:00:00',
updatedAt: '2019-01-01 00:00:00',
};

const newUser = {
name: user.name,
age: user.age,
address: user.address,
email: user.email,
phone: user.phone,
isDeveloper: user.isDeveloper,
isEnabled: user.isEnabled,
category: user.category,
isMale: user.isMale,
isFemale: user.isFemale,
loginType: user.loginType,
avatar: user.avatar,
createdAt: user.createdAt,
updatedAt: user.updatedAt,
};

我們可以看到基本上我們只是將 user 物件的屬性值,一一複製到 newUser 物件裡面而已,我們可以看到 newUser 的寫法非常冗長,而且如果 user 物件的屬性值有變動,那麼我們也必須要同步修改 newUser 物件的屬性值,這樣的寫法非常不好維護,因此我們可以利用 ES6 的解構賦值來簡化這個寫法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const user = {
name: 'Ray',
age: 18,
address: 'Taipei',
email: '[email protected]',
phone: '0912345678',
isDeveloper: true,
isEnabled: true,
category: 'developer',
isMale: true,
isFemale: true,
loginType: 'google',
avatar: 'https://example.com/avatar.png',
createdAt: '2019-01-01 00:00:00',
updatedAt: '2019-01-01 00:00:00',
};

const newUser = {
...user,
};

善加利用 ES6 解構賦值的方式,可以讓我們的程式碼更簡潔,也更容易維護。

但如果今天是只有部分屬性值需要複製,例如我們只想要複製 nameageaddressemailphone 這五個屬性值,那麼我們可以這樣寫:

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
const user = {
name: 'Ray',
age: 18,
address: 'Taipei',
email: '[email protected]',
phone: '0912345678',
isDeveloper: true,
isEnabled: true,
category: 'developer',
isMale: true,
isFemale: true,
loginType: 'google',
avatar: 'https://example.com/avatar.png',
createdAt: '2019-01-01 00:00:00',
updatedAt: '2019-01-01 00:00:00',
};

const data = ['name', 'age', 'address', 'email', 'phone'];

const newUser = {
...data.reduce((acc, key) => {
acc[key] = user[key];
return acc;
}, {}),
};

這樣子我們就只需要專心在 data 新增我們想要的屬性值,而不需要去修改 newUser 物件的屬性值。

當然使用 reduce 這種做法是比較進階一點的,所以這邊你也可以改用 forEach 來寫:

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
const user = {
name: 'Ray',
age: 18,
address: 'Taipei',
email: '[email protected]',
phone: '0912345678',
isDeveloper: true,
isEnabled: true,
category: 'developer',
isMale: true,
isFemale: true,
loginType: 'google',
avatar: 'https://example.com/avatar.png',
createdAt: '2019-01-01 00:00:00',
updatedAt: '2019-01-01 00:00:00',
};

const data = ['name', 'age', 'address', 'email', 'phone'];

// forEach
const newUser = {};

data.forEach((key) => {
newUser[key] = user[key];
});

另一種我常見的奇怪寫法就是 import,下方是一個 Font Awesome icon 的引入

1
2
3
4
5
6
import { faUser } from '@fortawesome/free-solid-svg-icons';
import { faUserSecret } from '@fortawesome/free-solid-svg-icons';
import { faUserTie } from '@fortawesome/free-solid-svg-icons';
import { faUserCircle } from '@fortawesome/free-solid-svg-icons';
import { faUserAstronaut } from '@fortawesome/free-solid-svg-icons';
import { faUserNinja } from '@fortawesome/free-solid-svg-icons';

相同的引入來源,但我們寫了六次,這樣的寫法非常不好維護,因此我們可以這樣寫:

1
2
3
4
5
6
7
8
import {
faUser,
faUserSecret,
faUserTie,
faUserCircle,
faUserAstronaut,
faUserNinja,
} from '@fortawesome/free-solid-svg-icons';

這樣子我們就可以一次引入多個 icon,而不需要一個一個引入。

被忽略的大括號

在 JavaScript 中,很常可以看到許多開發者忽略 if 語句的大括號,例如:

1
if (true) console.log('Hello World');

這種寫法本身並沒有錯誤,這是因為 JavaScript 引擎會自動幫我們加上大括號,當只有一行時還沒有什麼,但當你有多個判斷時就會顯得稍微有點混亂,尤其是搭配用 else if 時,例如:

1
2
3
4
5
6
const myName = 'Ray';

if (myName === 'Ray') console.log('Hello Ray');
else if (myName === 'John') console.log('Hello John');
else if (myName === 'Mary') console.log('Hello Mary');
else console.log('Hello World');

應該稍微可以感覺到混亂了,對於一位新手工程師來講,這一段必須先花個幾秒的時間去理解,因此如果有多個判斷時,會建議你還是加上大括號,例如:

1
2
3
4
5
6
7
8
9
10
11
const myName = 'Ray';

if (myName === 'Ray') {
console.log('Hello Ray');
} else if (myName === 'John') {
console.log('Hello John');
} else if (myName === 'Mary') {
console.log('Hello Mary');
} else {
console.log('Hello World');
}

儘管程式碼感覺較多,但是在表達上也比較清楚。

(除此之外,你不是在寫 Python,不要這麼簡寫)

當然,提到這件事情並不代表你就不要這樣做,只是因為許多人會過度的去省略大括號,因此實際開發來講,最佳實踐上依然很常見一行解決的狀況。

不要亂寫註解

適當的註解是可以幫助開發者釐清當初的想法,但是如果你的註解太多,那麼你的程式碼就會變得很難閱讀,例如:

1
2
3
4
5
6
7
8
9
10
11
12
// 宣告一個變數叫做 products 並賦予一個空陣列
const products = [];
// 宣告一個函式叫做 getProducts
function getProducts() {
// 使用 axios 來取得產品資料,來源是 https://example.com/products
axios.get('/products')
// 資料取得成功時走這裡
.then((response) => {
// 取得資料後,將資料放入 products 陣列中
products = response.data;
});
}

我們可以看到註解的數量已經快要比程式碼還要多了,過多的註解並不會幫助開發者理解程式碼,反而會讓程式碼變得複雜,因為程式碼本身就是一個註解,如果你的程式碼寫得不好,那麼你的註解也就沒有意義,與其花一大推時間在寫註解,不如花時間在寫程式碼上,思考如何讓程式碼更好閱讀,除非你這一塊的邏輯真的非常複雜。

除了上面這個例子之外,我也曾在 Vue 的專案中看到這樣的註解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { createApp } from 'vue'

createApp({
data() {
return {
products: [],
}
},
methods: {
getProducts() {
axios.get('/products')
.then((response) => {
this.products = response.data;
});
},
},
// mounted
created() {
getProducts();
},
}).mount('#app');

看到那一個 mounted 的註解了嗎?依照程式碼來看我們可以看到 getProducts 是在 created 階段被呼叫,但 created 上方卻有一個 mounted,此時的我會認為這個註解是不是寫錯了,還是有什麼特別意義,那麼為了確認這一條註解意義我可能要去問原本的開發者,如果他還在職那就沒什麼,但如果他離職呢?那麼這個註解就會變得毫無意義,因此我們應該要避免這樣的註解。

還有另一種註解,例如:

1
// import getProducts from './getProducts'; // 記得註解

其實當我看到這一段的時候我完全很茫然的狀態,因為這一段程式碼不是已經註解了嗎?為什麼還要特別寫一個記得註解?因此註解也是要適當的使用,盡可能避免過多、無意義的註解。

除此之外也會建議你盡可能地移除已經沒有作用的註解程式碼,只要確保該程式碼已經毫無價值且意義,那麼就會建議移除,確保程式碼的乾淨。

不要雞婆把程式碼改成一行

我之前有看過一段程式碼是直接各種壓縮到一行,例如:

1
2
3
const products = [];
function getProducts() {axios.get('/products').then((response) => {products = response.data;});}
getProducts();

拜託不要為了追求程式碼的行數然後各種壓縮成一行,這樣子可讀性是非常差的,壓縮這件事情我們可以透過 GulpWebpack 等工具來幫助我們,如果這樣子做,只會讓人家想請你喝咖啡泡茶聊天而已。

不要濫用三元運算子

三元運算子是一個非常好用的技巧,但是如果你過度濫用的話,其實反而會大幅降低可讀性,例如:

1
2
3
4
5
6
7
8
9
const products = [];

const myName = 'Ray';

function fn() {
return myName === 'Ray' ? true : false;
}

const status = (products.length > 0) || fn() ? 'success' : 'error';

因此在實際上來講,三元運算子還是會傾向於簡單的判斷,如果判斷較複雜的話,還是會建議使用 if 來去判斷較好。

片地開花的 console.log

console.log 是我們在開發時,最常拿來 debug 的技巧,但是也請記得當一個專案完成時,盡可能的移除 console.log,許多狀況跟一些規範都會提到 console.log 是開發階段時所使用的語法,因此在正式上線時應移除 console.log,這也是為了避免一些敏感資訊不小心曝露出來。

當然除了避免敏感資訊之外,過多的 console.log 遺留在專案上也會讓人覺得你是不是不太會寫程式,因此也請記得在專案完成時,盡可能的移除 console.log

換行要適當,也不要過度換行

我之前看過一份作業是這樣子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const myName = 'Ray';















console.log(myName);

一個變數跟 console.log 距離超級無敵遠,整份作業只有少少一百多行,打開來卻高達五百多行,其餘都是空白的換行,這樣子其實在閱讀上非常困擾,你會滾輪滾到爆。

再不然就是完全不換行,例如:

1
2
3
4
5
6
const myName = 'Ray';
console.log(myName);
function sayHello() {
console.log('Hello');
}
sayHello();

適當的換行可以讓程式碼更好閱讀,但是過度的換行也不好,因此請記得適當的換行而不要過度換行。

能用迴圈就迴圈寫

這邊提醒一下,接下來後面稍微會比較偏向框架方面的東西哩。

實戰開發時,往往會有許多地方是重複性的,曾經就有看過一位同學在寫程式碼時,每一個 option 都是自己手寫,就這樣子寫了 20 個 option,例如:

1
2
3
4
5
6
7
8
<select>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
...
</select>

耐心十足

如果你是使用框架開發的話,請善加利用框架提供的迴圈功能,以 Vue 來講就是 v-for,例如:

1
2
3
<select>
<option v-for="i in 20" :value="i">{{ i }}</option>
</select>

若是 React 則是:

1
2
3
4
5
<select>
{[...Array(20).keys()].map((i) => (
<option value={i + 1}>{i + 1}</option>
))}
</select>

不管怎麼樣都遠比你手動打 20 個 option 好。

Composition API 不要與 Options API 混用

這個算是我最常在 Vue3 最終作業上遇到的狀況,有些同學會在同一個頁面中混用 Composition API 與 Options API,而這種我稱之為 ComOption API(誤):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
export default {
data () {
return {
}
},
mounted () {
// ...
},
setup() {
return {
// ...
}
}
}
</script>

不要懷疑,這我很常看見有人這樣寫,雖然程式碼可以正常運作,但是強烈的建議不要混用。

那麼為什麼不建議混用呢?

首先這兩者本身生命週期本身就不同了,以 Option API 來講我們實戰開發上常用的是 createdmounted 這兩個生命週期,而 Composition API 則是 onMounted,因此光生命週期就有滿明顯的差異,更不用說是 Composition API 與 Option API 兩者的撰寫風格更是不同,你可以試想一下你接手的專案有兩種寫法你會有何感想。

這邊也附上 Kuro 準備好的 Vue 生命週期 對照表:

Vue 生命週期

React 的話則是 class component 與 function component,這兩者本身也是不同的撰寫風格,因此也是不建議混用。

結語

以上差不多就是我當助教這段時間看到的一些狀況,希望這些狀況跟舉例可以幫助到你的程式碼寫得更好,也希望可以避免你被同事約出去喝咖啡。

當然以上都只是一個參考而已,並不代表你就一定要這樣子寫,因為實際專案來講還是要看團隊的 Code Style,其中裡面的不要忽略大括號就非常看情境,只是許多人會過度的去省略。

除此之外其實上面有許多建議搭配 Lint 就可以解決,只是在使用 Lint 之前你可以先養好一定的開發習慣,這樣子打開 Lint 時就比較不會覺得害怕與可怕。