原文鏈接
適用於TypeScript的Clean Code(簡潔代碼)概念
啓發來自於clean-code-javascript
介紹
軟件工程原理,來自於Robert C.Martin's的《Clean Code》一書,適用於Typescript.clean-code-typescript不是一個風格指南,它是一個在TypeScript中生成可讀的、可重用的和可重構的軟件的指南。
並非每個原則都需要我們嚴格的遵循,原則越少越容易得到大家的認同。這裏僅僅有一些準則,但這些準則是Clean Code作者多年經驗的總結。
我們的軟件工程僅僅只有50多年的歷史,並且我們還在不斷的學習。當軟件架構與架構一樣古老時,說不定我們還會有更難遵循的規則。但是現在,現在,讓這些指南作爲評估您和您的團隊生成的TypeScript代碼質量的試金石。
還有一件事:瞭解這些規範並不會讓你立即就成爲一位更好的開發人員,按照這個規範工作也並不會意味着你再未來多年內,不會犯錯。每一段代碼一開始都是一個初稿,就像黏土被塑造成最終形狀前一樣。最後,我們和我們的同行一起審查,然後鑿掉不完美的地方。不要因爲需要改進初稿而退縮,一起打敗代碼吧!
變量
使用具有意義的變量名稱
已這種方式提供名稱,能夠讓讀者很輕鬆的知道這個變量提供了什麼。
Bad
function between<T>(a1: T, a2: T, a3: T): boolean {
return a2 <= a1 && a1 <= a3;
}
Good
function between<T>(value: T, left: T, right: T): boolean {
return left <= value && value <= right;
}
使用可讀的變量名稱
如果這個變量名稱不能發音,你將會像個白癡一樣和別人討論這個變量。
Bad
type DtaRcrd102 = {
genymdhms: Data;
modymdhms: Data;
pszqint: number;
}
Good
type Customer = {
generationTimestamp: Date;
modificationTimestamp: Date;
recordId: number;
}
對用相同類型的變量使用相同的詞彙
Bad
function getUserInfo(): User;
function getUserDetails(): User;
function getUserData(): User;
Good
function getUser(): User;
使用可搜索的名稱
我們會閱讀比我們寫的代碼更多的代碼。我們編寫的代碼是可讀的、可搜索的,這一點很重要。對有意義的變量不進行命名最終將會導致我們的程序更難理解。確保你的變量名稱是可搜索的。像TSLint這樣的工具可以幫助識別未命名的常量。
Bad
// What the heck is 86400000 for?
setTimeout(restart, 86400000);
Good
// Declare them as capitalized named constants.
const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000;
setTimeout(restart, MILLISECONDS_IN_A_DAY);
使用帶有解釋性的變量
Bad
declare const users: Map<string, User>;
for (const keyValue of users) {
// iterate through users map
}
Good
declare const users: Map<string, User>;
for (const [id, user] of users) {
// iterate through users map
}
避免心理映射
顯式永遠優於隱式。清晰表達纔是王道!
Bad
const u = getUser();
const s = getSubscription();
const t = charge(u, s);
Good
const user = getUser();
const subscription = getSubscription();
const transaction = charge(user, subscription);
不要添加不需要的上下文
如果你的類/類型/對象的名稱已經告訴了你一些信息,不要在變量中再重複這些信息。
Bad
type Car = {
carMake: string;
carModel: string;
carColor: string;
}
function print(car: Car): void {
console.log(`${car.carMake} ${car.carModel} (${car.carColor})`);
}
Good
type Car = {
make: string;
model: string;
color: string;
}
function print(car: Car): void {
console.log(`${car.make} ${car.model} (${car.color})`);
}
使用默認參數而不是判斷條件
默認參數通常比短路更清晰。
Bad
function loadPages(count?: number) {
const loadCount = count !== undefined ? count : 10;
// ...
}
Good
function loadPages(count: number = 10) {
// ...
}
函數
函數參數(2個或者更少)
限制函數的參數數量這一點非常的重要,更少的參數意味着更加容易測試。超過3個的參數會使你必須爲多種可能編寫多個單獨的變量來進行測試,這是一件非常痛苦的事情。
只有一個或兩個參數是一個理想情況,如果可能的話,我們儘量避免3個參數。應該整合除此之外的任何東西。通常,如果你的函數有兩個以上的參數,那麼你的函數可能試圖做過多的事情了。如果不是,大多數情況下,你可以通過更高級別的對象來作爲參數。
如果你發現你需要大象的參數,請嘗試使用對象。
爲了明確函數所期望的屬性,你能夠使用destructuring語句。這有遊一些優點:
- 當有人查看函數簽名的時候,他會立刻知道正在使用的屬性
- destructuring還會克隆傳遞給函數的參數對象的指定原始值。這有助於防止副作用。注意:解構對象中的object和array不會被克隆
- TypeScript會警告您未使用的屬性,如果沒有destructuring,這將是不可能的。
Bad
function createMenu(title: string, body: string, buttonText: string, cancellable: boolean) {
// ...
}
createMenu('Foo', 'Bar', 'Baz', true);
Good
function createMenu(options: { title: string, body: string, buttonText: string, cancellable: boolean }) {
// ...
}
createMenu({
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
});
你可以通過type aliases來進一步的提高可讀性
type MenuOptions = { title: string, body: string, buttonText: string, cancellable: boolean };
function createMenu(options: MenuOptions) {
// ...
}
createMenu({
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
});
函數應該只做一件事
這是迄今爲止軟件工程中最重要的規則。當一個函數做更多的事情時,他們更難編寫,測試和推理。當你的函數只執行一個操作時,你的函數將更容易重構,並且你的代碼也更加清晰。
Bad
function emailClients(clients: Client) {
clients.forEach((client) => {
const clientRecord = database.lookup(client);
if (clientRecord.isActive()) {
email(client);
}
});
}
Good
function emailClients(clients: Client) {
clients.filter(isActiveClient).forEach(email);
}
function isActiveClient(client: Client) {
const clientRecord = database.lookup(client);
return clientRecord.isActive();
}
函數名稱應該說明它們的作用
Bad
function addToDate(date: Date, month: number): Date {
// ...
}
const date = new Date();
// It's hard to tell from the function name what is added
addToDate(date, 1);
Good
function addMonthToDate(date: Date, month: number): Date {
// ...
}
const date = new Date();
addMonthToDate(date, 1);
函數應該只是一個抽象級別
當你有多個抽象級別時,你的函數通常做得太多了.拆分功能可以實現可重用性和更輕鬆的測試。
Bad
function parseCode(code: string) {
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
const REGEXES = [ /* ... */ ];
function parseCode(code: string) {
const tokens = tokenize(code);
const syntaxTree = parse(tokens);
syntaxTree.forEach((node) => {
// parse...
});
}
function tokenize(code: string): Token[] {
const statements = code.split(' ');
const tokens: Token[] = [];
REGEXES.forEach((regex) => {
statements.forEach((statement) => {
tokens.push( /* ... */ );
});
});
return tokens;
}
function parse(tokens: Token[]): SyntaxTree {
const syntaxTree: SyntaxTree[] = [];
tokens.forEach((token) => {
syntaxTree.push( /* ... */ );
});
return syntaxTree;
}
刪除重複的代碼
儘量避免重複代碼。重複代碼是一件非常糟糕的事情,因爲它以爲着如果你需要更改某個邏輯的時候,你需要在多個位置來更改內容。
通常你會有重複的代碼,因爲你有兩個或兩個以上略有不同的東西,它們有很多共同之處,但是它們之間的差異迫使你有兩個或多個獨立的函數來執行大部分相同的事情。刪除重複代碼意味着創建一個抽象,只需一個函數/模塊/類就可以處理這組不同的東西。
獲得正確的抽象是至關重要的,這就是爲什麼你應該遵循SOLID原則.糟糕的抽象比重複代碼更加糟糕,所以請更加小心。說完這個,如果你能做出很好的抽象,那就做吧!不要重複自己,否則你會發現自己在想要改變一件事的時候更新多個地方。
Bad
function showDeveloperList(developers: Developer[]) {
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: Manager[]) {
managers.forEach((manager) => {
const expectedSalary = manager.calculateExpectedSalary();
const experience = manager.getExperience();
const portfolio = manager.getMBAProjects();
const data = {
expectedSalary,
experience,
portfolio
};
render(data);
});
}
Good
class Developer {
// ...
getExtraDetails() {
return {
githubLink: this.githubLink,
}
}
}
class Manager {
// ...
getExtraDetails() {
return {
portfolio: this.portfolio,
}
}
}
function showEmployeeList(employee: Developer | Manager) {
employee.forEach((employee) => {
const expectedSalary = employee.calculateExpectedSalary();
const experience = employee.getExperience();
const extra = employee.getExtraDetails();
const data = {
expectedSalary,
experience,
extra,
};
render(data);
});
}
你應該對重複代碼持批評態度。有時候,你需要在重複代碼和引入不必要的抽象帶來的複雜度之間權衡。當來自於不同模塊的兩個實現看起來很相似但又存在於不同的域當中,重複代碼是可以被接受的,並且它優於提取一個公共代碼。在這種情況下,提取公共代碼,將會引入兩個模塊之間的間接依賴關係。
設置默認對象和對象屬性或解構(destructuring)
Bad
type MenuConfig = {title?: string, body?: string, buttonText?: string, cancellable?: boolean};
function createMenu(config: MenuConfig) {
config.title = config.title || 'Foo';
config.body = config.body || 'Bar';
config.buttonText = config.buttonText || 'Baz';
config.cancellable = config.cancellable !== undefined ? config.cancellable : true;
// ...
}
createMenu({body: 'Bar'})
Good
type MenuConfig = { title?: string, body?: string, buttonText?: string, cancellable?: boolean };
function createMenu(config: MenuConfig) {
const menuConfig = Object.assign({
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
}, config);
// ...
}
createMenu({ body: 'Bar' });
另外,你也可以通過解構來提供默認值:
type MenuConfig = { title?: string, body?: string, buttonText?: string, cancellable?: boolean };
function createMenu({ title = 'Foo', body = 'Bar', buttonText = 'Baz', cancellable = true }: MenuConfig) {
// ...
}
createMenu({ body: 'Bar' });
通過顯式傳入undefined或null值來避免任何副作用和意外行爲,你可以告訴TypeScript不允許這樣做。TypeScript選項--strictNullChecks
不要使用標誌作爲函數參數
標誌會告訴你的用戶這個函數不止做一件事,但函數應該只做一件事。如果你的函數按照布爾值來判斷執行不同的代碼,請將你的函數拆分。
Bad
function createFile(name: string, temp: boolean) {
if (temp) {
fs.create(`./temp/${name}`);
} else {
fs.create(name);
}
}
Good
function createTempFile(name: string) {
createFile(`./temp/${name}`);
}
function createFile(name: string) {
fs.create(name);
}
避免副作用(1)
如果一個函數除了取參數並返回一個或多個值以外還執行其他任何操作,則這個函數產生了副作用。這個副作用可能是寫一個文件,修改一些全局變量,或者將所有資金都給了一個陌生人。
現在,你確實需要副作用。就像前面的例子,你可能需要寫入文件。不要有多個函數和類來寫入同一個特定的文件,而是應該提供一個唯一的服務來做這個事情。
Bad
// Global variable referenced by following function.
let name = 'Robert C. Martin';
function toBase64() {
name = btoa(name);
}
toBase64();
// If we had another function that used this name, now it'd be a Base64 value
console.log(name); // expected to print 'Robert C. Martin' but instead 'Um9iZXJ0IEMuIE1hcnRpbg=='
Good
const name = 'Robert C. Martin';
function toBase64(text: string): string {
return btoa(text);
}
const encodedName = toBase64(name);
console.log(name);
避免副作用(2)
在JavaScript中,值、對象、數組是通過傳遞引用來傳遞的。對於對象和數組,如果你的函數直接去改變你的數組,如例子,直接添加物品到當前購物車下,其他使用這個購物車的函數的功能將會受到影響。這可能很好,但也可能很糟糕,讓我們想象一個糟糕的情況:
用戶點擊了“購買”按鈕,按鈕調用了購買函數,該函數將購物車數組發送到服務器。由於網絡連接不好,購買函數必須重試。現在,如果在此期間用戶在網絡請求開始之前意外地點擊了他們實際上不想要的項目上的“添加到購物車”按鈕,該怎麼辦?如果這發生在網絡請求之前,則購買功能將會意外的添加一個項目,因爲addItemToCart
函數通過對購物車的引用,將不需要的項目添加進入了購物車。
一個很好的解決方案是,addItemCart
始終克隆購物車,並對克隆的購物車進行修改並返回。這樣可以確保其他使用購物車的函數不會受到任何更改的影響。
有兩個需要注意的地方:
1.在某些情況下,你可能確實需要修改輸入的對象,但如果你使用本指南進行編程時,你會發現這種情況非常少。大多數情況下都可以重構爲沒有副作用的函數。(參考純函數)
2.克隆大對象對性能上的影響是非常嚴重的。幸運的是,這在實際開發中並不是一個大問題,因爲有很好的庫讓這種編程方法變快,而不用手動克隆以佔用大量的內存。
Bad
function addItemToCart(cart: CartItem[], item: Item): void {
cart.push({ item, date: Date.now() });
};
Good
function addItemToCart(cart: CartItem[], item: Item): CartItem[] {
return [...cart, { item, date: Date.now() }];
};
不要寫全局函數
污染全局是一個非常糟糕的做法,因爲這樣做你可能會和其他庫衝突,並且你的api用戶在生產中獲得異常之前,都不會很好的去處理。讓我們考慮一個例子:如果你想擴展JavaScript的原生Array方法以獲得一個可以顯示兩個數組之間差異的diff方法,該怎麼辦?你可以寫一個新的函數到Array.prototype
中,但是他可能會與另外一個試圖做同樣事情的庫發生衝突。如果那個其他庫只是使用diff來找到數組的第一個和最後一個元素之間的區別怎麼辦?這就是爲什麼使用類並簡單地擴展Array比全局函數更好的原因。
Bad
declare global {
interface Array<T> {
diff(other: T[]): Array<T>;
}
}
if (!Array.prototype.diff) {
Array.prototype.diff = function <T>(other: T[]): T[] {
const hash = new Set(other);
return this.filter(elem => !hash.has(elem));
};
}
good
class MyArray<T> extends Array<T> {
diff(other: T[]): T[] {
const hash = new Set(other);
return this.filter(elem => !hash.has(elem));
};
}
多使用函數式編程而不是命令式編程
儘可能多的使用這種編程方式
Bad
const contributions = [
{
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 < contributions.length; i++) {
totalOutput += contributions[i].linesOfCode;
}
Good
const contributions = [
{
name: 'Uncle Bobby',
linesOfCode: 500
}, {
name: 'Suzie Q',
linesOfCode: 1500
}, {
name: 'Jimmy Gosling',
linesOfCode: 150
}, {
name: 'Gracie Hopper',
linesOfCode: 1000
}
];
const totalOutput = contributions
.reduce((totalLines, output) => totalLines + output.linesOfCode, 0);
封裝條件語句
Bad
if (subscription.isTrial || account.balance > 0) {
// ...
}
Good
function canActivateService(subscription: Subscription, account: Account) {
return subscription.isTrial || account.balance > 0
}
if (canActivateService(subscription, account)) {
// ...
}
避免負條件
Bad
function isEmailNotUsed(email: string): boolean {
// ...
}
if (isEmailNotUsed(email)) {
// ...
}
Good
function isEmailUsed(email): boolean {
// ...
}
if (!isEmailUsed(node)) {
// ...
}