代碼審查關注什么:數據結構

數據結構是編程的基礎,所以它是計算機科學課程中一直講授的領域之一。然而,令人驚訝的是很容易錯誤使用或者選擇錯誤的數據結構。在本文中,我們將引導你作為代碼審查者關注需要關注的那些事情--我們將通過一系列示例代碼來討論“壞的代碼味道”,這些“味道”可能暗示我們選擇了錯誤的數據結構或者是數據結構被以錯誤的方式使用。

列表(Lists)

這也許是最常用的數據結構。因為是最常見的選擇,它有時被用在了錯誤的地方。

反模式:過多搜索

迭代列表本身當然不是一件壞事情。但是如果要求迭代是一個常用操作(比如像上面代碼這樣通過 ID 查找一個客戶),應該有更好的數據結構可以使用。在這個例子中,因為我們常常需要通過 ID 查找某個特定的項目,也許創建一個 ID-->Customer 的 map 更合適。

注意,在 Java 8 中以及其他支持更多表達式搜索的語言中,這一點可能沒有 for 循環那么明顯,但是問題仍然存在。

反模式:頻繁重新排序

如果使用它們的默認排序列表是很棒的,但是如果作為審查者你發現代碼中對列表重新排序,確認一下使用列表是否合適。在上面的例子中,在第16行 twitterUsers 總是重新排序以后再返回。再次說明,Java 8 使得這個操作看起來很簡單,可能很容易忽略這個信號:

在這種情況下,鑒于 TwitterUser 是獨立的并且看起來你需要一個默認已經排序好的集合,你可能需要類似 TreeSet 這樣的數據結構。

Maps

如果你選擇了正取的 key, map是一種對單個元素的訪問只有 O(1)復雜度的多用途數據結構。

反模式:Map 作為全局常量

map 是一個很好的通用數據結構,以致可以用一個全局的 map 來讓其他類訪問。

在上面的例子中,作者簡單的將 CUSTOMERS 這個 map 作為全局常量。CustomerUpdateService需要添加或者更新 customers 時就直接使用這個 map。這看起來沒什么問題,因為 CustomerUpdateService的職責就是負責添加和更新操作,這些操作最終會修改 map。問題是當其他類,尤其是系統中自其它模塊的類需要訪問數據的時候:

在這里,訂購服務是知道存儲 customers 的數據結構的。事實上,在上面的代碼中,作者已經犯了一個錯誤--他們沒有檢查 customer 是否為 null,所以第12行可能會引發 NullPointerExeption。作為審查者,你應該建議隱藏數據結構并提供合適的訪問方法。那樣的話其他訪問類會更容易理解,并且將管理 map 的復雜工作隱藏到 map 所屬的 CustomerRepository。另外,如果以后你想修改 customers 所使用的數據結構,或者使用分布式緩存或其他的技術,相關的修改都會限制在 CustomerRepository,而不是遍及整個系統。這就是信息隱藏原則。

盡管修改以后的代碼并不短,你獲得了標準化以及集中的核心函數--例如,你知道在獲取一個不存在的 customer 信息時總會返回一個異常。或者你也可以選擇返回一個新的 Optional 類型。

注意這是屬于在代碼審查中就應該發現的一類問題,如果一個全局變量的訪問已經遍及整個系統,需要隱藏它是比較困難的,但是當它第一次被引入時還是很容易實現的。

其他的反模式:迭代和重新排序

和列表一樣,如果在 map 上進行了大量的排序或者迭代操作,你應該建議采用其他可替換的數據結構。

Java 代碼需要特別關心的事情

在 Java 中,map 的行為依賴于 key 和 value 的 equalshashCode 方法的實現。作為審查者,應該檢查 key 和 value 類的這些方法,以確保獲得預期的行為。

Java 8 給 Map 接口添加了一些有用的方法。例如,上面代碼中的第11行使用的 getOrDefault方法可以簡化 CustomerRepository 的代碼。

Sets

一個常常不被充分使用的數據結構,它的優點是不會包含重復的元素。

反模式:有時你真的需要重復元素

我們假設你有一個 user 類,使用了 set 來跟蹤其訪問過的網站。現在有一個新的功能要求返回這些網站中最近訪問的一個。


這段代碼的作者將跟蹤一個用戶訪問網站的 set 從 HashSet 修改為 LinkedHashSet,后者的實現保留了插入順序,所以現在我們的 set 按照訪問的順序跟蹤每一個 URI。

然而這段代碼中有很多信號說明它是錯誤的。首先--由于 set 不是為按照位置訪問設計的,為了獲取最后一個元素必須迭代整個 set(第13-15行),這樣的訪問開銷太大,有時候 list 是一個完美的選擇。其次,由于 sets 中不包含重復的值,如果最后訪問的頁面在之前已經訪問過,那么它在 set 中的位置并不在最后。相反,它會處在第一次被添加的位置。

在這個例子中,list 或者 stack(參考下文)或者就是一個簡單的屬性都可以讓我們更好的獲得最后瀏覽過的頁面。

Java 需要特別注意
因為 set 的一個關鍵操作是 contains,作為審查者你應該檢查 set 所包含的類型的 equals 方法實現。

棧(Stacks)

Stacks是計算機科學課程最喜歡的數據結構之一,但是在現實世界中常常被忽略-在 Java 中,也許是因為 Stack 繼承自 Vector,所以有一點點過時。在這里我不討論具體的細節,只是列出一些關鍵的點:

  • Stacks 支持 LIFO,所以非常適合 push/pop 操作,但是真的不適合迭代操作。
  • Java 在1.6版本以后是使用 Deque來實現 stack。它既可以作為 queue 又可以作為 stack 使用,所有審查者需要檢查 dequene 在代碼中的使用方式是一致的。

隊列(Queues)

計算機科學最喜歡的另一個數據結構。Queues 常常在討論并發相關的話題時出現(確實,Java 中大多數 Queue 的實現都在 java.util.concurrent 中使用),因為它最常見的用法是在線程和模塊之間傳遞數據。

  • 隊列是 FIFO 的數據結構,通常在你想往尾部添加數據或者從頭部移除數據時非常合適。如果你在審查代碼是發現對隊列進行迭代操作(特殊情況下訪問隊列中間的元素),需要確認一下隊列是否是正確的數據結構。
  • 隊列可以是限定大小的,也可以是不限定大小的。不限定大小的隊列可能會一直增長,所以如果審查代碼時發現使用了不限定大小隊列,請注意我們在上一篇文章中討論的性能問題。限定大小的隊列也有它的問題--在審查代碼時,需要關注什么條件下隊列會滿,并且了解隊列滿的情況下系統會做出什么反應。

Java 開發者特別注意

作為審查者,你不僅僅要了解通用的數據結構特性,還需要注意各種實現的優點和弱點,這些知識在 Javadoc 中都有詳細的說明:

如果你使用的是 Java8,記住很多集合類都添加了新的方法。作為審查者,你應該意識到這一點--你可以在一些復雜的代碼中建議使用新的方法。

為什么要選擇正確的數據結構?

我們已經在這篇博客中討論了數據結構--怎樣確定被審查的代碼是否使用了錯誤的數據結構,以及各種數據結構的優缺點的要點,這樣作為審查者不僅僅可以確認數據結構沒有正確使用,而且可以給出更好的替代方案。我們一起來看一下為什么選擇正確的數據結構如此重要。

性能

如果你在計算機科學課程中學習了數據結構,你應該知道選擇數據結構對性能的影響,事實上,我們在這篇博客中甚至用“大O表示法”來強調特定數據結構的某些優點。在代碼中使用正確的數據結構當然會對性能有幫助,但是這不是選擇正確工具的唯一理由。

表述預期的行為

代碼的維護者,或者是使用你的系統 API 的開發者會根據數據結構做出相應的假設。如果一個方法調用通過 list 返回數據,開發者會假設數據已經以某種方式排序。如果是以 map 返回數據,開發者會假設會頻繁的根據 key 查找單個元素。如果數據是以 set 返回,開發者會假設一個元素只會存儲一次而不是多次。一個不錯的建議是在這個假設內工作而不是破壞它

降低復雜性

任何開發者,尤其是代碼審查者的總體目標應該是確保在最小的復雜度下代碼按照預期的行為工作-這樣可以使得代碼以后更易讀、更易理解、更容易修改、維護。在前面列出的一些反模式中(如錯誤使用 Set),我們可以發現選擇錯誤的數據結構會導致編寫更多的代碼。通常情況下選擇正確的數據結構都會簡化代碼。

總結

選擇正確的數據結構不僅僅是為了獲取性能或者在同行面前看起來很聰明。還會產生更易理解、更易維護的代碼。代碼編寫者選擇了錯誤數據結構的一些常見信號:

  • 頻繁通過迭代在一堆值中查找某幾個值
  • 頻繁重新排序數據
  • 沒有使用提供關鍵功能的方法--如棧的 push 或者 pop 方法
  • 不管是讀還是寫數據的代碼都很復雜

另外,不管是通過提供對數據結構本身的全局訪問,還是通過將類的接口緊密耦合到操作底層數據結構來暴露所選數據結構的細節,都會導致很脆弱的設計,并且以后難以修改。 在代碼審查過程中應盡早發現這些問題,而不是產生可避免的技術債務。

本文譯自 What to look for in a Code Review: Data Structures

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,237評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,957評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,248評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,356評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,081評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,485評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,534評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,720評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,263評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,025評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,204評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,787評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,461評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,874評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,105評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,945評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,205評論 2 375

推薦閱讀更多精彩內容