代碼審查關(guān)注什么:數(shù)據(jù)結(jié)構(gòu)

數(shù)據(jù)結(jié)構(gòu)是編程的基礎(chǔ),所以它是計(jì)算機(jī)科學(xué)課程中一直講授的領(lǐng)域之一。然而,令人驚訝的是很容易錯(cuò)誤使用或者選擇錯(cuò)誤的數(shù)據(jù)結(jié)構(gòu)。在本文中,我們將引導(dǎo)你作為代碼審查者關(guān)注需要關(guān)注的那些事情--我們將通過(guò)一系列示例代碼來(lái)討論“壞的代碼味道”,這些“味道”可能暗示我們選擇了錯(cuò)誤的數(shù)據(jù)結(jié)構(gòu)或者是數(shù)據(jù)結(jié)構(gòu)被以錯(cuò)誤的方式使用。

列表(Lists)

這也許是最常用的數(shù)據(jù)結(jié)構(gòu)。因?yàn)槭亲畛R?jiàn)的選擇,它有時(shí)被用在了錯(cuò)誤的地方。

反模式:過(guò)多搜索

迭代列表本身當(dāng)然不是一件壞事情。但是如果要求迭代是一個(gè)常用操作(比如像上面代碼這樣通過(guò) ID 查找一個(gè)客戶),應(yīng)該有更好的數(shù)據(jù)結(jié)構(gòu)可以使用。在這個(gè)例子中,因?yàn)槲覀兂3P枰ㄟ^(guò) ID 查找某個(gè)特定的項(xiàng)目,也許創(chuàng)建一個(gè) ID-->Customer 的 map 更合適。

注意,在 Java 8 中以及其他支持更多表達(dá)式搜索的語(yǔ)言中,這一點(diǎn)可能沒(méi)有 for 循環(huán)那么明顯,但是問(wèn)題仍然存在。

反模式:頻繁重新排序

如果使用它們的默認(rèn)排序列表是很棒的,但是如果作為審查者你發(fā)現(xiàn)代碼中對(duì)列表重新排序,確認(rèn)一下使用列表是否合適。在上面的例子中,在第16行 twitterUsers 總是重新排序以后再返回。再次說(shuō)明,Java 8 使得這個(gè)操作看起來(lái)很簡(jiǎn)單,可能很容易忽略這個(gè)信號(hào):

在這種情況下,鑒于 TwitterUser 是獨(dú)立的并且看起來(lái)你需要一個(gè)默認(rèn)已經(jīng)排序好的集合,你可能需要類似 TreeSet 這樣的數(shù)據(jù)結(jié)構(gòu)。

Maps

如果你選擇了正取的 key, map是一種對(duì)單個(gè)元素的訪問(wèn)只有 O(1)復(fù)雜度的多用途數(shù)據(jù)結(jié)構(gòu)。

反模式:Map 作為全局常量

map 是一個(gè)很好的通用數(shù)據(jù)結(jié)構(gòu),以致可以用一個(gè)全局的 map 來(lái)讓其他類訪問(wèn)。

在上面的例子中,作者簡(jiǎn)單的將 CUSTOMERS 這個(gè) map 作為全局常量。CustomerUpdateService需要添加或者更新 customers 時(shí)就直接使用這個(gè) map。這看起來(lái)沒(méi)什么問(wèn)題,因?yàn)?CustomerUpdateService的職責(zé)就是負(fù)責(zé)添加和更新操作,這些操作最終會(huì)修改 map。問(wèn)題是當(dāng)其他類,尤其是系統(tǒng)中自其它模塊的類需要訪問(wèn)數(shù)據(jù)的時(shí)候:

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

盡管修改以后的代碼并不短,你獲得了標(biāo)準(zhǔn)化以及集中的核心函數(shù)--例如,你知道在獲取一個(gè)不存在的 customer 信息時(shí)總會(huì)返回一個(gè)異常?;蛘吣阋部梢赃x擇返回一個(gè)新的 Optional 類型。

注意這是屬于在代碼審查中就應(yīng)該發(fā)現(xiàn)的一類問(wèn)題,如果一個(gè)全局變量的訪問(wèn)已經(jīng)遍及整個(gè)系統(tǒng),需要隱藏它是比較困難的,但是當(dāng)它第一次被引入時(shí)還是很容易實(shí)現(xiàn)的。

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

和列表一樣,如果在 map 上進(jìn)行了大量的排序或者迭代操作,你應(yīng)該建議采用其他可替換的數(shù)據(jù)結(jié)構(gòu)。

Java 代碼需要特別關(guān)心的事情

在 Java 中,map 的行為依賴于 key 和 value 的 equalshashCode 方法的實(shí)現(xiàn)。作為審查者,應(yīng)該檢查 key 和 value 類的這些方法,以確保獲得預(yù)期的行為。

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

Sets

一個(gè)常常不被充分使用的數(shù)據(jù)結(jié)構(gòu),它的優(yōu)點(diǎn)是不會(huì)包含重復(fù)的元素。

反模式:有時(shí)你真的需要重復(fù)元素

我們假設(shè)你有一個(gè) user 類,使用了 set 來(lái)跟蹤其訪問(wèn)過(guò)的網(wǎng)站?,F(xiàn)在有一個(gè)新的功能要求返回這些網(wǎng)站中最近訪問(wèn)的一個(gè)。


這段代碼的作者將跟蹤一個(gè)用戶訪問(wèn)網(wǎng)站的 set 從 HashSet 修改為 LinkedHashSet,后者的實(shí)現(xiàn)保留了插入順序,所以現(xiàn)在我們的 set 按照訪問(wèn)的順序跟蹤每一個(gè) URI。

然而這段代碼中有很多信號(hào)說(shuō)明它是錯(cuò)誤的。首先--由于 set 不是為按照位置訪問(wèn)設(shè)計(jì)的,為了獲取最后一個(gè)元素必須迭代整個(gè) set(第13-15行),這樣的訪問(wèn)開(kāi)銷太大,有時(shí)候 list 是一個(gè)完美的選擇。其次,由于 sets 中不包含重復(fù)的值,如果最后訪問(wèn)的頁(yè)面在之前已經(jīng)訪問(wèn)過(guò),那么它在 set 中的位置并不在最后。相反,它會(huì)處在第一次被添加的位置。

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

Java 需要特別注意
因?yàn)?set 的一個(gè)關(guān)鍵操作是 contains,作為審查者你應(yīng)該檢查 set 所包含的類型的 equals 方法實(shí)現(xiàn)。

棧(Stacks)

Stacks是計(jì)算機(jī)科學(xué)課程最喜歡的數(shù)據(jù)結(jié)構(gòu)之一,但是在現(xiàn)實(shí)世界中常常被忽略-在 Java 中,也許是因?yàn)?Stack 繼承自 Vector,所以有一點(diǎn)點(diǎn)過(guò)時(shí)。在這里我不討論具體的細(xì)節(jié),只是列出一些關(guān)鍵的點(diǎn):

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

隊(duì)列(Queues)

計(jì)算機(jī)科學(xué)最喜歡的另一個(gè)數(shù)據(jù)結(jié)構(gòu)。Queues 常常在討論并發(fā)相關(guān)的話題時(shí)出現(xiàn)(確實(shí),Java 中大多數(shù) Queue 的實(shí)現(xiàn)都在 java.util.concurrent 中使用),因?yàn)樗畛R?jiàn)的用法是在線程和模塊之間傳遞數(shù)據(jù)。

  • 隊(duì)列是 FIFO 的數(shù)據(jù)結(jié)構(gòu),通常在你想往尾部添加數(shù)據(jù)或者從頭部移除數(shù)據(jù)時(shí)非常合適。如果你在審查代碼是發(fā)現(xiàn)對(duì)隊(duì)列進(jìn)行迭代操作(特殊情況下訪問(wèn)隊(duì)列中間的元素),需要確認(rèn)一下隊(duì)列是否是正確的數(shù)據(jù)結(jié)構(gòu)。
  • 隊(duì)列可以是限定大小的,也可以是不限定大小的。不限定大小的隊(duì)列可能會(huì)一直增長(zhǎng),所以如果審查代碼時(shí)發(fā)現(xiàn)使用了不限定大小隊(duì)列,請(qǐng)注意我們?cè)?a href="http://www.itdecent.cn/p/8cf4f3fe5506" target="_blank">上一篇文章中討論的性能問(wèn)題。限定大小的隊(duì)列也有它的問(wèn)題--在審查代碼時(shí),需要關(guān)注什么條件下隊(duì)列會(huì)滿,并且了解隊(duì)列滿的情況下系統(tǒng)會(huì)做出什么反應(yīng)。

Java 開(kāi)發(fā)者特別注意

作為審查者,你不僅僅要了解通用的數(shù)據(jù)結(jié)構(gòu)特性,還需要注意各種實(shí)現(xiàn)的優(yōu)點(diǎn)和弱點(diǎn),這些知識(shí)在 Javadoc 中都有詳細(xì)的說(shuō)明:

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

為什么要選擇正確的數(shù)據(jù)結(jié)構(gòu)?

我們已經(jīng)在這篇博客中討論了數(shù)據(jù)結(jié)構(gòu)--怎樣確定被審查的代碼是否使用了錯(cuò)誤的數(shù)據(jù)結(jié)構(gòu),以及各種數(shù)據(jù)結(jié)構(gòu)的優(yōu)缺點(diǎn)的要點(diǎn),這樣作為審查者不僅僅可以確認(rèn)數(shù)據(jù)結(jié)構(gòu)沒(méi)有正確使用,而且可以給出更好的替代方案。我們一起來(lái)看一下為什么選擇正確的數(shù)據(jù)結(jié)構(gòu)如此重要。

性能

如果你在計(jì)算機(jī)科學(xué)課程中學(xué)習(xí)了數(shù)據(jù)結(jié)構(gòu),你應(yīng)該知道選擇數(shù)據(jù)結(jié)構(gòu)對(duì)性能的影響,事實(shí)上,我們?cè)谶@篇博客中甚至用“大O表示法”來(lái)強(qiáng)調(diào)特定數(shù)據(jù)結(jié)構(gòu)的某些優(yōu)點(diǎn)。在代碼中使用正確的數(shù)據(jù)結(jié)構(gòu)當(dāng)然會(huì)對(duì)性能有幫助,但是這不是選擇正確工具的唯一理由。

表述預(yù)期的行為

代碼的維護(hù)者,或者是使用你的系統(tǒng) API 的開(kāi)發(fā)者會(huì)根據(jù)數(shù)據(jù)結(jié)構(gòu)做出相應(yīng)的假設(shè)。如果一個(gè)方法調(diào)用通過(guò) list 返回?cái)?shù)據(jù),開(kāi)發(fā)者會(huì)假設(shè)數(shù)據(jù)已經(jīng)以某種方式排序。如果是以 map 返回?cái)?shù)據(jù),開(kāi)發(fā)者會(huì)假設(shè)會(huì)頻繁的根據(jù) key 查找單個(gè)元素。如果數(shù)據(jù)是以 set 返回,開(kāi)發(fā)者會(huì)假設(shè)一個(gè)元素只會(huì)存儲(chǔ)一次而不是多次。一個(gè)不錯(cuò)的建議是在這個(gè)假設(shè)內(nèi)工作而不是破壞它。

降低復(fù)雜性

任何開(kāi)發(fā)者,尤其是代碼審查者的總體目標(biāo)應(yīng)該是確保在最小的復(fù)雜度下代碼按照預(yù)期的行為工作-這樣可以使得代碼以后更易讀、更易理解、更容易修改、維護(hù)。在前面列出的一些反模式中(如錯(cuò)誤使用 Set),我們可以發(fā)現(xiàn)選擇錯(cuò)誤的數(shù)據(jù)結(jié)構(gòu)會(huì)導(dǎo)致編寫(xiě)更多的代碼。通常情況下選擇正確的數(shù)據(jù)結(jié)構(gòu)都會(huì)簡(jiǎn)化代碼。

總結(jié)

選擇正確的數(shù)據(jù)結(jié)構(gòu)不僅僅是為了獲取性能或者在同行面前看起來(lái)很聰明。還會(huì)產(chǎn)生更易理解、更易維護(hù)的代碼。代碼編寫(xiě)者選擇了錯(cuò)誤數(shù)據(jù)結(jié)構(gòu)的一些常見(jiàn)信號(hào):

  • 頻繁通過(guò)迭代在一堆值中查找某幾個(gè)值
  • 頻繁重新排序數(shù)據(jù)
  • 沒(méi)有使用提供關(guān)鍵功能的方法--如棧的 push 或者 pop 方法
  • 不管是讀還是寫(xiě)數(shù)據(jù)的代碼都很復(fù)雜

另外,不管是通過(guò)提供對(duì)數(shù)據(jù)結(jié)構(gòu)本身的全局訪問(wèn),還是通過(guò)將類的接口緊密耦合到操作底層數(shù)據(jù)結(jié)構(gòu)來(lái)暴露所選數(shù)據(jù)結(jié)構(gòu)的細(xì)節(jié),都會(huì)導(dǎo)致很脆弱的設(shè)計(jì),并且以后難以修改。 在代碼審查過(guò)程中應(yīng)盡早發(fā)現(xiàn)這些問(wèn)題,而不是產(chǎn)生可避免的技術(shù)債務(wù)。

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,534評(píng)論 19 139
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語(yǔ)法,類相關(guān)的語(yǔ)法,內(nèi)部類的語(yǔ)法,繼承相關(guān)的語(yǔ)法,異常的語(yǔ)法,線程的語(yǔ)...
    子非魚(yú)_t_閱讀 34,638評(píng)論 18 399
  • //Clojure入門教程: Clojure – Functional Programming for the J...
    葡萄喃喃囈語(yǔ)閱讀 4,036評(píng)論 0 7
  • 再過(guò)半個(gè)小時(shí)就是我24歲生日了。 并沒(méi)有什么不同。 小時(shí)候愛(ài)過(guò)生日,因?yàn)樯帐莻€(gè)滿足愿望的時(shí)刻。這天你最大! 這幾...
    今天是想走路回家的日子閱讀 368評(píng)論 2 1
  • 傳奇終會(huì)謝幕,青春也終究會(huì)成為過(guò)去,英雄自古惺惺相惜,對(duì)于林丹而言,一生能有李宗偉這樣一個(gè)對(duì)手何其有幸。 這次的里...
    Witty張闖閱讀 277評(píng)論 0 0

友情鏈接更多精彩內(nèi)容