[TOC]
# url
http://www.yinwang.org/blog-cn/2015/11/21/programming-philosophy
# 寫優雅的代碼
優雅的代碼的另一個特征是,它的邏輯大體上看起來,是枝丫分明的樹狀結構(tree)。這是因為程序所做的幾乎一切事情,都是信息的傳遞和分支。你可以把代碼看成是一個電路,電流經過導線,分流或者匯合。如果你是這樣思考的,你的代碼里就會比較少出現只有一個分支的if語句,它看起來就會像這個樣子:
~~~
if (...) {
if (...) {
...
} else {
...
}
} else if (...) {
...
} else {
...
}
~~~
注意到了嗎?在我的代碼里面,if語句幾乎總是有兩個分支。它們有可能嵌套,有多層的縮進,而且else分支里面有可能出現少量重復的代碼。然而這樣的結構,邏輯卻非常嚴密和清晰
# 寫模塊化的代碼
有些人吵著鬧著要讓程序“模塊化”,結果他們的做法是把代碼分部到多個文件和目錄里面,然后把這些目錄或者文件叫做“module”。他們甚至把這些目錄分放在不同的VCS repo里面。結果這樣的作法并沒有帶來合作的流暢,而是帶來了許多的麻煩。這是因為他們其實并不理解什么叫做“模塊”,膚淺的把代碼切割開來,分放在不同的位置,其實非但不能達到模塊化的目的,而且制造了不必要的麻煩。
真正的模塊化,并不是文本意義上的,而是邏輯意義上的。一個模塊應該像一個電路芯片,它有定義良好的輸入和輸出。實際上一種很好的模塊化方法早已經存在,它的名字叫做“函數”。每一個函數都有明確的輸入(參數)和輸出(返回值),同一個文件里可以包含多個函數,所以你其實根本不需要把代碼分開在多個文件或者目錄里面,同樣可以完成代碼的模塊化。我可以把代碼全都寫在同一個文件里,卻仍然是非常模塊化的代碼。
想要達到很好的模塊化,你需要做到以下幾點:
* 避免寫太長的函數。如果發現函數太大了,就應該把它拆分成幾個更小的。通常我寫的函數長度都不超過40行。對比一下,一般筆記本電腦屏幕所能容納的代碼行數是50行。我可以一目了然的看見一個40行的函數,而不需要滾屏。只有40行而不是50行的原因是,我的眼球不轉的話,最大的視角只看得到40行代碼。
* 如果我看代碼不轉眼球的話,我就能把整片代碼完整的映射到我的視覺神經里,這樣就算忽然閉上眼睛,我也能看得見這段代碼。我發現閉上眼睛的時候,大腦能夠更加有效地處理代碼,你能想象這段代碼可以變成什么其它的形狀。40行并不是一個很大的限制,因為函數里面比較復雜的部分,往往早就被我提取出去,做成了更小的函數,然后從原來的函數里面調用。
* 制造小的工具函數。如果你仔細觀察代碼,就會發現其實里面有很多的重復。這些常用的代碼,不管它有多短,提取出去做成函數,都可能是會有好處的。有些幫助函數也許就只有兩行,然而它們卻能大大簡化主要函數里面的邏輯。
有些人不喜歡使用小的函數,因為他們想避免函數調用的開銷,結果他們寫出幾百行之大的函數。這是一種過時的觀念。現代的編譯器都能自動的把小的函數內聯(inline)到調用它的地方,所以根本不產生函數調用,也就不會產生任何多余的開銷。
同樣的一些人,也愛使用宏(macro)來代替小函數,這也是一種過時的觀念。在早期的C語言編譯器里,只有宏是靜態“內聯”的,所以他們使用宏,其實是為了達到內聯的目的。然而能否內聯,其實并不是宏與函數的根本區別。宏與函數有著巨大的區別(這個我以后再講),應該盡量避免使用宏。為了內聯而使用宏,其實是濫用了宏,這會引起各種各樣的麻煩,比如使程序難以理解,難以調試,容易出錯等等。
* 每個函數只做一件簡單的事情。有些人喜歡制造一些“通用”的函數,既可以做這個又可以做那個,它的內部依據某些變量和條件,來“選擇”這個函數所要做的事情。比如,你也許寫出這樣的函數
~~~
void foo() {
if (getOS().equals("MacOS")) {
a();
} else {
b();
}
c();
if (getOS().equals("MacOS")) {
d();
} else {
e();
}
}
~~~
寫這個函數的人,根據系統是否為“MacOS”來做不同的事情。你可以看出這個函數里,其實只有c()是兩種系統共有的,而其它的a(), b(), d(), e()都屬于不同的分支。
這種“復用”其實是有害的。如果一個函數可能做兩種事情,它們之間共同點少于它們的不同點,那你最好就寫兩個不同的函數,否則這個函數的邏輯就不會很清晰,容易出現錯誤。其實,上面這個函數可以改寫成兩個函數:
~~~
void fooMacOS() {
a();
c();
d();
}
~~~
和
~~~
void fooOther() {
b();
c();
e();
}
~~~
如果你發現兩件事情大部分內容相同,只有少數不同,多半時候你可以把相同的部分提取出去,做成一個輔助函數。比如,如果你有個函數是這樣:
~~~
void foo() {
a();
b()
c();
if (getOS().equals("MacOS")) {
d();
} else {
e();
}
}
~~~
其中a(),b(),c()都是一樣的,只有d()和e()根據系統有所不同。那么你可以把a(),b(),c()提取出去:
~~~
void preFoo() {
a();
b()
c();
~~~
然后制造兩個函數:
~~~
void fooMacOS() {
preFoo();
d();
}
~~~
和
~~~
void fooOther() {
preFoo();
e();
}
~~~
這樣一來,我們既共享了代碼,又做到了每個函數只做一件簡單的事情。這樣的代碼,邏輯就更加清晰。
* 避免使用全局變量和類成員(class member)來傳遞信息,盡量使用局部變量和參數。有些人寫代碼,經常用類成員來傳遞信息,就像這樣:
~~~
class A {
String x;
void findX() {
...
x = ...;
}
void foo() {
findX();
...
print(x);
}
}
~~~
首先,他使用findX(),把一個值寫入成員x。然后,使用x的值。這樣,x就變成了findX和print之間的數據通道。由于x屬于class A,這樣程序就失去了模塊化的結構。由于這兩個函數依賴于成員x,它們不再有明確的輸入和輸出,而是依賴全局的數據。findX和foo不再能夠離開class A而存在,而且由于類成員還有可能被其他代碼改變,代碼變得難以理解,難以確保正確性。
如果你使用局部變量而不是類成員來傳遞信息,那么這兩個函數就不需要依賴于某一個class,而且更加容易理解,不易出錯:
~~~
String findX() {
...
x = ...;
return x;
}
void foo() {
String x = findX();
print(x);
}
~~~
- 書列表
- laravel框架關鍵技術
- 第一章 組件化開發與composer使用
- 簡介
- composer
- 添加路由組件
- 添加控制器模塊
- 添加模型組件
- 添加視圖組件
- 第三章 laravel框架中常用的php語法
- 匿名函數
- 文件包含
- 魔術方法
- 魔術常量
- 反射
- 后期靜態綁定
- traits
- 第四章 laravel框架中使用的HTTP協議基礎
- HTTP協議
- 數據庫
- 數據遷移
- 第六章 laravel框架中的設計模式
- IOC模式
- php核心技術與最佳實踐
- 第一章面向對象核心
- 反射
- 簡單ORM
- 異常和錯誤
- 接口
- 第二章,面向對象設計
- 設計原則
- 單一職責
- 接口隔離
- 開放封閉
- 替換原則
- 依賴倒置
- linux是怎么寫的呢?
- 第三章 正則表達
- 認識正則
- 第四章 php網絡技術應用
- HTTP協議詳解
- php和http相關函數
- 垃圾信息防御措施
- 現代操作系統
- 引論
- sql必知必會
- 限制結果
- 按位置排序
- where求職順序
- IN操作符
- like
- 函數
- group by
- 組合查詢
- 插入檢索出的數據
- 視圖
- 高性能mysql
- 第一章節 mysql架構與歷史
- mysql架構邏輯圖
- 連接與管理
- 優化與運行
- 讀寫鎖
- 鎖粒度
- 表鎖(table lock)
- 行級鎖(row lock)
- ACID
- 隔離級別
- 死鎖
- 隱式和顯式鎖定
- 多版本并發控制
- Innodb概覽
- 第四章節 Schema與數據類型優化
- 選擇優化的數據類型
- 日期和時間類型
- 標識列
- 特殊類型數據
- 表設計中的缺陷
- 范式
- 計數器表
- 第五章 創建高性能索引
- 索引基礎
- 索引類型
- 索引的優點
- 高性能索引策略
- 選擇合適的索引列順序
- 聚簇索引
- 順序的主鍵什么時候會造成更壞的后果
- 覆蓋索引
- 使用索引掃描來做排序
- 壓縮索引
- 冗余和重復索引
- 索引和鎖
- 支持多種過濾條件
- 什么是范圍條件
- 優化排序
- 維護索引和表
- 表損壞
- 減少索引和數據的碎片
- 第六章 查詢性能優化
- 掃描的行數和訪問類型
- 重構查詢方式
- 查詢執行的基礎
- 重構-改善既有代碼設計
- 第一章-重構
- 什么是重構
- 第一個案列
- 重構第一步
- 王垠博客
- 多態取代價格相關邏輯