編寫代碼時清晰至上

好的代碼是清晰的代碼,而不是聰明的代碼

編寫代碼時清晰至上

Photo by David Travis on Unsplash

許多程序員嘗試編寫乾淨,智能的代碼。 但是,有時候,痴迷於智能可能會使代碼庫更難以理解,並且可能會花費大量時間來閱讀和維護它。

如今,在團隊合作中,人們逐漸意識到編寫人工代碼的意義,這意味著您在編寫代碼時應該尊重他人,而不是炫耀自己的智慧。 人們正在嘗試不要使用"乾淨"一詞,因為這意味著即使您不是故意的,代碼也很髒。 丹尼爾·歐文(Daniel Irvine)在他的文章"乾淨代碼,骯髒代碼,人類代碼"中談到了這一點。

我並不是說乾淨是一件壞事。 在處理個人項目時,我會嘗試以一種聰明的方式使代碼庫變得乾淨。 但更重要的是,我使代碼庫更具可讀性和可理解性。

正如鮑伯叔叔在他的《清潔守則》中所說:

"總的來說,程序員是非常聰明的人。 聰明的人有時喜歡通過展示他們的心理雜耍能力來炫耀自己的聰明人。 畢竟,如果您可以可靠地記住r是URL的小寫版本,並且除去了主機和Schema,那麼您顯然必須非常聰明。 聰明的程序員和專業的程序員之間的區別是,專業人士理解清晰為王。 專業人士會盡力而為,並編寫他人可以理解的代碼。"-羅伯特·C·馬丁

最重要的是編寫清晰易懂的代碼。 其他人不僅是其他人,而且還是您,他們將在幾個月內重寫代碼。

在本文中,我並不是在談論人的代碼方面。 相反,我將通過一些示例來重點介紹如何將該原理應用於您的代碼,並最大程度地減少花一些時間來理解它的時間。

注意:為了解釋一些技巧,我將使用JavaScript或TypeScript。

涵蓋的示例:

· 命名

· 註釋

· 條件

· 循環

· 職能

· 測試


命名

在軟件開發中,許多程序員在命名事物時遇到麻煩。 但是我個人認為,關鍵是避免歧義並使用特定的詞語。

例如:

<code>const fetch = async () => {
return await axios.get('/users')
}
const users = await fetch()
.../<code>

在此代碼中,您可以預期將從服務器獲取什麼內容。 但是,如果導出了導出功能並在其他文件中使用該怎麼辦?

<code>export const fetch = async () => {
\treturn await axios.get('/users')
}/<code>

在其他文件中:

<code>import { fetch } from './utils'fetch() 
// fetch ... what?/<code>


相反,您可以更具體地命名:

<code>export const fetchUsers = async () => {
\treturn await axios.get('/users')
}/<code>


我說過你應該避免歧義。 請注意以下通用動詞:

· set

· get

· group

· begin

· validate

· send

另一個例子:

<code>const xxx = validateForm()/<code>


在這段代碼中,您可以理解validateForm是正在驗證表單,但是您期望返回什麼?

但是假設您這樣寫:

<code>const xxx = isFormValid()/<code>


然後非常清楚,該方法將返回true或false。

而且,如果您這樣編寫代碼,則可以假定該方法將返回一個數組或形式錯誤的映射:

<code>const xxx = getFormErrors()/<code>

另一個例子:

<code>const token = getToken()/<code>

如您所見,getToken可能會獲得一個令牌。 但是從什麼呢? 如果它使用異步功能從服務器獲取令牌怎麼辦?

<code>const token = getToken()
// use token for somethingdoSomething(token)/<code>


這可能會在doSomething函數中導致未定義的錯誤,因為在這種情況下,您需要等待getToken完成。

<code>const token = await getToken()
// use token for somethingdoSomething(token)/<code>


它工作正常。 但是getToken在這種情況下不合適,因此您可以將其重命名:

<code>const token = await fetchToken()
// use token for somethingdoSomething(token)/<code>

這樣一來,更清楚的是該方法將從某些服務器或異步設備中獲取令牌。

為了解決這些問題,許多聰明的人提出了一個很好的例子,但是重要的是讓人們以簡單的方式知道它的用途。


註釋

通常,註釋的目的是幫助人們儘可能地瞭解代碼,並且註釋可以使人們更快地理解代碼。 但是您不必總是對代碼發表註釋。 您需要知道毫無價值的註釋和良好的註釋之間的界限。

什麼沒什麼好評論

如果人們可以輕鬆理解代碼的功能,則無需對代碼進行註釋。

例如:

<code>// Find student from lists, with the given id
const student = students.find(s => s.id === id)/<code>


另一個例子:

<code>// Calculate tax based on the income and wealth value and ....
const income = document.getElementById('income').value;
const wealth = document.getElementById('wealth').value;
tax.value = (0.15 * income) + (0.25 * wealth);

// .../<code>

這似乎是解釋它是什麼的很好的評論,但可以進行改進。

<code>function calculateTax(income, wealth) {
\treturn (0.15 * income) + (0.25 * wealth);
}/<code>

應將代碼塊移至函數中,並輸入一個名稱來解釋其功能。 簡潔明瞭的功能名稱和自記錄功能要好於註釋。

註釋什麼

我們介紹了您不應該註釋的代碼類型。 接下來,我們將看到應該註釋的內容。

您需要在以下代碼處添加註釋:

· 有缺陷,例如性能問題

· 可能會導致人們意想不到的行為

· 需要進行總結,以便人們可以輕鬆掌握細節

· 需要解釋為什麼有更好的方法時必須以這種方式編寫

這些是您在編寫代碼時想出的寶貴見解。 如果沒有這些註釋,人們可能會認為存在錯誤,或者應該對代碼進行測試或修復,這可能會浪費時間。 為避免這種情況,您應該解釋為什麼以某種方式編寫代碼。

重要的是要讓自己穿上別人的鞋子。 提前考慮並預測人們可能會陷入的陷阱。

條件

在編寫代碼時,我們必須處理條件。 許多if / else語句使您停止閱讀代碼庫,而陷入困境。 我相信條件語句越少,代碼的可讀性就越高。

通過使用圈複雜度,您可以計算代碼的複雜度。 如果在代碼中使用了大量的if / else,循環或switch語句,則計數將很高。 通常,計數越高,代碼越複雜。

如果您是IntelliJ用戶,則可以在首選項中檢查複雜性:

編寫代碼時清晰至上

選中"功能過於複雜"框。

並且當您在函數中編寫許多if / else語句時,它會警告您:

編寫代碼時清晰至上

注意:在VS Code中,可以使用一些插件,例如CodeMetrics。

關鍵是儘可能減少不必要的條件,並最大程度地減少理解代碼的時間。

以下是一些簡化和使條件可讀的技巧:

· 首先處理正面的案例,而不是負面的案例

· 早點返回

· 使用Array.includes處理多個案例

· 使用可選鏈接處理未定義的檢查

首先處理正面,而不是負面

哪個更適合您閱讀?

<code>if (!debug) { 
// do something
} else {
debugSomething()
}/<code>

要麼

<code>if (debug) { 
debugSomething()
} else {
// do something
}/<code>


在大多數情況下,優先使用正面案例。 但是,如果否定情況是更簡單,更謹慎的情況,則可以這樣編寫:

<code>if (!user) 
throw new Error('Please sign in first')
// do a lot of things here
// ...
/<code>

早點返回

例如:

<code>export const formatDate = (date) => {
let result
if (date) {
const dateObj = new Date(date)

if (isToday(dateObj)) {
result = 'Today'

} else if (isYesterday(dateObj)) {
result = 'Yesterday'

} else if (!isThisYear(dateObj)) {
result = format(dateObj, 'MMMM d, yyyy')

} else {
result = format(dateObj, 'MMMM d')

}

} else {
throw new Error('No date')
}

return result
}/<code>

它工作正常,但是代碼有點長且嵌套。 而且,如果添加了if / else語句,將更難弄清楚右括號在哪裡,並且更難調試代碼。

為了使它看起來更整潔,我們需要做的是:

· 如果沒有日期,則拋出錯誤

· 如果日期是今天,則返回"今天"

· 如果日期是昨天,則返回"昨天"

· 如果日期不在今年,則返回日期和年份

· 如果不符合上述條件,則返回日期和月份和日期

<code>export const formatDate = (date) => {
if (!date) throw new Error('No date') // If no date, throw an error

const dateObj = new Date(date)
if (isToday(dateObj)) return 'Today' // If the date is today, return 'Today'
if (isYesterday(dateObj)) return 'Yesterday' // If the date is yesterday, return 'Yesterday'
if (!isThisYear(dateObj)) return format(dateObj, 'MMMM d, yyyy') // If the date is not in this year, return date with year

return format(dateObj, 'MMMM d') // If no matching the above, return date with month and date
}/<code>

看起來更好。 從函數中多次返回非常適合使代碼可讀。

使用Array.includes處理多個案例

如果您有多個條件,則可以使用Array.includes以避免擴展語句。

例如:

<code>if (kind === 'Persian' || kind === 'Maine' || kind === 'British Shorthair') {
\t// do something ...
}/<code>

考慮到以後可以將其他條件添加到語句中,我們想要重構代碼,如下所示:

<code>const CATS_TYPE = ['Persian', 'Maine', 'British Shorthair']

if (CATS_TYPE.includes(kind)) {
\t// do something ...
}/<code>

具有類型數組,您可以從代碼中單獨提取條件。

使用可選鏈接處理未定義的檢查

可選的鏈接允許您深入訪問嵌套對象,而無需在臨時變量中重複分配結果。 通過使用此選項,可以減少條件檢查中的多次檢查。

注意:如果要在JavaScript中使用可選的鏈接運算符,則需要安裝Babel插件。 在3.7以上的Typescript中,無需任何配置即可使用它。

例如:

<code>if (user && user.addressInfo) {
let zipcode
if (user.addressInfo.zipcode) {
zipcode = user.addressInfo.zipcode
} else {
zipcode = ''
}

// do something
}/<code>

如果要檢查用戶是否存在並避免發生未定義的錯誤,則需要編寫類似於上面示例的條件。

但是通過使用可選的鏈接運算符,代碼將是:

<code>const zipcode = user?.addressInfo?.zipcode || ''/<code>

這樣看起來更好並且更易於維護。 可以訪問內部的嵌套對象並避免發生未定義的錯誤。

您可以在TypeScript遊樂場中使用此炫酷功能

循環

簡化循環使您的代碼更容易理解。

在實際情況下,您可能會在對象中遇到複雜的嵌套循環。 如果您有嵌套對象並且必須在todo3中獲得列表名稱,該怎麼辦:

<code>const todos = [
{
code: 'code',
name: 'name',
list: [
{
name: 'todo name',
},
{
name: 'todo name',
},
],
todo2: [
{
code2: 'code2',
name2: 'name2',
list: [
{
name: 'todo name2',
description: '',
},
{
name: 'todo name2',
description: '',

}
],
todo3: [
{
code3: 'code3',
name3: 'name3',
list: [
{
name: 'todo name3',
description: '',
},
{
name: 'todo name3',
description: '',
}
]
}
]
}
]
},
]/<code>

例如,您可以這樣編寫:

<code>const list = [];
todos.forEach(t => {
t.todo2.forEach(t2 => {
t2.todo3.forEach(t3 => {
t3.list.forEach(l => {
list.push({
todo3Name: l.name
})
})
})
})
})/<code>

您會得到以下列表:

編寫代碼時清晰至上

但這可以使用reduce函數來改進:

<code>const list = todos
.reduce((acc, t) => [...acc, ...t.todo2], [])
.reduce((acc, t2) => [...acc, ...t2.todo3], [])
.reduce((acc, t3) => [...acc, ...t3.list], [])
.map(l => ({ todo3Name: l.name }))/<code>

那也很好。

編寫代碼時清晰至上

為了避免可讀代碼的複雜性,刪除嵌套循環至關重要。

職能

在編寫函數時,請牢記以下提示:

· 使用摘要名稱來說明其操作。

· 為一個目的創建一個功能。

· 較小的函數更易讀。

使用摘要名稱來說明其操作

乍看之下的代碼如下,您可能會停止閱讀並試圖弄清楚它在做什麼:

<code>const tmp = new Set();
const filtered = lists.filter(a => !tmp.has(a.code) && tmp.add(a.code))/<code>

那呢?

<code>const filtered = uniqueByCode(lists)/<code>

您可能會期望有一個函數,通過查看對象來刪除具有重複代碼的對象。

兩者都能很好地工作,並獲得相同的結果。

編寫代碼時清晰至上

但是第二個更具可讀性,可以幫助解釋該功能的作用。

另一個例子:

<code>const person = { score: 25 };

let newScore = person.score
newScore = newScore + newScore
newScore += 7
newScore = Math.max(0, Math.min(100, newScore));
console.log(newScore) // 57/<code>

如果我們為其編寫函數,則代碼將如下所示:

<code>let newScore = person.score
newScore = double(newScore)
newScore = add(newScore, 7)
newScore = boundScore(0, 100, newScore)

console.log(newScore) // 57/<code>

好多了 但個人而言,我喜歡管道運算符的想法,該運算符與將多個函數鏈接在一起以提高函數編程的可讀性一起使用。

如果我們在JavaScript中使用管道,則代碼將如下所示:

<code>const person = { score: 25 };

const newScore = person.score
|> double
|> add(7, ?)
|> boundScore(0, 100, ?);

newScore //=> 57/<code>


為一個目的創建功能

現在我們瞭解了摘要名稱的重要性。 但是,如果您不能為您的功能起一個好名字怎麼辦?

例如:

<code>const updateUser = async (user) => {
try {
await axios.post('/users', user)
await axios.post('/user/profile', user.profile)
const email = new Email()
await email.send(user.email, 'User has been updated successfully')
const logger = new Logger()
logger.notify()
} catch (e) {
console.log(e)
throw new Error(e)
}
}/<code>

您可能想知道它應該是updateUserAndProfile,updateUserAndProfileAndNotify還是其他名稱。 當您陷入困境時,就該將代碼分成較小的部分了,因為人們很難同時理解多條代碼。

當您編寫用於更新用戶的函數時,代碼應如下所示:

<code>const updateUser = async (user) => {
try {
await axios.post('/users', user)
} catch (e) {
// handling error
}
}/<code>
<code>const handleUpdate = async (user, onUpdated) => {
try {
await updateUser(user)

await updateProfile(user.profile)
await onUpdated(user) // email or notify something
} catch (e) {
// handling error
}
}/<code>

這是一個非常簡單的示例,但是實際開發中有很多情況。 要牢記的關鍵思想是退後一步,考慮功能應該做什麼,並考慮所有問題,以便一次只執行一項任務。

較小的函數更易讀

當您出於某個目的編寫較小的函數時,代碼將更具可讀性和可理解性。

例如:

<code>const generateQuery = (params) => {
const query = {}

try {
if (params.email) {
const isValid = isValidEmail(params.email)
if (isValid) {
query.email = params.email
}
}

const defaultMaxAgeLimit = JSON.parse(localStorage.getItem('defaultMaxAgeLimit') || '')
if (params.maxAge) {
if (params.maxAge < 25) {
query.maxAge = params.maxAge
}
} else {
query.maxAge = defaultMaxAgeLimit
}

if (params.limit) {
query.limit = params.limit

}

// do a lot of things here
// ...

} catch(err) {
// error handing
}

return query
}/<code>

該函數輸出如下:

編寫代碼時清晰至上

假設您在一個函數中有大量代碼,這些函數創建查詢來搜索某些數據。 如果電子郵件查詢出了點問題,則您必須瀏覽內部的函數,找到電子郵件的實現並進行修復。 之後,您將必須檢查更改是否會影響該函數中的其他代碼。

通常,人們一次只能考慮兩件事。 代碼表達越大,理解和維護就越困難。

因此,使代碼更小:

<code>const email = (query, params) => {
if (!params.email) return query

const isValid = isValidEmail(params.email)
if (!isValid) throw new Error('Invalid email')

return { ...query, ...{ email: params.email } }
}

const maxAge = (query, params) => {
const obj = { maxAge: '' }
if (!params.maxAge) obj.maxAge = JSON.parse(localStorage.getItem('defaultMaxAgeLimit') || '')
if (params.maxAge && params.maxAge < 25) obj.maxAge = params.maxAge

return { ...query, ...obj }
}

const limit = (query, params) => {
if (!params.limit) return query

return { ...query, ...{ limit: params.limit } }
}


const generateQuery = (params) => {
let query = {}

try {
query = email(query, params)
query = maxAge(query, params)
query = limit(query, params)
} catch(err) {
// error handing
}

return query
}/<code>

將巨型代碼分解為小段可以使代碼更清晰,更易讀。 更重要的是,每個問題都與其餘代碼分開,因此您可以輕鬆地調試和測試它。

如果您還想使其更具通用性和聲明性,則可以這樣編寫:

<code>const generateQuery = (params, callbacks) => {
let query = {}

try {
callbacks.forEach(c => {
query = c(query, params)
})
} catch(err) {
// error handing
}

return query
}

const result = generateQuery({ email: '[email protected]', maxAge: 20 }, [email, maxAge, limit])/<code>

它輸出相同的結果。

編寫代碼時清晰至上

儘管我建議我們希望使代碼更小,更通用,但在重構之前,您將不得不首先考慮為什麼以這種方式編寫此代碼。 也許您的同事出於某種原因寫了它。 即使有很多改進,您也想與編寫它的人交談,並討論它是否好。

這全都與人工密碼有關。 您可以在"再見,乾淨代碼"一文中更詳細地瞭解它。

測試中

我不是在談論TDD,而是在團隊開發中可讀性對測試的重要性。

編寫測試非常重要,因為:

· 在沒有文檔的情況下,您的隊友可以通過閱讀測試說明輕鬆瞭解詳細信息。

· 您的隊友可以理解真正的代碼應該如何工作以及為什麼。

· 您的隊友可以輕鬆添加新功能,而不必擔心會破壞代碼。

· 鼓勵您的隊友添加測試。 (如果測試代碼太大且令人生畏,則可能會破窗!)

這些是我個人在團隊發展中的經驗教訓。 優秀的程序員始終會編寫具有良好可維護性的測試。

這是編寫測試時的一些技巧:

· 用簡單的英語描述測試的目的(最好使用您的母語)。

· 遵循AAA(安排,執行,聲明)模式。

· 使添加測試用例(表驅動的測試模式)變得容易。

用簡單的英語描述測試要做什麼

就像我在上面說的,如果測試是描述性的,則人們可以輕鬆地瞭解它在做什麼。

假設我們有一個類似util的函數:

<code>export const getAnimal = (code) => {
if (code === 1) return 'CATS'
if (code === 2) return 'DOGS'
if (code === 3) return 'RABBITS'

\treturn null
}/<code>

並編寫一個測試:

<code>import { getAnimal } from './util';

describe('getAnimal', () => {
it('passes', () => {

\texpect(getAnimal(1)).toEqual('CATS')
})
})/<code>

這是一個非常簡單的示例,因此您可能可以理解它正在嘗試執行的操作。 但是,如果它變大並弄亂了,您將很難理解它。

沒關係,因為您已經編寫了此功能,並且知道其功能。 但是測試不僅適合您,還適合您的隊友。

讓我們更具描述性:

<code>describe('getAnimal', () => {
it('should get CATS when passing code 1', () => {
\texpect(getAnimal(1)).toEqual('CATS')
})
})/<code>

這看起來有點多餘。 但這不是重點。 通過描述它,您可以使人們知道正確的行為是在代碼為1時獲取CATS。

為了使其在每個上下文中都更清楚,可以使用上下文塊,如下所示:

<code>describe('getAnimal', () => {
context('when passing code 1', () => {
it('should get CATS', () => {
\texpect(getAnimal(1)).toEqual('CATS')
})
})
})/<code>

注意:如果使用Jest,則可以安裝jest-plugin-context。

通過這樣編寫,您可以在每個塊中分隔特定的上下文。

遵循AAA模式

AAA模式允許您將測試分為三個部分:安排,操作和聲明。

在安排部分,您可以在其中設置數據或模擬要在測試中使用的功能。

act部分是調用測試方法並在需要時捕獲輸出值的地方。

assert部分是您對輸出進行聲明的地方。

如果將其應用於上面的示例,代碼將如下所示:

<code>describe('getAnimal', () => {
context('when passing code 1', () => {
beforeEach(() => {
// arrange
// prepare data here
})

it('should get CATS', () => {
// act
const result = getAnimal(1)

// assert
expect(result).toEqual('CATS')
})
})
})/<code>

這是使用酶進行反應測試的另一個示例:

<code>describe('Component', () => {
let wrapper: ReactWrapper;

beforeEach(() => {
// arrange
// mock useEffect function
jest
.spyOn(React, 'useEffect')
.mockImplementation(f => f());
});

it('should render successfully', () => {
// act
wrapper = mount(<component>);

// assert
expect(wrapper).toMatchSnapshot();
});

it('should update the text after clicking', () => {
// act
wrapper = mount(<component>);
wrapper.find('button').simulate('click');

// assert
expect(wrapper.text().includes("text updated!"));
});
});/<code>


一旦習慣了這種模式,就可以更輕鬆地閱讀和理解測試。

在GitHub上,javascript-testing-best-practices是解釋JavaScript測試的好指南。

輕鬆添加測試用例(表驅動測試模式)

在Go測試中,經常使用表驅動測試模式。 它的優點是能夠通過定義每個表條目中的輸入和預期結果來涵蓋許多測試用例。

這是Go中fmt包的示例:

<code>var flagtests = []struct {
\tin string
\tout string
}{
\t{"%a", "[%a]"},
\t{"%-a", "[%-a]"},
\t{"%+a", "[%+a]"},
\t{"%#a", "[%#a]"},
\t{"% a", "[% a]"},
\t{"%0a", "[%0a]"},
\t{"%1.2a", "[%1.2a]"},
\t{"%-1.2a", "[%-1.2a]"},
\t{"%+1.2a", "[%+1.2a]"},
\t{"%-+1.2a", "[%+-1.2a]"},
\t{"%-+1.2abc", "[%+-1.2a]bc"},
\t{"%-1.2abc", "[%-1.2a]bc"},
}
func TestFlagParser(t *testing.T) {
\tvar flagprinter flagPrinter
\tfor _, tt := range flagtests {
\t\tt.Run(tt.in, func(t *testing.T) {
\t\t\ts := Sprintf(tt.in, &flagprinter)
\t\t\tif s != tt.out {
\t\t\t\tt.Errorf("got %q, want %q", s, tt.out)
\t\t\t}
\t\t})
\t}
}/<code>


輸入和預期輸出在flagtests變量中定義。 您要做的就是循環瀏覽,運行測試並檢查結果。

您可以將其應用於JavaScript測試。

例如:

<code>import { getAnimal } from './util';

describe('getAnimal', () => {
context('when passing code 1', () => {
it('should get CATS', () => {
const result = getAnimal(1)
expect(result).toEqual('CATS')

})
})

context('when passing code 2', () => {
it('should get DOGS', () => {
const result = getAnimal(2)
expect(result).toEqual('DOGS')
})
})

context('when passing code 3', () => {
it('should get RABBITS', () => {
const result = getAnimal(3)
expect(result).toEqual('RABBITS')
})
})

context('when no match', () => {
it('should get null', () => {
const result = getAnimal(100)
expect(result).toEqual(null)
})
})
})/<code>

並使其適用:

<code>const cases = [
{
code: 1,
expected: 'CATS',
},
{
code: 2,
expected: 'DOGS',
},
{
code: 3,
expected: 'RABBITS',
},
{
code: 100,
expected: null,
},
]

describe('getAnimal', () => {
cases.forEach(c => {
context(`when passing code ${c.code}`, () => {
it(`should get ${c.expected}`, () => {

const result = getAnimal(c.code)
expect(result).toEqual(c.expected)
})
})
})
})/<code>

測試將通過:

編寫代碼時清晰至上

如果您有很多測試用例,則此技術將很有用。

結論

我通過一些示例介紹瞭如何使代碼易於理解。 記住,乾淨,智能的代碼並不總是更好。 重要的是退後一步並問自己:"這比較乾淨,但是可以理解和閱讀嗎?" 或"其他隊友是否可以維護?" 如果是這樣,你可以去做。

希望本文對您有所幫助。

如果您有任何建議,意見和想法,請告訴我。


(本文翻譯自Manato Kuroda的文章《Clarity Is King When Writing Code》,參考:https://medium.com/better-programming/clarity-is-king-when-writing-code-752b85101484)


分享到:


相關文章: