一句註釋引發的思考
接到一個有雞毛信般的緊急需求(當然,002的需求向來是如此緊急的):大屏展示原來只有二個品牌資料,現增加到三個品牌的資料。一句話的需求,且沒有業務邏輯變更,我認為可以迅雷不及掩耳之勢,2小時收拾乾淨交差。當我滿腔激情的定位的核心邏輯部分時,這樣一句註釋(見下圖),讓我頓時思緒天馬行空:
這個作者經歷了什麼樣一個撕心裂肺的過程?但是可以肯點的是這一定是一個有想法的作者,不由得心中肅然起敬。
這段程式碼經歷了多少次的蹂躪,才會讓作者的心潮有如此波瀾?
抑或,這到底提出了怎麼樣一個需求,讓作者需要透過這樣的註釋來宣洩心中怨氣?
巴拉開程式碼修改記錄,作者已經去別的地方高就了,要不是留了這些程式碼,實在想不起有這樣的一個同事存在過;程式碼提交記錄比較整潔,大部分程式碼是在5月29號提交,5月30大概是修復bug提交了小部分程式碼。如此看來,程式碼沒有經歷過什麼苦難,這裡的需求僅僅是每個品牌的門店按訂單數量排序(如下圖),想想怎麼也鬧出什麼大動靜... 再細讀作者留下的程式碼,只能說作者給自己設定了難度係數(這說法太含蓄了),稍微有一點改動,便是牽一髮而動全身,於是留了這樣一個不太成熟且不太有價值的註釋,想起了最近在讀的一本書,Bob大叔的《程式碼整潔之道》,頗有一點不成熟的感觸。
如何衡量程式碼質量
《程式碼整潔之道》一書開篇第一句話就是一個著名的論斷:衡量程式碼質量的唯一有效標準:WTF/min。 一看到WTF這樣的簡稱就不明覺厲,一頓搜尋居然沒有找到“標準”的解釋,更覺得高深莫測。 也是在後來一個偶然的時間看到原來是 What-The-Fuck 的縮寫,看重看這張經典的圖,一下子恍然頓悟:原來如此,就該如此。
原本學得這是一本好書,甚至覺得大部分程式都應該去閱讀這類的書籍一遍(比如 馬丁·福勒出品的《重構-改善既有程式碼的設計》)。 看到這樣的論述,更覺此書接地氣,彷彿與大師拉近了距離了。回想起過往種種,以及最近修改歷史程式碼時的反應,沒什麼比WTF更有表達力了。
再回到開篇講的註釋,當時的作者必然也是有類似的反應,只是他的吐嘲物件是他自己,或者他只會認為這是需求的而不是程式碼的問題。所以 WTF/min作為衡量程式碼質量的唯一有效標準,還得加一個定語:優秀的Coder喊出的WTF次數,才是真正的標準。 至於如何為優秀的程式碼,在《程式碼整潔之道》,《重構-改善既有程式碼的設計》等經典書籍裡都有詳細的描述:
- 從變數命名,到註釋,到方法,到類,到模組都有非常詳細的規範;
- 從抽象到邊界,到依賴也有完整的方法論;
- 從SOLID設計原則到,元件相關的 CRP,CCP,CRP等原則都有從理論到落地的解說。
坦白講,讀完這些書後,我感覺自己原來寫的程式碼,很多都缺乏這樣的思考,也有不少程式碼相當醜陋,甚至覺得:在code這件事讓,只能算得上初窺門徑。於是越時讀書,越覺得自己無知。最近讀技術書籍的間歇,順便翻讀了幾本名人傳記:奧本海默為了讓人覺得他是天才,總是"偷偷"的讀書學習;特斯拉甚至在病危之際也是一心讀書;讓我們有在劍手的錢學森利用所有空閒時間閱讀方能一年完成碩士學位,並獲得航空和數學雙博士,成本最年輕的終生教授...(省略好多榮譽)。他們無一不是透過自己努力讀書,思考,實踐終成我等學習的楷模。最近招聘的經歷中,大部分應聘的人幾乎不看技術書籍的了,也是讓我捉摸不透。
讀技術書籍沒用了嗎
最近兩月一直忙於面試,溝通了沒有100個也有80個候選人,大部分人都沒有了讀書習慣,更不用說技術書籍了,倒是有部分說覺得blog比書籍有用多了。有些說工作太忙,有得說沒有用....他們中有的在傳統公司朝9晚5,有的在網際網路企業996,有從小企業過來的,也有從阿里,快手過來的... 其中一個人的話,讓我記憶非常深刻,最後環節,我問他現在還讀書不,他說為了進阿里,他非常努力了2年,把《深入理解java虛擬機器》讀了2遍,《高效能mysql》,《深入理解kafka》... 都仔細閱讀了,後來進了阿里,覺得這些書都沒有沒啥用了,也不在閱讀其他技術書籍了,最後我不知道應該說啥,畢竟我沒去過阿里,於是結束了面試。就我自己而言,早些年面試也是被問得體無完膚,也有過這樣的心態閱讀了大量類似 《深入理解java虛擬機器》,《MySQL技術內幕:InnoDB儲存引擎》,《RocketMQ技術內幕:RocketMQ架構設計與實現原理》,《從Paxos到Zookeeper:分散式一致性原理與實踐》等書籍,確實也讓自己在後來的面試中可以從容面對。但越是閱讀,越是感覺自己不知道的東西越多,越是想要透過閱讀來充實自己。於是又開始閱讀系統設計,架構一類的書籍。時至今日,讀到《程式碼整潔之道》時,依然覺得即使在做了10年的編碼這件事上,不懂的依然有非常之多(哈哈,也許是悟性不夠)。 從前面的名人,到我身邊認識的人,大凡優秀的人的,獨有閱讀的習慣,並且有大量閱讀自己專業相關的書籍。
最近和媳婦探討讀書這件事兒,說我週末在家除了溜娃就是重新整理聞,現如今的新聞包括熱搜又大部分是沒有“營養”的,也聊到目下短影片盛行,下到3歲,上到70,地鐵上,公園裡,城市裡,老家裡... 到處都是,當然也包括我們自己的爸媽,談及此,心中不覺升起一股名族憂心(哈哈,操心有點多了)。於是我放下了新聞,當然了周5晚上,等娃睡著後,我們買些宵夜,找一部金典電影還是保持著。 一段時間後,發現一個周閱讀10幾個小時好像也挺正常的,閱讀成生活的一部分。也沒有了過去那種讀了多久了,要休息下的,看看新聞,看看電視的想法了。現在想來,讀書也好,新聞,短影片也罷,本質且沒啥差異,內心富足就好。
再回到前面的面試,也不知道,我在面試過程中,把讀書這一塊看得如此重,是否合適,但是我相信:喜歡閱讀技術書籍的人,應該都不會太差。
回到前面的程式碼
回家開篇的註釋問題,想和大家一直分享下程式碼重構過程,如果不幸被作者看到,希望不要介懷,就如Bob大叔所講,每個程式設計師都應該接專業眼光的檢視(哈哈也許我也不是那麼專業)。 需求比較簡單,就是兩個品牌下的門店根據訂單數排序。 現在的需求是增加了第三個品牌,門店資訊有品牌屬性。
如果作者閱讀了Clean Code ,他就會明白程式碼走向整潔的4原則:
- 執行所有測試;
- 不可重複;
- 表達了程式設計師的意圖;
- 儘可能減少類和方法的數量。
就會把排序演算法抽離出來,與業務邏輯分離,避免大量重複;
如果他深刻喊出了 Don't Repeat YourSelf, 就不會有麼多 ConsultationOrderRank 物件的出事化,甚至不會單獨處理有資料與沒資料的情況。
如果作者閱讀了Clean Architecture,他就會明白要面向抽象,而不是具體去程式設計,
他就會面向品牌這個概念去程式設計,而不是面向具體的品牌1,品牌2去實現。
//需求變更,改的像辣雞。
if (CollectionUtils.isEmpty(orderList)) { List<CfgStore> allStoreList = cfgStoreService.getStoresBLAndBabyBL(); List<CfgStore> bellaList = allStoreList.stream().filter(st -> { return st.getType() == 0; }).sorted(Comparator.comparingInt(CfgStore::getStoreId)).collect(Collectors.toList()); ArrayList<ConsultationOrderRank> ballaResult = new ArrayList<>(); int bellaIndex = 0; for (CfgStore store : bellaList) { ConsultationOrderRank consultationOrderRank = new ConsultationOrderRank(); consultationOrderRank.setStoreName(store.getNameAlias()); consultationOrderRank.setStoreId(store.getStoreId()); consultationOrderRank.setOrderNum(0); consultationOrderRank.setSort(bellaIndex); ballaResult.add(consultationOrderRank); bellaIndex++; } List<ConsultationOrderRank> blRankResult = ballaResult.stream() .sorted(Comparator.comparing(ConsultationOrderRank::getSort)).collect(Collectors.toList()); List<CfgStore> babyBellaList = storeList.stream().filter(st -> { return st.getType() == 1; }).sorted(Comparator.comparingInt(CfgStore::getStoreId)).collect(Collectors.toList()); ArrayList<ConsultationOrderRank> babyBallaResult = new ArrayList<>(); int babyIndex = 0; for (CfgStore store : babyBellaList) { ConsultationOrderRank consultationOrderRank = new ConsultationOrderRank(); consultationOrderRank.setStoreName(store.getNameAlias()); consultationOrderRank.setStoreId(store.getStoreId()); consultationOrderRank.setOrderNum(0); consultationOrderRank.setSort(babyIndex); babyBallaResult.add(consultationOrderRank); babyIndex++; } List<ConsultationOrderRank> babyRankResult = babyBallaResult.stream() .sorted(Comparator.comparing(ConsultationOrderRank::getSort)) .collect(Collectors.toList()); Order order = Order.builder().consultationOrderRankStBellaList(blRankResult) .consultationOrderRankBabyBellaList(babyRankResult).build(); return order; } List<CfgStore> others = storeList.stream().filter(store -> { return !Arrays.stream(storeIdArr).collect(Collectors.toList()).contains(store.getStoreId()); }).collect(Collectors.toList()); Map<Integer, CfgStore> storeMap = storeList.stream().collect(Collectors.toMap(CfgStore::getStoreId, store -> { return store; })); //品牌1門店ID List<Integer> blIdList = storeList.stream().filter(st -> st.getType().equals(0)) .map(CfgStore::getStoreId).collect(Collectors.toList()); //品牌2門店ID List<Integer> babyblIdList = storeList.stream().filter(st -> st.getType().equals(1)) .map(CfgStore::getStoreId).collect(Collectors.toList()); //品牌2分組資料 Map<Integer, List<HeOrder>> babyblMap = orderList.stream() .filter(order -> babyblIdList.contains(order.getStoreId())) .collect(Collectors.groupingBy(HeOrder::getStoreId)); //品牌2分組資料 Map<Integer, List<HeOrder>> blMap = orderList.stream() .filter(order -> blIdList.contains(order.getStoreId())) .collect(Collectors.groupingBy(HeOrder::getStoreId)); //品牌1排行資料 List<ConsultationOrderRank> bellaList = new ArrayList<>(); //品牌2排行資料 List<ConsultationOrderRank> babyBellaList = new ArrayList<>(); //品牌1 for (Entry<Integer, List<HeOrder>> entry : babyblMap.entrySet()) { CfgStore cfgStore = storeMap.get(entry.getKey()); String storeName = cfgStore.getNameAlias(); if (Strings.isNotBlank(storeName)) { List<HeOrder> orderNum = entry.getValue(); ConsultationOrderRank consultationOrderRank = new ConsultationOrderRank(); consultationOrderRank.setStoreName(storeName); consultationOrderRank.setStoreId(entry.getKey()); consultationOrderRank.setOrderNum(orderNum == null ? 0 : orderNum.size()); babyBellaList.add(consultationOrderRank); } } List<CfgStore> otherbabyBlList = others.stream().filter(store -> { return store.getType() == 1; }).collect(Collectors.toList()); for (CfgStore store : otherbabyBlList) { ConsultationOrderRank consultationOrderRank = new ConsultationOrderRank(); consultationOrderRank.setStoreName(store.getNameAlias()); consultationOrderRank.setStoreId(store.getStoreId()); consultationOrderRank.setOrderNum(0); babyBellaList.add(consultationOrderRank); } //品牌2 for (Entry<Integer, List<HeOrder>> entry : blMap.entrySet()) { CfgStore cfgStore = storeMap.get(entry.getKey()); String storeName = cfgStore.getNameAlias(); if (Strings.isNotBlank(storeName)) { List<HeOrder> orderNum = entry.getValue(); ConsultationOrderRank consultationOrderRank = new ConsultationOrderRank(); consultationOrderRank.setStoreName(storeName); consultationOrderRank.setStoreId(entry.getKey()); consultationOrderRank.setOrderNum(orderNum == null ? 0 : orderNum.size()); bellaList.add(consultationOrderRank); } } List<CfgStore> otherBellaList = others.stream().filter(store -> { return store.getType() == 0; }).collect(Collectors.toList()); for (CfgStore store : otherBellaList) { ConsultationOrderRank consultationOrderRank = new ConsultationOrderRank(); consultationOrderRank.setStoreName(store.getNameAlias()); consultationOrderRank.setStoreId(store.getStoreId()); consultationOrderRank.setOrderNum(0); bellaList.add(consultationOrderRank); } //品牌1排序 List<ConsultationOrderRank> blRank = bellaList.stream().sorted(Comparator.comparing(ConsultationOrderRank::getOrderNum).reversed()) .collect(Collectors.toList()); int blSort = 0; int blIndexCounter = 0; for (int i = 0; i < blRank.size(); i++) { //訂單=0, 訂單值不同, 遞增 boolean flag = blRank.get(i).getOrderNum() == 0 || (i != 0 && blRank.get(i).getOrderNum() != blRank.get(i - 1).getOrderNum()); if (flag) { blSort = blSort + blIndexCounter + 1; blIndexCounter = 0; } else { if (i != 0) { blIndexCounter++; } } blRank.get(i).setSort(blSort); } //品牌2排序 List<ConsultationOrderRank> babyBlRank = babyBellaList.stream().sorted(Comparator.comparing(ConsultationOrderRank::getOrderNum).reversed()) .collect(Collectors.toList()); int babySort = 0; int babyIndexCounter = 0; for (int i = 0; i < babyBlRank.size(); i++) { //訂單=0, 訂單值不同, 遞增 boolean flag = babyBlRank.get(i).getOrderNum() == 0 || (i != 0 && babyBlRank.get(i).getOrderNum() != babyBlRank.get(i - 1).getOrderNum()); if (flag) { babySort = babySort + babyIndexCounter + 1; babyIndexCounter = 0; } else { if (i != 0) { babyIndexCounter++; } } babyBlRank.get(i).setSort(babySort); }
我相信作者如果經常閱讀技術書籍,寫出的程式碼應該是這樣的。
//統計每個品牌每個門店訂單數量 for (Integer brandType : brandTypeList){ Map<Integer, Long> theBrandStoreOrderCount = orderList.stream().filter(order -> brandType.longValue() == order.getBrandType()).collect(Collectors.groupingBy(HeOrder::getStoreId, Collectors.counting())); List<CfgStore> brandStores = storeList.stream().filter(store -> store.getType().equals(brandType)).collect(Collectors.toList()); List<ConsultationOrderRank> storeOrderRank = new ArrayList<>(); brandStores.forEach(store ->{ Long orderCount = 0L; if (theBrandStoreOrderCount.containsKey(store.getStoreId())){ orderCount = theBrandStoreOrderCount.get(store.getStoreId()); } ConsultationOrderRank storeOrder = ConsultationOrderRank.builder() .storeId(store.getStoreId()) .orderNum(orderCount.intValue()) .storeName(store.getStoreName()) .sort(0).build(); storeOrderRank.add(storeOrder); }); List<ConsultationOrderRank> sortedStoreRank = storeOrderRank.stream().sorted(Comparator.comparing(ConsultationOrderRank::getOrderNum).reversed()).collect(Collectors.toList()); setSortWithSameRankNum(sortedStoreRank); BrandOrderStatistic statistic = BrandOrderStatistic.builder() .name(CfgStoreEnum.getValueByCode(brandType)) .storeOrderRank(sortedStoreRank) .brandType(brandType).build(); brandOrderStatistics.add(statistic); }
如此這般,我們當時踐行了編碼裡的童子軍規:當你離開營地時候,要讓它比你來的時候更整潔乾淨。
成為一名優秀的程式設計師!