解讀阿里官方代碼規范


2017年開春,阿里對外公布了「阿里巴巴Java開發手冊」從頭到尾瀏覽了一遍這份手冊之后,感覺很棒。雖然其中的某些觀點筆者不能苟同,但大部分的規范還是值得絕大多數程序員學習和遵守的。

筆者將對這份代碼規范中的一些細節做一些解讀,包含筆者的觀點和想法,可以作為這份代碼規范的擴展閱讀。對於規范中某些「顯而易見」的條款,將不在解讀范圍之列(換言之,這都不懂,就說明你天賦不夠,乘早別做程序員了)。

當然,筆者在日常的編程過程中屬於「代碼潔癖偏執狂」,所以文中的某些觀點僅代表個人看法,請勿人生攻擊。

阿里官方代碼規范解讀系列總計五篇,已在本公眾號發過,本篇為合集,對之前文章中的部分內容作了修訂。

命名規約

復制代碼
1.1.1 代碼中的命名均不能以下划線或美元符號開始,也不能以下划線或美元符號結束

1.1.2 代碼中的命名嚴禁使用拼音與英文混合的方式,更不允許直接使用中文的方式

1.1.3 / 1.1.4 類名使用UpperCamelCase風格,必須遵從駝峰形式(某些情況諸如領域模型相關的命名除外);方法名、參數名、成員變量、局部變量都統一使用lowerCamelCase風格,必須遵從駝峰形式

1.1.5 常量命名全部大寫,單詞間用下划線隔開

1.1.9 包名統一使用小寫,點分隔符之間有且僅有一個自然語義的英語單詞
復制代碼

上述規則,主要是規定了你書寫Java的時候,哪些字符可以用,什么時候用大寫,什么時候用小寫。應該說,絕大多數寫Java的都遵循着上述的規范,就像筆者說的:尼瑪這都不懂,乘早改行別寫Java了。

筆者在實際編程過程中,對類名的風格,可能更激進一些,根據阿里的規范:

類名使用UpperCamelCase風格,必須遵從駝峰形式,但以下情形例外:(領域模型的相關命名)DO / BO / DTO / VO等

實際上還是有可能會存在着諸如:UserVO,UserDTO,UserDAO這樣的命名。對不起,在筆者的團隊中,這樣的命名也會被禁止,這里分為2種情況:

禁止使用 VO / BO / DTO / VO 等進行領域模型的命名

有讀者要問,那么如果萬一項目中要使用DTO或者VO咋辦?筆者的觀點如下:

第一,項目中避免使用DTO或者VO,DTO是一個早在2004年就被討論並認定為一個反模式的東西;

第二,誰規定領域模型就一定要用DTO或者VO做結尾?還原領域模型的本來意義才是命名的核心,一個User在實際業務系統中可能是一個Admin或者Supervisor,那就直接用Admin來命名,而不是把User轉化成UserVO,UserVO啥都不是,是初級程序員造出來的一個怪胎。

所有的DAO使用正常的駝峰法進行命名,例如:UserDao

對上面這條,或許有很多DAO大寫黨要發飆了。其實筆者就想反問這些人一句:你咋不把UserService寫成UserSERVICE呢?

命名原則

1.1.5 力求語義表達完整清楚,不要嫌名字長

1.1.10 杜絕完全不規范的縮寫,避免望文不知義

這兩條說的是命名的基本原則,總的來說其實表達了一個意思:你他媽的別給我用縮寫!

其實有很多程序員會非常神奇的患上「縮寫綜合症」。比如非常典型的就是:UserMgmt,這他媽是什么鬼?多敲幾個字母會死么?

類的命名

復制代碼
1.1.6 抽象類命名使用Abstract或Base開頭;異常類命名使用Exception結尾;測試類命名以它要測試的類的名稱開始,以Test結尾

1.1.13 對於Service和DAO類,基於SOA的理念,暴露出來的服務一定是接口,內部的實現類用Impl的后綴與接口區別

1.1.13 如果是形容能力的接口名稱,取對應的形容詞做接口名

1.1.14 枚舉類名建議帶上Enum后綴,枚舉成員名稱需要全大寫,單詞間用下划線隔開

1.1.11 如果使用到了設計模式,建議在類名中體現出具體模式

1.1.9 包名統一使用單數形式;類名如果有復數含義,類名可以使用復數形式
復制代碼

上述規則,主要在講述具體命名一個類的時候的一個「用詞規范」。這些用詞規范絕大多數實際上也是一種約定俗成,比如Abstract前綴,Exception后綴等等。

對於接口的命名,筆者最為不能忍受的一種命名,就是將所有的接口以大寫字母I開頭,諸如:IUserService。真是一種坑爹到極致的命名:第一,IUserService糾結是一個啥玩意兒?好好的UserService,加上一個大寫字母I,直接失去了閱讀時的語義性;第二,誰他媽知道你這東西到底是大寫字母I還是數字1啊?

有關枚舉類名是否加上Enum后綴,筆者對此有所保留。在筆者的團隊中,是不使用Enum作為后綴的,但對此並不反感。至於枚舉成員名稱,不用大寫字母並下划線隔開的,基本屬於缺心眼行為。Enum的設計初衷就是對常量的規整和擴展,所以命名規范繼承自常量是比較合理的一種選擇。

在命名中體現設計模式,相信這一點很多程序員都能遵守。因為在筆者看來,能在代碼中熟練使用設計模式的同學,通常也不會是一個對自己毫無要求的爛貨。這條命名規范在Spring以及很多優秀的開源項目中都有很深刻的體現。

類名是否可以使用復數形式,相信主要的分歧來自於工具類。筆者的規定是:

提供一系列靜態方法的工具類,一概使用Utils作為后綴命名

這條規范的依據,主要來自於筆者發現commons和spring這兩個比較優秀的開源框架中提供的工具類通常都帶s結尾。

常量規約

復制代碼
1.2.1 不允許出現任何魔法值(即未經定義的常量)直接出現在代碼中

1.2.3 不要使用一個常量類維護所有常量,應該按常量功能進行歸類,分開維護

1.2.4 常量的復用層次有五層:跨應用共享常量、應用內共享常量、子工程內共享常量、包內共享常量、類內共享常量

1.2.5 如果變量值僅在一個范圍內變化用Enum類。如果還帶有名稱之外的延伸屬性,必須使用Enum類

1.1.12 盡量不要在接口里定義變量,如果一定要定義變量,肯定是與接口方法相關,並且是整個應用的基礎常量
復制代碼

常量規約的核心有兩點:第一,別使用常量;第二,讓常量可控。

常量的存在按照筆者個人的理解是向下兼容的選擇(因為JDK1.5之后才出現枚舉)外加用起來足夠爽(想象一下靜態調用時引用的便捷性,甚至基本類型可以直接參與業務邏輯的計算)。

所以在上述規則中,我們可以看到常量進化到枚舉的趨勢,也能看到由於用起來足夠爽帶來的常量管理需求:要求分組(1.2.3)以及要求放在合適的位置(1.2.4)。

有關分組,筆者有不同意見:常量分組未必要分散到不同的類,在一個常量類中定義靜態類也是一種分組方式,有時候這樣的分組方式可能管理起來更有效。

至於接口中只能定義常量不能定義變量,基本就屬於幼兒園規則了。

語法糖

復制代碼
1.2.2 long或者Long初始賦值時,必須使用大寫的L,不能是小寫的l,小寫容易跟數字1混淆,造成誤解

1.1.12 接口類中的方法和屬性不要加任何修飾符號(public 也不要加),保持代碼的簡潔性,並加上有效的Javadoc注釋

1.4.2 所有的覆寫方法,必須加@Override注解

1.4.3 可變參數必須放置在參數列表的最后。(提倡同學們盡量不用可變參數編程)

1.4.4 對外暴露的接口簽名,原則上不允許修改方法簽名,避免對接口調用方產生影響。接口過時必須加@Deprecated注解,並清晰地說明采用的新接口或者新服務是什么

1.4.5 不能使用過時的類或方法

1.4.10 序列化類新增屬性時,請不要修改serialVersionUID字段,避免反序列失敗

1.4.17 循環體內,字符串的聯接方式,使用StringBuilder的append

1.4.18 final可提高程序響應效率

1.4.19 慎用Object的clone方法來拷貝對象
復制代碼

有關語法糖的總結其實比較牽強,因為絕大多數的規則看上去都比較小兒科,比如像覆寫方法的@Override注解,@Deprecated注解,可變參數的問題等等基本上都在IDE層面解決了。

當一個項目在IDE中產生了一些由於使用過時方法之類的事兒導致的warning時,有潔癖的程序員應該主動修復這個warning。這也是是一個程序員的基本素養問題。

最后的三條,筆者認為有點雞肋,對於初級程序員,大多還到不了考慮final和clone的層次;而中高級程序員,這幾條規則對他們而言並無問題。

基本類型

復制代碼
1.2.2 long或者Long初始賦值時,必須使用大寫的L,不能是小寫的l,小寫容易跟數字1混淆,造成誤解

1.4.7 所有的相同類型的包裝類對象之間值的比較,全部使用equals方法比較

1.4.8 所有的POJO類屬性必須使用包裝數據類型

1.4.8 RPC方法的返回值和參數必須使用包裝數據類型

1.4.8 所有的局部變量【推薦】使用基本數據類型
復制代碼

有關基本類型的聲明(1.2.2)和比較(1.4.7),這兩條規則比較直觀,不再敘述。

而有關基本類型和包裝類型的使用,這東西一直是吵架的核心。用還是不用?這是個問題!很顯然,阿里同學的觀點是:為了提高程序的容錯性和擴展性,盡可能使用包裝類型。

從阿里同學舉的例子來說,也是有一定說服力的:

數據庫的查詢結果可能是null,因為自動拆箱,用基本數據類型接收有NPE風險

比如顯示成交總額漲跌情況,即正負x%,x為基本數據類型,調用的RPC服務,調用不成功時,返回的是默認值,頁面顯示:0%,這是不合理的,應該顯示成中划線-。
所以包裝數據類型的null值,能夠表示額外的信息

不過筆者認為,如果程序員對程序能夠駕馭得比較好,基本類型也是一種很好的選擇。因為基本類型有一些比較好用的特性:比如說默認值。筆者在這里也舉個例子進行說明:

復制代碼
通常我們都會用is_disabled字段在數據庫中表示某一個表的記錄是否被邏輯刪除,而這個字段,在Java代碼中被映射成什么類型呢?

Boolean?如果被映射成包裝類型,那么數據庫里面的這個字段就可以為null,有些讀者會說,這並沒有什么問題啊。可是,數據庫is_disabled字段如果為null,代表什么邏輯含義呢?
這條記錄究竟是有效還是無效? 如果這個字段不能為null,那么將其映射成基本類型是一個皆大歡喜的事情:既保證了數據庫數據的完整性,我們在初始化的時候還可以忽略這個字段,因為boolean天生的默認值就是false
復制代碼

所以,筆者對於包裝類還是基本類型的結論是:

一切跟着業務的實際情況而定,基本類型也有其生存空間

方法命名

復制代碼
1.1.15  Service/DAO層方法命名規約
 - 獲取單個對象的方法用get做前綴
 - 獲取多個對象的方法用list做前綴
 - 獲取統計值的方法用count做前綴
 - 插入的方法用save(推薦)或insert做前綴
 - 刪除的方法用remove(推薦)或delete做前綴
 - 修改的方法用update做前綴
復制代碼

有關方法的命名,筆者想多說幾句不同意見。對於上述的規則,筆者認為適合在DAO這個層次進行實踐,而不能應用於Service層。

使用Hibernate作為持久層框架的讀者,對Hibernate的API應該比較熟悉,而上面的命名規范,和Hibernate對外暴露的API名稱是很接近的。我們知道,通常到了DAO這個層次,數據庫操作相對來說是一個原子操作,所以增刪改查的語義是最適合做方法命名的。這也就是筆者認為這套規則在DAO層能夠被實踐得很好的一個原因。

當然,上述規則中有一個例外:

獲取單個對象用load做前綴,避免使用get

原因很簡單,get可能是getter方法的前綴,作為一個偏執狂,老子不冒風險。

話題回到Service的命名上來,為什么筆者不認同使用相同的命名規范作用於Service層呢?因為Service層通常是對外暴露的接口,具有一定的業務意義,也就是說Service層通常也不會是簡單的增刪改查,而是若干原子操作的集合。

舉兩個很簡單的例子:發短信。發短信這個業務中可能包含了本地配置的讀取、本地數據庫的讀寫,遠程服務的調用。我們可以看到這是一連串數據庫操作甚至是異構系統調用的集合實現,能用簡單的增刪改查來命名嗎?所以,筆者的觀點很簡單:

Service層接口方法的命名,應還原業務的本來面目,采用動詞或者動賓結構來進行方法的命名

舉例來說:resetPassword / login / sendMessage 都是比較合理的命名方式。

方法和屬性

復制代碼
1.4.9 定義DO/DTO/VO等POJO類時,不要設定任何屬性默認值

1.4.11 構造方法里面禁止加入任何業務邏輯,如果有初始化邏輯,請放在init方法中

1.4.14 當一個類有多個構造方法,或者多個同名方法,這些方法應該按順序放置在一起,便於閱讀

1.4.15 類內方法定義順序依次是:公有方法或保護方法 > 私有方法 > getter/setter方法

1.4.16 setter方法中,參數名稱與類成員變量名稱一致,this.成員名=參數名。在getter/setter方法中,盡量不要增加業務邏輯

1.4.20 類成員與方法訪問控制從嚴
復制代碼

這幾條規約理解起來不難,執行起來也不難。要探究背后的原因,可能就需要花點功夫。

比如,構造方法和setter/getter方法禁止加入業務邏輯,主要是這些方法有很大概率被程序框架的反射機制調用。一旦其中含有業務邏輯,那么調試和定位就會變成災難

不過對於getter方法,通常要網開一面。因為在實際情況中,我們往往會在一個POJO中加入額外的getter方法用於序列化或者內部邏輯的使用。在這種情況下,避免和其他getter方法產生分歧是需要注意的地方。

至於說到類內的方法定義順序,筆者基本同意上述規則,但在實際執行中可能更加嚴格:getter和setter方法的順序也有嚴格講究,必須是先getter方法,后setter方法,而不是讓它們成對出現。

有關類成員和方法的訪問控制,阿里的同學洋洋灑灑說了好幾條,語法層面偏多,在這里就不再詳細展開。

格式規約

格式規約是代碼規范中爭議最大的,由於條目眾多,在這里就不逐一解讀,挑選幾條大致說一下。

復制代碼
1.3.5 縮進采用4個空格,禁止使用tab字符

1.3 6. 單行字符數限不超過 120 個

1.3.8 IDE的text file encoding設置為UTF-8; IDE中文件的換行符使用Unix格式,不要使用windows格式

1.3.10 方法體內的執行語句組、變量的定義語句組、不同的業務邏輯之間或者不同的語義之間插入一個空行
復制代碼

絕大多數情況下,空格黨和Tab黨的較量是空格黨完勝。筆者也不記得是多少年前被一位前輩教育說禁止使用Tab,就保持了良好的習慣至今。對於縮進,個人比較贊同4個空格,但HTML等頁面上使用4個空格的話,一些復雜頁面的縮進就會比較恐怖,此時可以降級為2個空格。

對於單行字符數的限制不超過120個這條規則,筆者完全不能認同。這里面牽涉到的情況比較多,不能一棒子打死了。有些邏輯有大量的分支和循環的嵌套,如果遵循4個空格的縮進原則,碰到方法名稱還比較長的狀況,就要折行,這給代碼閱讀帶來極大的困擾;另外有一種情況,就是需要額外進行比較長的注釋編寫,不能寫在一行里的感覺真是比較糟糕,因為還得考慮斷句才不影響閱讀。另外,筆者有一個習慣是在條件語句邊上加一句注釋,這樣就有很大概率會超出120字:

有人會問,條件語句邊上加注釋是什么鬼?從上面的代碼上可以看到,條件語句上面的一行注釋實際上在解釋整個代碼片段,而條件語句邊上的注釋說明的是條件語句本身!當然如果讀者有更好的寫注釋的位置,請及時給筆者留言。

文件的UTF-8和Unix格式沒什么好說的,IDE支持的也很好。但這一點對初級程序員尤為重要,我已經不知道多少次就這個問題懲罰過實習生了。

有關語句組空行,是筆者極力推薦的一個做法。這不僅僅是為了空行而空行,這里的空行本身就是一種編程思路的整理。而筆者還有一個習慣就是對比較復雜的邏輯,都在語句組的前面加上注釋,注釋也用編號編排,這樣回頭debug時也會極大提升效率。

集合類型

阿里規范中的集合類型這個章節,感覺寫得比較雞肋。絕大多數的規范似乎都是針對初級程序員的。筆者看了半天也沒總結出一條值得額外解讀的,所以權當復習一遍基礎知識就好。

並發處理

復制代碼
1.6.1 獲取單例對象需要保證線程安全,其中的方法也要保證線程安全

1.6.2 創建線程或線程池時請指定有意義的線程名稱,方便出錯時回溯

1.6.3 線程資源必須通過線程池提供,不允許在應用中自行顯式創建線程

1.6.4 線程池不允許使用Executors去創建,而是通過ThreadPoolExecutor去創建
復制代碼

上面這4條規則主要是針對線程的創建和使用。由於Spring的存在,其實上述情況不太可能發生。

1.6.5 高並發時,同步調用應該去考量鎖的性能損耗。能用無鎖數據結構,就不要用鎖;能鎖區塊,就不要鎖整個方法體;能用對象鎖,就不要用類鎖

1.6.7 對多個資源、數據庫表、對象同時加鎖時,需要保持一致的加鎖順序,否則可能會造成死鎖

1.6.8 並發修改同一記錄時,避免更新丟失,要么在應用層加鎖,要么在緩存加鎖,要么在數據庫層使用樂觀鎖,使用version作為更新依據

上面這3條規則主要是針對鎖。不過這幾條規則看上去更像是3道面試題的答案。這3道面試題分別是:

使用鎖同步,有什么需要注意的地方?

什么是死鎖?舉例說明什么情況會發生死鎖?

什么是樂觀鎖?什么是悲觀鎖?分別用在什么場景?

相信能解答上述面試題的同學,應該對上面的原則了然於心。

復制代碼
1.6.9 多線程並行處理定時任務時,Timer運行多個TimeTask時,只要其中之一沒有捕獲拋出的異常,其它任務便會自動終止運行,使用ScheduledExecutorService則沒有這個問題

1.6.10 使用CountDownLatch進行異步轉同步操作,每個線程退出前必須調用countDown方法,線程執行代碼注意catch異常,確保countDown方法可以執行,
避免主線程無法執行至countDown方法,直到超時才返回結果回溯 1.6.13 volatile解決多線程內存不可見問題。對於一寫多讀,是可以解決變量同步問題,但是如果多寫,同樣無法解決線程安全問題 1.6.14 HashMap在容量不夠進行resize時由於高並發可能出現死鏈 1.6.15 ThreadLocal無法解決共享對象的更新問題,ThreadLocal對象建議使用static修飾。這個變量是針對一個線程內所有操作共有的,所以設置為靜態變量,
所有此類實例共享此靜態變量
復制代碼

上面這些規則基本上屬於知識貼范疇,可以一帶而過,有些可能不太會實際碰到。像定時任務,可能使用Spring的封裝更多一些,而Spring默認就是使用ScheduledExecutorService的。

而CountDownLatch的異常捕獲,也是一個老生常談的問題了,屬於多線程編程的基本功。

最后的三條對於寫應用的同學接觸不多,但寫底層的同學應該會很熟悉。

注釋規約

注釋規約的內容比較多,這里也僅挑選一些具有代表性的進行解讀 

復制代碼
1.8.5 所有的枚舉類型字段必須要有注釋,說明每個數據項的用途

1.8.6 與其"半吊子"英文來注釋,不如用中文注釋把問題說清楚。專有名詞與關鍵字保持英文原文即可

1.8.8 注釋掉的代碼盡量要配合說明,而不是簡單的注釋掉

1.8.10 好的命名、代碼結構是自解釋的,注釋力求精簡准確、表達到位
復制代碼

枚舉類加注釋是非常必要的,因為枚舉通常是都是常量的擴展,而常量是需要說明的。

鑒於很多程序員的英語水平,筆者建議英語不夠好的程序員直接使用中文寫注釋。

對於注釋掉的代碼,筆者的意見是在絕大多數情況下應該直接刪除,除非在很短的時間內還有恢復的余地。

有關什么是好的命名和代碼結構,什么樣的命名能夠使得代碼做到自解釋,筆者將另外撰文進行說明。

數據庫規約

數據庫規約本身並不屬於Java規約的范疇,不過阿里的規范中包含了不少數據庫規約的內容,所以筆者也同樣加以解讀。

復制代碼
3.1.1 是與否概念的字段,必須使用is_xxx的方式命名,數據類型是unsigned tinyint( 1表示是,0表示否)

3.1 2 表名、字段名必須使用小寫字母或數字;禁止出現數字開頭,禁止兩個下划線中間只出現數字

3.1.3 表名不使用復數名詞

3.1.4 禁用保留字

3.1.5 唯一索引名為uk_字段名;普通索引名則為idx_字段名

3.1.10 表的命名最好是加上業務名稱_表的作用

3.1.11 庫名與應用名稱盡量一致
復制代碼

上述規約主要說的是庫、表、字段的命名規約。應該說絕大多數的上述規約都是參考項,需要根據實際情況進行調整,我們逐條來說。

有關布爾值的數據庫映射,對於使用is_xx進行命名沒有異議,對於數據類型是否應該使用tinyint稍有保留,筆者實際上使用bit更多。由於布爾值所對應的Java類型是boolean,所以筆者通常在命名時,利用boolean的默認值特性,對一些常用的命名進行更加嚴格的規定。比如「是否有效」,命名成為「is_disabled」就要比「is_enabled」來的好。因為 is_disable = false 是絕大多數程序的事實邏輯,這樣就可以利用boolean值默認為false的特性。

Java中的絕大多數命名都使用駝峰法,而數據庫的命名實際上更加嚴格。光光小寫是不夠的,而是要強制使用下划線命名法(主要是因為SQL是大小寫不敏感的語言)。筆者在實際工作中經常看到使用駝峰法命名表名或者字段名的,這種基本上屬於小學沒畢業的行為。

有關表名不能使用復數,不能使用關鍵字,這些屬於比較基礎的命名規范,應該遵守。但是筆者在這里提出更為嚴格的要求:不僅不能使用SQL關鍵字進行命名,同樣不允許使用Java關鍵字!因為絕大多數情況,數據庫字段會被映射到相應的Java對象,如果可以使用Java關鍵字,那么映射的時候就是自找麻煩了。

最后三條規約屬於建議,相信每個公司都有自己獨特的規定。比如筆者見過有一些寫Oracle出身的程序員,習慣使用tbl_做表名的前綴,使用vw_做視圖的前綴。個人覺得這個方面不宜做過多規定,只要團隊保持風格整體一致即可。

3.1.6 小數類型為decimal,禁止使用float和double

3.1.7 如果存儲的字符串長度幾乎相等,使用char定長字符串類型

3.1.8 varchar是可變長字符串,不預先分配存儲空間,長度不要超過5000

這三條主要說的是數據庫設計時的類型規約。

除了上述三條之外,在筆者團隊另外還會遵守如下幾條:

明確日期和時間,日期使用date類型並使用xxDate進行Java字段命名,時間使用date_time類型並使用xxTime進行Java字段命名,以示區分

上面這條主要是和日期時間有關的,強制這樣的規約,對於提升代碼的可讀性是有幫助的。

枚舉類型在數據庫中既可以映射成int,也可以映射成varchar,視實際情況定

通常對於排序和檢索有強依賴的,枚舉類型映射成int比較理想,否則可以映射成varchar。雖然從效率上說,int基本上會強於varchar,但varchar畢竟可讀性更好,所以還是應該一分為二來看。

復制代碼
3.1.9 表必備三字段:id, gmt_create, gmt_modified

3.1.8 如果存儲長度大於此值,定義字段類型為text,獨立出來一張表,用主鍵來對應,避免影響其它字段索引效率

3.1.12 如果修改字段含義或對字段表示的狀態追加時,需要及時更新字段注釋

3.1.13 字段允許適當冗余,以提高性能,但是必須考慮數據同步的情況

3.1.14 單表行數超過500萬行或者單表容量超過2GB,才推薦進行分庫分表

3.1.15 合適的字符存儲長度,不但節約數據庫表空間、節約索引存儲,更重要的是提升檢索速度
復制代碼

上面的規約主要涉及到一些數據庫表設計上的原則。

其中,3.1.8是非常值得大家注意的一點,筆者個人的習慣是對於大字段,拆表的同時,優化SQL,盡可能做到用主鍵單獨取大字段,避免產生效率瓶頸。

而3.1.14是希望提醒一些自視甚高的架構師不要過早的進行過度設計。這里筆者提一點:

對於每一張數據庫表的設計,應該預估表在未來若干時間段內的數量,以采取最佳的程序處理措施

這里所說的最佳程序處理措施包括並不限於:使用應用級別緩存對數據庫進行減壓;選取合適的時間點對表進行分庫分表;是否進行人為拆表以保證較快的SQL執行等等。

有關3.1.13,我們在有關SQL編寫環節還會說到。

復制代碼
3.2.1 業務上具有唯一特性的字段,即使是組合字段,也必須建成唯一索引

3.2.2 超過三個表禁止join

3.2.3 在varchar字段上建立索引時,必須指定索引長度

3.2.4 頁面搜索嚴禁左模糊或者全模糊

3.2.5 如果有order by的場景,請注意利用索引的有序性

3.2.7 利用延遲關聯或者子查詢優化超多分頁場景

3.2.9 建組合索引的時候,區分度最高的在最左邊
復制代碼

上述規約主要講的是和索引相關的內容。對於這塊,筆者不是專業的DBA,所以只是挑了其中和程序開發特別有關的來講一講。

比如3.2.2的禁止超過3個表的join,在筆者的團隊中,規定更為嚴格:

禁止超過2個表的join語句出現在程序中

其實不許使用join是很多初級程序員非常不能理解的。要說明白這個問題,估計又要長篇大論,筆者會另辟文章進行說明。但這里還是引用一下robbin的觀點(筆者表示深刻贊同):

另外有關嚴禁使用全模糊查找,建組合索引時,區分度最高的往左放這些原則,在一定程度上會改變我們編寫程序的習慣,所以應該時刻注意。

復制代碼
3.3.1 不要使用count(列名)或count(常量)來替代count(*)

3.3.5 在代碼中寫分頁查詢邏輯時,若count為0應直接返回,避免執行后面的分頁語句

3.3.6 不得使用外鍵與級聯,一切外鍵概念必須在應用層解決

3.3.7 禁止使用存儲過程,存儲過程難以調試和擴展,更沒有移植性

3.3.9 in操作能避免則避免,若實在避免不了,需要仔細評估in后邊的集合元素數量,控制在1000個之內

3.4.1 在表查詢中,一律不要使用 * 作為查詢的字段列表,需要哪些字段必須明確寫明
復制代碼

上面的這幾條屬於SQL編寫規約。阿里的規范中洋洋灑灑講了很多條,實際上都是在給程序員提個醒,筆者在這里不在贅述

有關count(*)的爭論,一直有大量的說法。此次阿里的規范總算為count(*)黨找到了SQL標准,應該說也基本為這件事情畫上了句號。

有關外鍵和級聯,筆者稍有困惑的是外鍵。因為按照筆者的理解,外鍵影響數據庫插入的速度應該有限,與外鍵約束帶來的好處相比,或許還是有外鍵更好一些(有這方面經驗的讀者可以留言指點迷津)。級聯是惡魔,必須禁止。

至於存儲過程,或許Oracle出身的DBA會跳出來唱反調了。筆者的觀點和阿里相同:存儲過程很難移植和維護,應該拋棄

有關表查詢中不許使用 * 作為查詢的字段列表,這點或許能夠成為規約,但筆者並不十分認同。尤其是對於使用Hibernate作為ORM工具的同學來說,這條規則執行起來有難度。

代碼風格

1.7.1 在一個switch塊內,每個case要么通過break/return等來終止,要么注釋說明程序將繼續執行到哪一個case為止;
在一個switch塊內,都必須包含一個default語句並且放在最后,即使它什么代碼也沒有

這條主要是期待程序員人為把握好代碼的執行邏輯。對於switch語句,如果沒有終止語句,會依次執行每一個case塊。實際上,筆者認為switch語句是一個比較差的語法糖,通常情況下都可以用更加優雅的方式來寫,包括並不限於使用設計模式。所以在筆者的團隊中是禁止使用switch語句的。

1.7 2 在if/else/for/while/do語句中必須使用大括號,即使只有一行代碼,避免使用下面的形式:if (condition) statements;

這一條比較有意思,因為這種一行式的代碼風格,在javascript里面經常會看到,所以很多全棧工程師也會把它引入到Java中來。筆者對此並不反感,但確實在可讀性上不那么友好。

復制代碼
1.7.3 推薦盡量少用else, if-else的方式可以改寫成:
if(condition){
...
return obj;
}
// 接着寫else的業務邏輯代碼;

1.7.3 如果非得使用if()...else if()...else...方式表達邏輯,【強制】請勿超過3層,超過請使用狀態設計模式
復制代碼

上面這點筆者比較認同,因為else不僅會帶來大段的代碼縮進的困擾,同時也會降低代碼的可讀性。不過對於那些堅持必須在代碼的最后一行統一return的同學,上面的寫法可能就不太容易接受了。實際上,上述代碼結構比較常見於Spring的源碼中,倒不是盡早return,而是else的邏輯塊可能直接throw異常出去了。

1.7.4 除常用方法(如getXxx/isXxx)等外,不要在條件判斷中執行其它復雜的語句,將復雜邏輯判斷的結果賦值給一個有意義的布爾變量名,以提高可讀性
boolean existed = (file.open(fileName, "w") != null) && (...) || (...);
if (existed) {
...
}

有關這一條,補充說明一下:將復雜的邏輯判斷結果賦值給一個有意義的布爾變量名,除了提高可讀性之外,實際上能夠極大方便調試。但筆者認為單單只是抽取部分代碼,並不能提高可讀性,而是應該將復雜的邏輯判斷進一步封裝為一個方法

上面的代碼片段中,左邊是阿里風格,右邊是陸老師的風格,大家可以比較一下,哪個更好?哪個更符合面向對象的思維呢?

1.7.5 循環體中的語句要考量性能,以下操作盡量移至循環體外處理,如定義對象、變量、獲取數據庫連接,進行不必要的try-catch操作

這一條值得說一下,因為有些代碼會走得比較深,寫着寫着就忘了它處於循環體的內部了。所以保持一個謹慎的心態比較重要。

復制代碼
1.7.7 方法中需要進行參數校驗的場景:
1) 調用頻次低的方法。 
2) 執行時間開銷很大的方法,參數校驗時間幾乎可以忽略不計,但如果因為參數錯誤導致中間執行回退,或者錯誤,那得不償失。
3) 需要極高穩定性和可用性的方法。 
4) 對外提供的開放接口,不管是RPC/API/HTTP接口。
5) 敏感權限入口。

1.7.8 方法中不需要參數校驗的場景: 
1) 極有可能被循環調用的方法,不建議對參數進行校驗。但在方法說明里必須注明外部參數檢查
2) 底層的方法調用頻度都比較高,一般不校驗。畢竟是像純凈水過濾的最后一道,參數錯誤不太可能到底層才會暴露問題。一般DAO層與Service層都在同一個應用中,
部署在同一台服務器中,所以DAO的參數校驗,可以省略 3) 被聲明成private只會被自己代碼所調用的方法,如果能夠確定調用方法的代碼傳入參數已經做過檢查或者肯定不會有問題,此時可以不校驗參數
復制代碼

這兩條說的是參數校驗,說的比較在理,也比較全面。比起很多公司的奇葩規定來說,要人性化得多。筆者認為需要補充的是:參數的校驗主要還需要從格式和業務兩個層面進行考量。業務層面的校驗往往要比單純的格式校驗更為復雜,所以在寫代碼時可以建立一定層次的假設,當然這可能也會引入團隊溝通的問題,需要根據實際情況權衡。

有關阿里代碼風格方面的解讀,受限於阿里自身提出的規約比較少。對此,筆者是稍有失望的。因為代碼風格規約是最能夠體現一個團隊對於代碼整潔程度的一個衡量標准。所以筆者忍不住在這里多加了幾條筆者團隊的共識,供讀者參考:

在任何情況下,代碼量越少越容易維護

基於上面的原則,筆者的團隊會鼓勵使用三目表達式對簡要的if/else進行重構

當然,像下列左側的代碼,也會重構成右側的:

 

一個復雜的Service層邏輯,不應超過30行,否則需要進行邏輯規整和抽象

在業務邏輯中盡可能不要使用setter方法,而是使用構造函數或者封裝成一個有邏輯意義的方法,提高代碼的可讀性

什么?連setter方法都不讓用?這是什么SB一樣的規約啊!事實上,筆者團隊確實是這么做的,我們來看一下代碼:

 

在上面的代碼中,左側代碼中的setter方法調用,會被封裝到ShuttleOrder對象中的cancel方法中去。在實際的service代碼中,只會出現下半部分的一行代碼。

這樣做的好處在於:cancel這個方法被封裝后,shuttleOrder.cancel()的調用從可讀性上要明顯優於使用2句setter方法,同時也為將來的邏輯擴展預留了位置。這也是面向對象的一種實踐。

 

轉載自:https://www.cnblogs.com/winner-0715/p/7594254.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM