寫高質量的程式碼
阿新 • • 發佈:2018-12-11
有人曾經說過我們正在臨近程式碼的終點。很快程式碼就會自動生產出來,不需要再人工編寫。程式設計師完全沒用了,因為商務人士 可以從規約直接生成程式碼。除非我們的人工智慧達到真的可以像人一樣思考,否則說這些都是扯淡!就目前來看我們在未來好多好多年還是無法拋棄掉程式碼, 因為程式碼呈現了需求的細節。在某些層面上,這些細節無法被忽略或者抽象,必須明確之。將需求明確到機器可以執行的細節程度,就是程式設計所要做的事。而這種規約正是程式碼。
我相信我們每個人都見過糟糕的程式碼, 看這些程式碼一般都令人痛苦不堪,到處都暗藏的沼澤地,一不留神就會陷入深淵。那麼為什麼要寫這樣糟糕的程式碼呢?是想快點完成?還是趕時間?都有可能。或許你覺得自己要幹好所需的時間不夠;假使花時間清理程式碼,老闆就會大發雷霆。或許你只是不耐煩再搞這套程式,期望早點結束。或許你看了看自己承諾要做的其他事,意識到需要儘快完成手頭上的工作,好接著完成下一件工作。雖然每個人的理由大概不會相同,但我相信這種事我們都幹過。我們都曾經瞟過一眼自己親手造成的混亂,決定棄之不顧,走向新的一天。我們都曾經看到自己的爛程式居然能執行,然後斷言能執行的爛程式總比什麼都沒有強。但當我們自己看到別人寫的爛程式時,肯定都說過這麼爛的程式碼還不如干掉自己重新寫。我們都曾經說過有朝一日再回頭清理。但是對於終日忙碌的程式設計師來說:稍後等於永遠不。因此我們必須時刻對自己嚴格要求,不僅僅只是按照需求完成開發任務,時刻保持程式碼的整潔,否則都最後肯定是自作自受。現在踩得坑,很多都是以前蹦躂留下的。
合理利用好集合的有序性(sort)和穩定性(order),避免集合的無序性(unsort)和不穩定性(unorder)帶來的負面影響。有序性是指遍歷的結果是按某種比較規則依次排列的。穩定性指集合每次遍歷的元素次序是一定的。例如:ArrayList是order/unsort;HashMap是unorder/unsort;TreeSet是order/sort。
1. 有意義的命名
我們的程式碼其實就是無數的類名,方法名和變數名的一個大集合。如果隨心所欲的亂起名字,那麼看你程式碼的人將相當痛苦,想必就是自己過一段時間來看自己的程式碼, 也得不會那麼容易吧。好的命名使人讀你的程式碼像是讀文章一樣簡單明瞭,通過名字其他人就可以知道你的程式碼的業務邏輯是什麼,不必再花費時間在看程式碼的具體實現細節。- 類名使用UpperCamelCase風格,必須遵從駝峰形式。但是一常見的英文縮寫除外。例如: TCP/ DTO / AO / UID等
- 方法名、引數名、成員變數、區域性變數都統一使用lowerCamelCase風格,必須遵從駝峰形式。
- 常量命名全部大寫,單詞間用下劃線隔開,力求語義表達完整清楚,不要嫌名字長。
- 杜絕完全不規範的縮寫,避免望文不知義。例如: AbstractClass“縮寫”命名成AbsClass;condition“縮寫”命名成 condi,此類隨意縮寫嚴重降低了程式碼的可閱讀
- 介面類中的方法和屬性不要加任何修飾符號,保持程式碼的簡潔性。
- 類中布林型別的變數,都不要加is字首。雖然在Hybris中不會引起錯誤,但是看看生成的get方法為isIsXXXXX,生成的set方法為setIsXXXXXX,不易閱讀。
- 杜絕使用讓人產生誤解的名字。例如: 之前我使用過一個叫做getConsultantForUID的一個方法,但是返回值確實一個CustomerData。
2.方法
對於方法來說重要的一定是短,單一職權,不要做過多的事。我在之前專案見到幾百行程式碼的方法以及上千行程式碼的類。我特別尤其討厭那種我滾動了幾下也沒有到頭的方法, 看了後邊忘了前邊,來來回回反覆的滾動來檢視程式碼,看這些程式碼使人抓狂。- 每個方法應該只做一件事,做好這件事,只做這一件事。這樣可以是複雜的事情變得簡單化,程式碼也更加清晰明瞭。
- 方法的引數不要太多,一般如果超過三個引數我們就需要考慮封裝引數物件。
- 不要重複自己,不要寫重複的程式碼。重複不僅後期程式碼難以維護,而且是很多Bug的源頭。在之前的一個專案,做一件事的方法我曾經見過4種以上的實現,這些重複的邏輯分佈在不同的層級中,不僅維護困難而且導致與這個邏輯相關的功能Bug不斷。
- 引數中不要傳遞標識引數如True或者False,這樣不僅醜陋不堪,而且使方法簽名立刻變得複雜,我們需要銘記方法只做一件事。
- 抽離Try/Catch,將錯誤處理邏輯與業務邏輯分離,錯誤處理本就是另外一件事。
3.註釋
關於註釋,我想說是清晰明瞭的高質量程式碼根本就不需要寫註釋,如果你打算通過寫註釋來向別人解釋你的程式碼,請在這之前考慮能否通過優化程式碼,通過程式碼來闡述你的意思而不是註解。保證方法和類的短小再加上有意義的命名,那麼別人看我們的程式碼一目瞭然,根本就不需要註釋。
- 儘量不要寫//TODO 如果非寫不可,請加上作者名,和預計需要完成的時間,不然到了後期別人根本不知道這是誰寫的,到底需不需要完成或者刪除。
- 如果寫了註解,便要維護這個註解,不然改了程式碼,但是不改註解,這就是在挖坑。
- 不需要的程式碼就直接刪除,不要註解掉了事,這會使別人產生困惑。
- 介面過時必須加@Deprecated註解,並清晰地說明採用的新介面或者新服務是什麼。
4.錯誤處理
當我們討論錯誤處理時,就一定要提及那些容易引發錯誤的做法。最常見的就是Null判斷。我們可以想象得到我們的專案中有多少檢查Null值得判斷,有多少Bug是因為Null導致的。下面是我之前改的一個Bug:public void removeUserGroup(String userGroupCode) { B2BCustomerModel b2bCustomer = (B2BCustomerModel)getUserService().getCurrentUser(); UserGroupModel userGroup = getContactService().getUserGroupByCode(userGroupCode); Set<ContactModel> contacts = userGroup.getContacts();//throw a NPE } public UserGroupModel getUserGroupByCode(final B2BCustomerModel consultant, String code) { UserGroupModel userGroupModel = consultant.getContactGroups().stream() .filter(c -> StringUtils.equals(code, c.getCode())).findFirst().orElse(null); return userGroupModel; }當時在執行contactGroup.getContacts()的時候出現了NPE, 我們一般的想法就是那就在加一個非空判斷不就行了嗎。但是這個Bug的真正原因卻是因為我們在之前處理userGroupCode的時候程式碼出現了錯誤,導致傳入了一個錯誤的userGroupCode。如果修改這個Bug的人並沒有注意到這一點的話,就只是簡單的添加了一個非空判斷,這就使這個問題隱藏的更深。但是如果當時getUserGroupByCode的方法寫的如下:
public UserGroupModel getUserGroupByCode(final B2BCustomerModel consultant, String code) { UserGroupModel userGroupModel = consultant.getContactGroups().stream() .filter(c -> StringUtils.equals(code, c.getCode())).findFirst().orElse(null); if(userGroupModel == null){ throw new UserGroupNotFoundException(String.format("User Group[%s] not found", code)) } return userGroupModel; }修改Bug的人很明顯的就可以看到這個錯誤是因為userGroupCode傳入錯誤導致的,並且找到這個Bug的真正原因。方法返回Null, 基本上是在給自己增加工作量,也是在給呼叫者添亂。只要有一處沒有檢查Null,應用程式就會失控。
5.控制語句
在高併發場景中避免使用“等於”作為中斷或者退出的條件。如果併發控制沒有處理好,容易產生等值判斷被擊穿的情況,可以使用大於或者小於的區間判斷條件來替代。 不要在判斷條件中寫入執行復雜業務邏輯的程式碼,將複雜業務邏輯的執行結果賦值給一個有意義的布林變數名,可以大大提高程式碼的可閱讀性。如下程式碼:if ((isAfterPickUpTime && openCloseDayPart != null && !DAY_PART_STATUS_INSIDE.equals(openCloseDayPart.getDayPartStatus())) || (openCloseDayPart != null && DAY_PART_STATUS_ENDINGSOON.equals(openCloseDayPart.getDayPartStatus()))){..............}這段程式碼對於閱讀它的人來說簡直是一種折磨。可以改成如下程式碼來提高閱讀性:
boolean productIsAvailableInDayPart = (isAfterPickUpTime && openCloseDayPart != null && !DAY_PART_STATUS_INSIDE.equals(openCloseDayPart.getDayPartStatus())) || (openCloseDayPart != null && DAY_PART_STATUS_ENDINGSOON.equals(openCloseDayPart.getDayPartStatus())); if(productIsAvailableInDayPart ){..............}迴圈體中執行的語句要考慮程式碼效能,儘量將定義物件、變數、獲取資料庫連線、try-catch等與迴圈變數無關的業務邏輯移至迴圈體外。 物件應該在其需要使用的範圍內在建立,例如:
for (final String code : pendingPromotionCodes){ final PromotionSourceRuleModel sourceRule = commercePromotionService.getPromotionSourceRuleByCode(code); if (sourceRule != null) { final MyOfferData myOfferData = offerConverter.convert(sourceRule); if (commercePromotionService.isPromotionAvailable(sourceRule.getCode())) { pendingOffers.add(myOfferData); } } }當sourceRule不為空時offerConverter.convert(sourceRule)總會執行,但是我們只是在它有效的時候才需要執行它,因此可以做如下改變:
for (final String code : pendingPromotionCodes){ final PromotionSourceRuleModel sourceRule = commercePromotionService.getPromotionSourceRuleByCode(code); if (sourceRule != null) { if (commercePromotionService.isPromotionAvailable(sourceRule.getCode())) { final MyOfferData myOfferData =offerConverter.convert(sourceRule); pendingOffers.add(myOfferData); } } }
6.集合處理
ArrayList的subList結果不可以強轉成ArrayList,否則會丟擲ClassCastException異常。subList 返回的是 ArrayList 的內部類 SubList,並不是 ArrayList而是ArrayList 的一個檢視,對於SubList子列表的所有操作最終會反映到原列表上。在subList場景中,高度注意對原集合元素的增加或刪除,均會導致子列表的遍歷、增加、刪除產生ConcurrentModificationException 異常。 使用工具類Arrays.asList()把陣列轉換成集合時,不能使用其修改集合相關的方法,它的add/remove/clear方法會丟擲UnsupportedOperationException異常。asList的返回物件是一個Arrays內部類,並沒有實現集合的修改方法。Arrays.asList體現的是介面卡模式,只是轉換介面,後臺的資料仍是陣列。 集合泛型定義時,在JDK7及以上,使用diamond語法或全省略。菱形泛型,即diamond,直接使用<>來指代前邊已經指定的型別。如下:// <> diamond方式 HashMap<String, String> userCache = new HashMap<>(16); // 全省略方式 ArrayList<User> users = new ArrayList(10);使用集合轉陣列的方法,必須使用集合的toArray(T[] array),傳入的是型別完全一樣的陣列,大小就是list.size()。使用toArray帶參方法,入參分配的陣列空間不夠大時,toArray方法內部將重新分配記憶體空間,並返回新陣列地址;如果陣列元素個數大於實際所需,下標為[ list.size() ]的陣列元素將被置為null,其它陣列元素保持原值,因此最好將方法入引數組大小定義與集合元素個數一致。直接使用toArray無參方法存在問題,此方法返回值只能是Object[]類,若強轉其它型別陣列將出現ClassCastException錯誤。 集合初始化時,指定集合初始值大小。例如HashMap使用HashMap(int initialCapacity) 初始化。initialCapacity = (需要儲存的元素個數 / 負載因子) + 1。注意 負載因子(即loader factor)預設為 0.75,如果 暫時無法 確定 初始值大小,請設定為 16(即預設值)。假如一個HashMap需要 放置 1024個元素, 由於 沒有設定容量 初始大小,隨著元素不斷增加容 量 7次被迫擴大, resize需要重建 hash表,嚴重影響效能。 Map類集合K/V能不能儲存null值的情況,如下表格:
集合類 | key | value | super | 說明 |
Hashtable | 不允許為null | 不允許為null | Dictionary | 執行緒安全 |
ConcurrentHashMap | 不允許為null | 不允許為null | AbstractMap | 鎖分段技術(JDK8:CAS) |
TreeMap | 不允許為null | 允許為null | AbstractMap | 執行緒不安全 |
HashMap | 允許為null | 允許為null | AbstractMap |
執行緒不安全 |
7.併發處理
執行緒池不要使用 Executors去建立,而是通過ThreadPoolExecutor的方式,這樣的處理方式讓寫的人更加明確執行緒池執行規則,避資源耗盡風險。Executors返回的執行緒池物件的弊端 如下 :- FixedThreadPool和SingleThreadPool執行緒的等待佇列長度為Integer.MAX_VALUE,可能會堆積大量請求,從而導致OOM。
- CachedThreadPool和ScheduledThreadPool的執行緒最大數量為Integer.MAX_VALUE,可能會建立大量請求,從而導致OOM。
public class DoubleCheckedDemo { private DoubleCheckedDemo(){} private static volatile DoubleCheckedDemo doubleCheckedDemo = null; public static DoubleCheckedDemo getInstance() { if (doubleCheckedDemo == null){ synchronized (DoubleCheckedDemo.class){ if(doubleCheckedDemo == null) { doubleCheckedDemo = new DoubleCheckedDemo(); } } } return doubleCheckedDemo; } }
volatile解決多執行緒記憶體不可見問題。對於一寫多讀,是可以解決變數同步問題,但是如果多寫,同樣無法解決執行緒安全問題。如果是count++操作,使用如下類實現:AtomicInteger count = new AtomicInteger(); count.addAndGet(1); 如果是JDK8,推薦使用LongAdder物件,比AtomicLong效能更好(減少樂觀鎖的重試次數)。
HashMap在容量不夠進行resize時由於高併發可能出現死鏈,導致CPU飆升,在開發過程中可以使用其它資料結構或加鎖來規避此風險。參考資料:
程式碼整潔之道 Clean Code
https://www.jianshu.com/p/59f5a5cbdf7c
集合類