歡迎關注我的公衆號睿Talk
,獲取我最新的文章:
一、前言
最近在做一些項目重構的工作,看了不少髒亂差的代碼,身心疲憊。本文將討論如何編寫整潔的代碼,不求高效運行,只求可讀性強,便於維護。
二、爲什麼要寫簡潔的代碼
作爲一個合格的程序員,寫出簡潔的代碼是基本的職業素養。相信絕大部分的程序員都不會故意寫噁心代碼的,無論是對自己或者對別人都沒有任何好處。那麼,是什麼阻礙我們寫出優秀代碼呢?有下面這麼幾種可能性:
- 時間緊任務重,沒那麼多時間考慮代碼設計
- 偷懶圖方便,不過腦子機械式的寫代碼
- 注意力只集中在功能的實現,不考慮後期的維護成本
- 知識儲備不夠,不知道怎樣寫出優雅的代碼
出來混遲早要還的,無論是上述哪種原因,混亂代碼一旦被寫出來,代碼作者肯定是要爲其買單的,只是買單的方式會各有不同。可能是後期維護的時候邊改邊抽自己,也可能是別人改代碼的時候邊改邊罵你傻x。
那麼,代碼寫好了會有什麼好處呢?起碼有以下幾方面:
- 後期維護更高效,無論是改 bug 還是新增功能,無論是自己改還是別人改
- 後期更少的加班
- 思考如何編寫整潔代碼的過程中,技術能力會隨之提高
- 寫出優雅的代碼,會更有成就感,更熱愛自己的工作
- 別人看到這麼優雅的代碼,會讚不絕口,個人影響力會放大
既然有這麼多好處,那到底怎麼評判代碼寫得好不好呢?是自己覺得好就是好嗎?顯然不是。代碼寫得是否整潔是客觀的,是 code review 的人或後期維護的人覺得好纔是真的好。所以加強 code review 也是倒逼寫出優秀代碼的一種方式。
個人認爲代碼的優秀程度分以下幾個層次:
- 程序能正常運行
- 異常情況有應對方法
- 簡單明瞭,易於後期維護
- 團隊成員間能高效協作
- 能高性能運行
層次越高,難度越大,挑戰越大。作爲一個有追求的程序員,我們應該不斷突破自己的邊界,追求卓越,更上一層樓。
三、代碼設計原則
寫出優雅整潔的代碼,就要遵循特定的設計原則。理解透徹這些原則後,還要結合具體的項目落地,不斷的練習和重構。下面總結出的一些通用原則供參考。
KISS(keep it simple, stupid)
業務邏輯要直截了當,不要引入各種依賴,多層次調用。以 React 爲例,常見的錯誤是將props
在state
裏存一份,計算的時候再從state
中取。這樣帶來的問題是要時刻監聽props
的變化,然後再同步到state
中。這完全是多此一舉,直接用props
進行計算即可。
// bad
componentWillReceiveProps(nextProps) {
this.setState({num: nextProps.num});
}
render() {
return(
<div>{this.state.num * 2}</div>
);
}
/***************************/
// good
render() {
return(
<div>{this.props.num * 2}</div>
);
}
DRY (don’t repeat yourself)
不用做機械式的複製粘貼,要鍛鍊自己抽象的能力,儘量將通用的邏輯抽象出來,方便日後重用。
Open/Closed (open to extension but closed to modification)
代碼要對擴展開放,對修改封閉。儘量做到在不修改原有代碼的基礎上,增加新的功能。React 的容器組件和展示組件分離用的就是這種思想。
function Comp() {
...
}
class ContainerComp extends PureComponent {
async componentDidMount() {
const data = await fetchData();
this.setState({data});
}
render() {
return (<Comp data={this.state.data}/>);
}
}
從架構層面說,微內核架構也是遵循這一設計原則。它能保證核心模塊不變的情況下,通過插件機制無限爲系統賦予新的能力。我們常用的 Webpack 就是一個很好的例子,它通過引入 loader 和 plugin 的機制,極大的擴展了其文件處理的能力。
Composition > Inheritance
首先說一下類這個概念。本質上來說,定義類就是爲了代碼的複用,對於需要同時創建多個對象實例的情況下,這種設計模式是非常有效的。比如說連接池中就需要同時存在多個連接對象,方便資源複用。而對於前端來說,絕大部分的業務場景都是單例,這種情況下通過定義工具函數或者直接使用對象字面量會更加高效。工具函數儘量使用純函數,使代碼更易於理解,不用考慮副作用。這裏說的僅限於業務代碼的範疇,如果是框架型的項目,場景會複雜得多,類會更有用武之地。
既然類都不需要用了,繼承就更無從談起了。繼承的問題是多級繼承之後,定位問題會非常困難,要一級一級往上找才能找到錯誤出處。而組合就沒有這種問題,因爲多個功能都是平級的,哪裏出問題一眼就能看出來。比較一下下面 2 種風格的代碼:
繼承的寫法:
class Parent extends PureComponent {
componentDidMount() {
this.fetchData(this.url);
}
fetchData(url) {
...
}
render() {
const data = this.calcData();
return (
<div>{data}</data>
);
}
}
class Child extends Parent {
constructor(props) {
super(props);
this.url = 'http://api';
}
calcData() {
...
}
}
組合的寫法:
class Parent extends PureComponent {
componentDidMount() {
this.fetchData(this.props.url);
}
fetchData(url) {
...
}
render() {
const data = this.props.calcData(this.state);
return (
<div>{data}</data>
);
}
}
class Child extends PureComponent {
calcData(state) {
...
}
render() {
<Parent url="http://api" calcData={this.calcData}/>
}
}
哪種更易於理解呢?
Single Responsibility
遵循單一職責的代碼設計得好,將代碼組合起來就會非常的清爽。舉一個註冊的場景,可以劃分爲下面幾個職責:
- UI 展示
- 輸入合法性判斷
- 網絡請求
- 聚合層
僞代碼如下:
// UI.js
export default function UI() {
...
}
// api.js
export function regist(name, email) {
...
}
// validate.js
export function validateName(name) {
...
}
export function validateEmail(email) {
...
}
// Regist.js
export default class Regist extends PureComponent {
...
onSubmit = async () => {
const {name, email} = this.state;
if (validateName(name) && validateEmail(email)) {
const resp = await regist(name, email);
...
}
}
render() {
<UI onSubmit={onSubmit}>
}
}
可以看到聚合層的代碼非常簡潔,哪裏出問題了就到相應的地方改就好了,即使是多人協作,也不容易出問題。
Separation of Concerns
關注點分離原則跟單一職責原則有點類似,但更強調的是系統架構層面的設計。典型的例子就是 MVC 模式,Model、View、Control 三層之間都有明確的職責劃分,做到了高內聚低耦合。
React 的源碼設計也是基於這一原則,分爲ReactElement
, ReactCompositeComponent
和 ReactDomComponent
三層。ReactElement
負責描述頁面的 DOM 結構,也就是著名的 Virtual DOM;ReactCompositeComponent
處理的是組件生命週期、組件 Diff 和更新等邏輯;而ReactDomComponent
是真正進行 DOM 操作的類。三層之間分工明確,緊密協作,共同組成了一個強大的前端框架。
getData: function(opts, callback) {
// 獲取數據後的處理
callback = function(err, data) {
...
};
var reqData = {
...
};
yApi.request(region, "dcos", "TpcDescribePhysvrsList", reqData).then(
function(result) {
if (res.result == 0) {
...
callback(null, server);
...
} else {
self.hideLoading();
tips.error("服務器異常!");
failAttempt();
}
},
function() {
self.hideLoading();
tips.error("服務器異常!");
failAttempt();
}
);
}
Clean Code > Clever Code
提倡簡潔易懂的代碼,而不是晦澀難懂的“聰明”代碼,如下面這種:
let a, b=3, t = (a=2, b<1) ? (console.log('Y'),'yes') : (console.log('N'),'no');
單一代碼文件不超過 200 行
文件一旦超過 200 行,說明邏輯已經有點複雜了,要想辦法抽離出一些純函數工具方法,讓主線邏輯更加清晰。工具方法可以放在另外的文件裏面,減少讀代碼的心理壓力。需要說明的是不是所有的文件都不能超過 200 行,像工具方法這種,都是各自獨立的邏輯,寫多少行都無所謂。需要控制的是緊密關聯的業務代碼。
四、前端代碼如何拆分
上面提到要合理的拆分代碼,那到底怎麼拆呢?對於前端的組件代碼,有下面一些拆分點以供參考:
- UI
- 展現邏輯
- 事件處理
- 業務邏輯
- 網絡請求
- 配置類純JSON對象
- 工具類純函數
需要說明的是展現邏輯和業務邏輯是兩回事,最好不要混在一起寫。比如組件的顯示隱藏是展現邏輯,而數據的校驗就是業務邏輯。
五、總結
本文討論了書寫整潔代碼的必要性和重要性,結合實例列出了一些設計原則,還給出了組件代碼拆分的方式。程序員的職業生涯是一個自我修煉的過程,時刻關注代碼質量,是提高技術水平的重要一環。