關於重構的講義

<script type=text/javascript> new Draggable("related_topics"); </script> Bad Smells & Refactoring
以前做的一個培訓,當時備課時還是花了一些工夫。ppt貼不上來,把備課稿貼在這,備份一個吧。
 
Bad Smells & Refactoring
1 題記
Any fool can write code that a computer can understand. Good programmers write code that humans can understand.——Martin Fowler
(任何一個傻瓜都能寫出計算機可以理解的代碼。唯有寫出人類容易理解的代碼,纔是優秀的程序員。)
2 Bad Smells
2.1 Duplicated Code重複的代碼
(重複代碼,不需要定義,大家都知道是什麼東東。)
1. “重複的代碼”有什麼不好?(比方說,從可維護性的角度看,很多地方的代碼看似一樣,又有可能有細微差別,對讀代碼的人容易產生困擾,於是可以定罪,可讀性不好;同樣的修改可能需要修改多次,而且容易遺漏,可修改性不好。)(但是,重複代碼也有好處啊,代碼行數多,千行代碼故障率會少,對我們的考覈比較有利……開玩笑的是麼?)
2. 代碼重複到什麼程度,Smell纔算Bad?(if (filename == null || filename.trim().equals(""))這句話可作爲例子,我們認爲這句話就算是重複代碼了,比如哪天filename.trim().equals("")這個判斷我覺得不好,想要換成filename.trim().length() == 0,那麼豈不是霰彈式修改?使用絕技“Ctrl-C/ Ctrl-V”、同時還有用“Ctrl-F”,因爲大家沒有住在一起,要搜索一下才聯繫得到……)
3. 三種情況的重複代碼:
a) 同一個class中的兩個method含有相同表達式。(Extract Method)
b) 兩個互爲兄弟的subclass內含有相同表達式。(Extract Method、Pull up Method、Form Template Method)
c) 兩個毫不相關的class內含有相同表達式。( Extract Method、 Extract Class)
2.2 Long Method過長函數
2.2.1 “過長函數”有什麼不好?
1. 可讀性:使用短函數,高層函數看起來像系列註釋、低層函數不超過(比方說)10行,邏輯一目瞭然,可讀性更優。
2. 可重用性:(長函數可能包含邏輯A/B/C,想單獨重用A邏輯,不可能。一個類比的例子,發動機的重用機會肯定比車大。大家知道深圳BYD使用的是三菱發動機,標緻307/206 1.6系列使用的發動機跟富康、愛麗舍16V系列是相同的。發動機上面的螺絲重用機會更大。很多不同種類發動機上使用的螺絲很可能是同一個廠家生產的相同螺絲。)
3. 可插入性:使用短函數,利用Override等操作,替換處理邏輯會更加容易。比如,更容易應用模板方法模式Template Method,參見Form Template Method章節。
2.2.2  “短函數”難道沒有缺點?
1. 調用開銷?(現代OO語言幾乎已經完全免除了進程內的“函數調用動作的額外開銷”。)
2. 看代碼的時候,跳來跳去,很心煩?(高層函數看起來像系列註釋,而且函數名字起得好,單純看代碼,比如接手模塊的時候,低層的函數甚至可以不看。)
2.2.3 函數到了多長,Smell纔算Bad?(寫多長的函數才比較合適?)
1. 李維說:5行。(李維大家認識麼?臺灣IT界著名散文家,與侯捷齊名。不得不承認,此處我斷章取義了,他說“5行”,是有一定語境的,他是想說“很短”的意思,建議大家不要追究他說的是5還是6。)
2. Martin Fowler說:長度不是問題,關鍵在於函數名稱和函數本體之間的語義距離。(Martin Fowler大家認識麼?《重構》《UML精粹》《分析模式》《企業應用架構模式》等經典著作的作者。後面,Martin還是,如果提煉動作可以強化代碼的清晰度,那就去做,就算函數名稱比提煉出來的代碼還長也無所謂。什麼叫做“函數名稱與函數本體之間的語義距離”?函數名稱要能概括函數體的動作,我引用的這句話沒有能表達Martin的所有意思。)
3. 王振宇說:應該很短、可以較長,只要函數是在做一件從語義上無法再次拆解的事。(王振宇大家認識麼?注意到沒有,我們認爲這句話是對Martin上一句話的補充,在函數體的組織方式上做了要求。簡單說就是,一個函數做一件事兒。Extract Method中的那個例子。)
2.3 Large Class(過大類)
(一個例子)
1. “過大類”有什麼不好?(難維護、容易出現重複代碼。)
2. “過大類”的常見情況:
a) 本應該是針對不同類的操作放到一個類中。(比如,某些類本應拆出一些小的零件類,該類與零件類之間可能是關聯、依賴關係,但沒有這麼做,而是所有代碼放在一個類中。)
b) 大量靜態方法放在“*Comm”類中。
i. 現象:大量靜態方法放在“Comm”類中。很像C的函數庫。(這個現象常見麼?好不好?)
ii. 評述:這是一種使用面嚮對象語言編寫面向過程代碼的嘗試。我個人覺得這類嘗試是一種倒退,拒絕面向對象所帶來的所有特性。沒有很好地封裝,程序的結構化不好;使用方與之的依賴是靜態的;沒有可插入性。
iii. 措施:解決這個問題的做法是,按照操作實施在哪個對象身上來把操作規劃到對象所在的類裏面;如果保持靜態方法不變,也不要所有方法放到一個類中,最好按照語義來劃分到合適的類。JDK類庫提供了那麼多方法,很少出現靜態函數庫的現象。當然,也不是說不存在,比如Math、ArrayList等類,不過至少他們在語義上分得很清晰。一個例子,比如,String的substring方法,很可能被一些程序員設計成public static,因爲他們可能覺得無法把這個方法歸屬到哪個類中,於是放到“Comm”類中。(大家體會一下?是不是這麼個事兒。)
iv. 疑問:那不是想調用某個方法的時候就要new一個實例(性能問題!)?首先,這又是面向過程的思維方式,想的是過程、調用,而不是對象、依賴、關聯。其次,輕量級的對象,創建、回收成本很低。我曾經對一些不同算法、策略從性能角度做過相應的比較試驗,通常,執行次數在10w-100w以上量級時,纔有差別。我使用面向對象的做法,可以明顯地獲得更優的可維護性(可讀、可擴展、可改、可插入、可重用),而且面向對象本身不會造成什麼性能問題。當然,具體情況要具體分析,如有明顯的性能隱患,最好能夠做一個簡單的試驗,用數據來說話。做個類比,說某些政客在很多場合的潛臺詞中認爲,民主、自由會破壞安定的大好局面,顯然不能夠讓人信服;同樣,說面向對象增加了對象的創建、銷燬成本,會影響性能,影響軟件系統的穩定局面,也是不能夠讓人信服的。於是在編程時儘可能地使用靜態方法,這種做法,不可取。
3. 類長到多大才算“Large”?類,應該較小、可以較大,只要該類從語義上無法再次拆解。(“發動機”類,可以包含對“螺絲”類的引用(關聯),但不要把“螺絲”類的操作也放到“發動機”類中來實現。)
2.4 Long Parameter List(過長參數列)
1. “過長參數列”有什麼不好?(難讀、難用、難記,還有一點,無法獲得對象所帶來的優勢。比如,參數之間的約束關係沒有得到很好地封裝。例如,startTime/endTime,他倆作爲參數來講,可能不算長,這裏僅做示例來說事兒。這對錶示時間範圍的參數可能多處使用,在沒有包裝成對象的時候,如果要保證“startTime < endTime”這個約束,就需要所有用到這對參數的地方都做判斷;包裝成對象,情況就好多了,比如叫做TimeRange,在類的構造函數中可以做這個判斷。顯然,使用對象,把變化封裝得好一些。)
2. 想一下,JAVA類庫的函數,比起C類庫的函數,傳遞的參數是不是大都短很多?(應該是。這體現了面向對象的優勢。)
3. “參數太多”這個Smell如何去除?
a) Introduce Parameter Object,無非是把多個參數封裝成一個對象。
2.5 Divergent Change(發散式變化)
1. 什麼是“發散式變化”?
a) “某一個類受到多種變化的影響”,A/B/C/D……多種功能變化的時候它都需要修改。
2. 爲什麼會造成“發散式變化”?哪兒沒弄好?
a) 大致是由於這個類擔負了多項任務,太操心了,不該他做的事兒也來做,越俎代庖。很可能需要再拆分幾個類出來,把變化封裝得更細。
3. 歷史教訓(反面教材)(以前我寫配置MAF端代碼的時候,寫過一個P_Unit類,他處理所有BSC單元的邏輯,但各種單板的邏輯是不一樣的,於是DTB改邏輯的時候要修改P_Unit、ABPM改的時候要修改P_Unit、IPCF、UPCF、GCM……所有具有特殊邏輯的單板修改功能的時候,他都要修改,甚至HDLC/UID等邏輯修改的時候P_Unit都要改。顯然該類管得太多了。後來,我看了一本書,翻然悔悟,痛下把代碼決心做了重構。其實早在03年,徐峯(據說徐峯要離開公司,這麼牛的人離開了對我們整個OMC損失很大,我在這裏提一下他的名字,簡陋地送別一下。)做配置CAF的時候建議我針對每種有特殊邏輯的板子弄一個類,我完全不以爲然。顯然,當時沒有理解“封裝變化”這四個字。)
2.6 Shotgun Surgery(霰彈式修改)
1. 什麼是“霰彈式修改”?
a) “一個變化引發多個類的修改”,完成某個需求的時候,A/B/C/D……多個類都需要修改。
2. 爲什麼會造成“霰彈式修改”?哪兒沒弄好?
a) 大致是多個類之間的耦合太嚴重。很可能是類沒有規劃好,沒有把變化封裝得足夠令人滿意。
3. 一個插曲:記得此前討論這個Bad Smell的時候,嚴鈞認爲,去掉這個Bad Smell不好強求,而且舉出Abstract Factory模式作爲例證。也有道理。我在這一點上是這麼認爲的:我們要清楚的認識到我們努力的方向,Abstract Factory模式同樣不完美,它沒有滿足Open-Close原則。我們可以在某些條件(包括技術條件)受限的時候寫出不完美的代碼,但一定要知道它是不完美的。
a) Factory Method模式(工廠方法)代替Abstract Factory來說事兒。
 
每增加一種Produce的實現類,就要同時增加一個對應該類的Creator類。當時嚴鈞可能說的是Abstract Factory模式,我用Factory Method模式來說事兒,因爲他簡單些,但同樣可以說明問題。
b) Open-Close原則
軟件實體應該對擴展開放,對修改關閉。Open-Close原則是一個願景性質的原則,如果系統能夠達到Open-Close原則描述的情形就比較理想了,對擴展開放、對修改關閉,即,不修改原有代碼即可完成對系統的擴展。系統可以獲得最大可能的穩定性,加功能的時候舊有代碼不修改,當然不會帶入BUG。
? 玉帝招安美猴王的故事
齊天大聖美猴王,想當初可是功夫了得,從東海龍王那兒拿了根棍兒,大鬧天宮……叫囂得不行(後來怎麼不靈了,一根燈草、一根頭繩、一條看大門的獅子狗都整不過),喊出來一些革命口號:“皇帝輪流做,明年到我家”,“只教他搬出去,將天宮讓與我!”。有一些農民起義領袖的風範。
太白金星給玉皇大帝打了個報告出主意:“把他宣來上界……與他籍名在籙……一則不動衆勞師,二則收仙有道也”。
玉皇大帝遵循Open-Close原則招安了美猴王。不動衆勞師,不破壞天規,是關閉對已有系統的修改,不修改,是Closed。收仙有道,是對已有系統的擴展,可擴展,是Open。
 
同時應用了依賴倒換原則,合成/聚合複用原則,以後有機會給大家講講面向對象的設計原則。
4. 講回霰彈式修改這個smell,很多程序在接手時,前輩一再囑咐,改什麼功能的時候,一定要注意,什麼什麼……一堆地方必須同時修改,要細心不要漏了……這很可能是設計水平的問題給維護造成的難度。其實如果程序設計得好,此後的工作將愉快很多。(插播廣告)我記得,剛看到斯諾克比賽在電視上轉播的時候(那時候還小,初中吧,還沒見過真的斯諾克檯球桌),很不屑,覺得他們大部分時候在打一些比較近距離的球,最多半張臺子距離吧,我甚至盲目自信,感覺那些球我都能打進,電視上的人有什麼了不起。其實大家都知道,母球走位是難度更大、更重要的工作(要經過全盤性思考的),走位走好了,下一杆就好打;就好比做軟件,程序結構設計得好,維護就更容易。
2.7 Data Clumps(數據泥團)
1. 什麼是“數據泥團”?
a) 某些數據項經常黏在一起行動,稱之爲“數據泥團”。時間長了就應該考慮是不是該把他們封裝到一個對象中,來封裝這個泥團所可能具有的邏輯。(比如一堆表示定位信息的字段,system、subsystem、unit、rack、shelf、slot……總是一起出現。)(這個Bad Smell在在很多時候與Long Parameter List,是一樣的,但Data Clumps的涵蓋範圍比Long Parameter List要大一些,比如,某些類的Field,可能沒有當作參數來傳遞,但是總是黏在一起,也可能出現數據之間的邏輯,於是也需要綁成一個對象,來做封裝。)
2. 如何判斷是否屬於“數據泥團”?
a) 刪掉這些數據項中的其中之一項,其他數據有沒有因此失去意義?(比如startTime/ endTime,就是成對錶示時間範圍的,去掉其中一個,另一個失去意義。)
2.8 Switch Statements(Switch驚悚現身)
(驚悚現身,很像香港翻譯好萊塢電影片名的風格是麼?)
1. Switch語句有什麼不好?
a) 容易形成“長函數”(比較容易理解)
b) 容易形成“霰彈式修改”
2. 如何替換掉Switch語句?
a) 多態(使用Pet的例子的第三個版本來說明)
3. 是不是使用多態可以去掉所有Switch語句?
a) 不是。比如,根據消息號,分發把消息分發到相應的處理函數(處理類)來處理。(原因是某些情況下,調用端無法動態創建確切(子類的)實例,於是依然需要分發過程。即,需要Switch語句分發、或者“配置文件+反射”的方式分發。)
4. 對Switch語句有什麼要求?
a) Switch語句可以存在,但每個case的處理語句不應超過2-3行。
2.9 Comments(過多的註釋)
(首先,註釋本身沒有錯,很多時候註釋是必須存在的。但,註釋過多,就是壞味道了。)
1. Why?爲什麼?
a) 過多的註釋,是降低代碼可讀性的幫兇。(如果,代碼只有通過大量註釋才能被理解,那麼說明代碼的可讀性不好。事實上,很多文章也就此有些說法:代碼要寫得“自解釋能力強”、自己解釋自己;代碼就是文檔。這就要求,類、方法的編寫要清爽,類名、方法名、變量名要起得好。)
2. How?如何寫好註釋?(Why?How?想起那個關於兩個漁夫和一個美人魚的葷笑話,由於有未成年人士在場,我不便當衆詳細講。)
a) 寫“why”。(註釋應該寫代碼的編寫思路,特別是某些地方沒有按常理出牌,要寫註釋來說明。比如,對數組做for循環遍歷,邊界一般是數組的length,如果某一次出於某種特殊考慮,沒這麼做,就需要註釋說明。)
b) 不寫“what”。(註釋不要寫代碼是幹什麼的,“what”這樣的信息應該儘量包含在類名、方法名、變量名中。)
c) 不寫充數註釋。(不要爲了寫註釋而寫註釋,不要往豬肉裏注水,雖然沒什麼大礙,但終歸是沒品味的做法。比如,“String a = null;//創建一個String實例。”看到這樣的註釋,我胃口都不舒服。雖然部門有註釋比例的要求,但像我們這樣的高級程序員、高級工程師,還是不要充數用的註釋。)
3 Refactoring
3.1 Extract Method
void printOwing() {
  printBanner();
  //print details
  System.out.println ("name:" + _name);
  System.out.println ("amount" + getOutstanding());
}
重構爲:
void printOwing() {
  printBanner();
  printDetails(getOutstanding());
}
void printDetails (double outstanding) {
  System.out.println ("name:" + _name);
  System.out.println ("amount" + outstanding);
}
(前面說了,函數應該短。重複一下帶來的好處:可讀性好,高層函數像註釋、低層函數行數少;可重用性好,比如上例中的printDetails,可能別處也能用,重構前是無法被重用的;可插入性好,子類可能寫一個新的printDetails,使用不同格式打印。)
3.1.1 抽取函數時候,參數、臨時變量如何處理
1. Replace Temp With Query 去掉臨時局部變量,代之以查詢類的方法,拆開的小函數需要此臨時變量的時候,就調用這個查詢方法。
2. Introduce Parameter Object 讓長參數列變得簡潔。
3. Replace Method with Method Object 去掉多個參數、局部變量。爲待重構的大函數創建一個對象,這樣,所有方法內的臨時變量就變成對象的field,於是大函數拆開的所有小函數就共享這些field,不必再使用參數傳遞。
3.1.2 起名字很重要!名字應該:(此處的名字包括函數、類等)
1. 清晰、恰當。表達的信息涵蓋函數的所作所爲。
a) 當一個函數名字爲了涵蓋函數所爲“必須”起成“do1stThingAndDo2ndThing”的時候,就有必要實施Extract Method來抽取函數了。
b) 一個OMC代碼中的例子,某函數叫做checkParameter,但函數體中除了檢查參數之外,還“順便”爲幾個類屬性賦值,雖然此函數很短、很超值,但我們認爲他的命名是不恰當的,甚至他的函數設計也是不恰當的,一個函數要幹單純的一件事兒,函數內部從語義上無法再次分解。
2. 儘量簡短、可以較長。但應該首先滿足上一條要求。
a) compareToIgnoreCase(String類的方法)、getDisplayLanguage(Locale類的方法)、getTotalFrequentRenterPoints(《重構》書中的示例代碼),這些函數名長不長?(重要的是把信息表述清楚,名字長一點沒關係。)
3.2 Replace Temp with Query
double basePrice = _quantity * _itemPrice;
if (basePrice > 1000)
  return basePrice * 0.95;
else
  return basePrice * 0.98;
重構爲:
if (basePrice() > 1000)
    return basePrice() * 0.95;
  else
    return basePrice() * 0.98;

double basePrice() {
  return _quantity * _itemPrice;
}
(例子很容易理解,basePrice是臨時變量,臨時變量的問題在於:它們是暫時的,而且只能在所屬函數內使用。由於臨時變量只有在所屬函數內纔可見,所以它們會驅使你寫出更長的函數,因爲只有這樣你才能訪問到想要訪問的臨時變量。如果把臨時變量替換爲一個查詢式(query method),那麼同一個class中的所有函數都將可以獲得這份信息。爲拆解大函數提供了方便。)
3.2.1 一個藉助Replace Temp with Query來提煉函數的例子
double getPrice() {
    int basePrice = _quantity * _itemPrice;
    double discountFactor;
    If (basePrice >1000) discountFactor = 0.95;
    else basePrice = 0.98;
    return basePrice * discountFactor;
}
重構爲:
double getPrice() {
    return basePrice() * discountFactor();
}
private int basePrice() {
    return _quantity * _itemPrice;
}
Private double discountFactor() {
    If (basePrice() >1000) return 0.95;
    else return 0.98;
}
(重構前,在getPrice方法中,先計算基礎價格、再計算折扣因子、再計算最終價格,做了語義上可以再次拆解的三件事兒,不符合“函數只做一件事兒”的要求,於是使用Extract Method方法來重構。藉助Replace Temp with Query重構方法,將臨時變量basePrice、discountFactor用相應的查詢函數來替代。
需要指出的是,此次重構,查詢函數basePrice被調用了兩次,損失的一點點性能可以忽略,我們認爲這樣做是值得的。)
3.3 Split Temporary Variable
double temp = 2 * (_height + _width);
System.out.println (temp);
temp = _height * _width;
System.out.println (temp);
重構爲:
final double perimeter = 2 * (_height + _width);
System.out.println (perimeter);
final double area = _height * _width;
System.out.println (area);
(如果臨時變量被賦值超過一次就意味它們在函數中承擔了一個以上的責任(循環變量等用途除外)。
例子中,臨時變量temp開始被用來記錄矩形周長,後來被用來記錄矩形面積。應該拆解爲多個臨時變量,否則:
1. 影響代碼可讀性。(多用途臨時變量通常無法獲得合適的命名)
2. 增加代碼出錯機會。(程序某處,可能都記不清這個臨時變量現在是記錄什麼數值的)
實際操作中,推薦使用final來限定臨時變量被賦值次數。)
3.4 Remove Assignments to Parameters
int discount (int inputVal, int quantity, int yearToDate) {
 if (inputVal > 50) inputVal -= 2;
重構爲:
int discount (int inputVal, int quantity, int yearToDate) {
 int result = inputVal;
 if (inputVal > 50) result -= 2;
1. 不要對參數賦值
void nextDate(Date arg) {
    arg.setDate(arg.getDate() + 1);
}
void nextDate(Date arg) {
    arg = new Date(arg.getYear(), arg.getMonth(), arg.getDate() + 1);
}
上面兩個函數的寫法,哪個是對參數賦值了的?哪個會起到應有的作用?
2. Java是pass by value(傳值)的
a) 傳進函數體中的參數,是調用語句那個傳入參數在內存中的一份拷貝
b) 函數體內對參數的再賦值不會影響調用方的參數原始值(比如,修改int等基本類型參數的數值、修改Object等對象引用的指向)
c) Java中,對參數的再次賦值是一種純粹降低程序清晰程度的做法
3.5 Replace Method with Method Object
class Order...
      double price() {
 double primaryBasePrice;
 double secondaryBasePrice;
 double tertiaryBasePrice;
 // long computation;
 ...
      }

重構爲:
 
(一個大的函數,提煉出一個專門實現這個函數功能的類。
比如getMoney方法可能就是提煉出一個MoneyGetter類。此處是price方法提煉出PriceCalculator類。
這樣做的理由是,price可能是個很大的函數,我們爲了獲得短函數的優勢(前面說過的,可重用、可讀、可插入等),想利用Extract Method抽取出多個短函數,但每個短函數可能都需要primaryBasePrice、secondaryBasePrice、tertiaryBasePrice等幾個臨時變量,把它們都作爲參數傳遞顯然太笨了。而把這個大函數提煉成類,這些臨時變量就變成了類的Field,在類中是共享的,這樣,抽取出來的小函數之間就可以不需要傳遞參數,就很容易實現Extract Method這個重構過程。)
3.5.1 如此這般之後,類是不是太多了?
面向對象的套路,玩的就是類。函數不嫌多,爲什麼嫌類多?多個風馬牛不相及的函數雜居在一個類中(能夠容忍麼?),爲什麼不多弄幾個類把它們各自封裝?類,對應了現實世界的有機無機物種,把物種分得足夠細緻,世界纔得到了完美地描述。
3.6 Replace Array with Object
String[] person = new String[3];
person[0] = “Robert De Niro";
person[1] = “60";
person[2] = “Actor”;
重構爲:
Person robert = new Person();
robert.setName(" Robert De Niro");
robert.setAge(“60");
robert.setProfession(“Actor”);
1. 數組應該容納一組相似的對象,用戶很難記住“數組第一個元素是人名、第二個元素是年齡”這樣的約定。
a) 用註釋來保證這種約定麼?
b) 使用數組是出於效率考慮麼?(擡槓?)
2. 同理,能夠恰當地利用既有數據結構把接口約束得緊一些(更貼切、更嚴絲合縫),對大家都有好處。
a) 比如,能夠確定是一組Person類實例,就要用Person[]來裝,而不用Set、Map這樣的“廣口”容器。(拒絕“私下、口頭約定,註釋”等靠不住的協議方式所帶來的弊端。)
3.7 Encapsulate Field
public String _name;
重構爲:
private String _name;
public String getName() {
    return _name;
}
public void setName(String arg) {
    _name = arg;
}
強調封裝:
一般來講,任何時候都不要將類field聲明爲public(常量除外)。數據和使用數據的行爲被集中在一起,一旦情況發生變化,代碼的修改比較容易,因爲需要修改的代碼都集中在同一塊地方,而不是星羅棋佈地散落在整個程序中。
3.8 Replace Magic Number with Symbolic Constant
double potentialEnergy(double mass, double height) {
 return mass * 9.81 * height;
}
重構爲:
double potentialEnergy(double mass, double height) {
 return mass * GRAVITATIONAL_CONSTANT * height;
}
static final double GRAVITATIONAL_CONSTANT = 9.81;
宏值定義代替散落在代碼中的“魔術數”,沒什麼好說的。
3.9 Encapsulate Collection
 
重構爲:
 
依然是強調封裝:
1. 類內高內聚:數據和對數據的操作緊密結合在一起,對數據的實施操作比較容易。(比如,餐廳管理系統中的某個類,dishes(Vector類型)作爲類的field,裝載點菜時被點中的菜目,如果想統計一下哪些菜受歡迎,對於按Encapsulate Collection設計的類就比較容易操作,add方法中做一些手腳即可(分類累加)。)
2. 類之間松耦合:內部數據結構不要暴露的外界,外界也不需要關心。(這樣,即便你把內部數據由array換成Vector,外部都不需要知道。)
3.10 Replace Type Code with Class
 重構爲:
3.10.1 Why?Type Code有什麼不好?
1. Type Code會降低可讀性。
a) 在定義的地方可能看不出來(定義時使用宏值,可讀性挺好),但在使用的地方就會顯現問題。上例中,比如有個方法getCharacter獲得血型對應的性格描述,參數是血型,使用Type Code時,參數類型爲int,重構後,參數類型爲BloodGroup,顯然後者的可讀性好。
2. 使用Type Code失去了使用對象所擁有的獨立擴展的機會。
a) 像這個例子,Type Code很容易有它自己的行爲,比如根據血型得到性格描述、得到ABO溶血癥可能性、判斷血型之間的輸血匹配可能……於是將其抽取成類是比較好的做法。(還是封裝!)
3.10.2 插科打諢,Meilir Page-Jones講的故事
(故事是講面向對象的,面向對象的主要特徵有哪些?封裝、繼承、多態)
Meilir Page-Jones在《UML面向對象設計基礎》(個人認爲此書堪稱經典)一書中編了一個故事:
軟件界在“面向對象”的定義上,一度很難達成一致。我開始步入面向對象領域時,決定澄清一下“面向對象”的定義。
我把數十位面向對象的老前輩關在一個沒有食物和水的房間裏。我告訴他們只有當他們的定義達成一致的意見,並且可以在軟件世界發佈時才允許他們出去。在一小時的喧譁過後,房內一片安靜,老前輩們背靠背誰也不理誰了,陷入了僵局。此時,蹦出來一位組織者,讓每個人都列出他們認爲在面向對象世界中不可缺少的特性,大家同意。一通羅列,每個人都列出了三個五個、十個八個。
此時,剛纔蹦出來那位組織者又蹦出來開始講話,說,現在我們大致有兩種做法:一種是建立一個長列表,該列表是每個人列表的並集;另一種是建立一個短列表,該列表是每個人列表的交集。大家選擇了後者,產生了一個短列表,該列表中的特性在每個人列表中都有。這個列表確實很短,短到只有一個詞,“封裝”。
一堆廢話告訴大家一個道理,封裝,是面向對象最爲重要的特性,封裝好了,才能做到所謂的高內聚、松耦合。獲得面向對象思想許諾的種種優勢。
3.11 Replace Type Code with Subclass
(跟前面寵物店的例子是不是很像?)
 重構爲:
3.12 Replace Type Code with State/Strategy
 重構爲:
這個就厲害了!清晰地展示了“合成/聚合複用原則”。
上面例子,將Engineer和Salesman弄成並列的子類,是存在問題的。(什麼問題?)
1. Salesman明確地從Employee繼承,那麼就無法再從Male、Newcomer等類繼承來獲得他們的特性。
2. Salesman的實例被new出來之後,他可能轉崗做研發,想變成Engineer,無法實現。
3.12.1 什麼是合成/聚合複用原則
1. 要儘量使用合成/聚合,儘量不要使用繼承。
2. 從複用角度來說:“合成/聚合複用”比“繼承”複用靈活。前者是動態複用(因而具有可插入性)、後者是靜態複用(編譯時就固定了複用關係),而且後者的複用有“不支持多重繼承”的限制。
3.13 Decompose Conditional
if (date.before (SUMMER_START) || date.after(SUMMER_END))
    charge = quantity * _winterRate + _winterServiceCharge;
else charge = quantity * _summerRate;
重構爲:
if (notSummer(date))
    charge = winterCharge(quantity);
else charge = summerCharge (quantity);
Extract Method在條件判斷語句段中的應用。
3.14 Consolidate Conditional Expression
double disabilityAmount() {
 if (_seniority < 2) return 0;
 if (_monthsDisabled > 12) return 0;
 if (_isPartTime) return 0;
 // compute the disability amount
重構爲:
double disabilityAmount() {
 if (isNotEligableForDisability()) return 0;
 // compute the disability amount
這條比較雕蟲小技,可視具體情況參考實施。
3.15 Consolidate Duplicate Conditional Fragments
if (isSpecialDeal()) {
  total = price * 0.95;
  send();
}
else {
  total = price * 0.98;
  send();
}
重構爲:
if (isSpecialDeal()) {
  total = price * 0.95;
}
else {
  total = price * 0.98;
}
send();
雖然這條也比較雕蟲小技,但前面這樣的代碼確實也有人寫得出來。
3.16 Remove Control Flag
set done to false
while not done
    if (condition)
        do something
        set done to true
    next step of loop
Control Flag爲什麼不好?
影響可讀性,程序看起來比較繞。
3.16.1 一個例子
void checkSecurity(String[] people) {
  String found = "";
  for (int i = 0; i < people.length; i++) {
    if (found.equals("")) {
      if (people[i].equals ("Don")){
        sendAlert();
        found = "Don";
      }
      if (people[i].equals ("John")){
        sendAlert(); 
        found = "John";
      }
    }
  }
  someLaterCode(found);
}
無論找到Don還是John都退出循環做其他事兒。注意:這裏使用了標誌found。
重構爲:
void checkSecurity(String[] people) {
  String found = foundMiscreant(people);
  someLaterCode(found);
}
String foundMiscreant(String[] people){
  for (int i = 0; i < people.length; i++) {
    if (people[i].equals ("Don")){
      sendAlert();
      return "Don";
    }
    if (people[i].equals ("John")){
      sendAlert();
      return "John";
    }
  }
  return "";
}
重構之後,去掉了標誌,增加了函數的出口,同時增加了程序的可讀性。
3.17 Replace Nested Conditional with Guard Clauses
(衛語句:某些條件判斷爲真時,立即從函數返回。這樣的判斷就應該首先、單獨進行,把這種單獨檢查稱之爲“衛語句”。Guard Clauses。是Kent Beck給起的名字,Kent Beck是TDD、XP的第一倡導者。)
double getPayAmount() {
  double result;
  if (_isDead) result = deadAmount(); 
  else {
    if (_isSeparated) result = separatedAmount();
    else {
      if (_isRetired) result = retiredAmount();
      else result = normalPayAmount();
    };
  }
  return result;
};
重構爲:
double getPayAmount() {
  if (_isDead) return deadAmount();
  if (_isSeparated) return separatedAmount();
  if (_isRetired) return retiredAmount();
  return normalPayAmount();
};
(好處是明顯的,可以減少if/else嵌套的數目,從而強烈地提高程序可讀性。比較重要的是,需要習慣“函數有多個出口”這種做法。)
3.18 Replace Conditional with Polymorphism
double getSpeed() {
    switch (_type) {
        case EUROPEAN:
            return getBaseSpeed();
        case AFRICAN:
            return getBaseSpeed() - getLoadFactor() *
                _numberOfCoconuts;
        case NORWEGIAN_BLUE:
            return (_isNailed) ? 0 : getBaseSpeed(_voltage);
    }
    throw new RuntimeException ("Should be unreachable");
}
重構爲:
 
(跟講Switch那個Bad Smell時舉過的例子,基本一樣。)
3.19 Introduce Parameter Object
 重構爲:
3.20 Replace Error Code with Exception
int withdraw(int amount) {
    if (amount > _balance) {
        return -1;
    else {
        _balance -= amount;
        return 0;
    }
}
重構爲:
void withdraw(int amount) throws BalanceException {
    if (amount > _balance) throw new BalanceException();
    _balance -= amount;
}
Why?
1. 提高代碼可讀性。
2. 方便調用方。調用方可以不再判斷返回的Error Code,而只是把異常直接拋出去,待最終接受方處理。比如,類C/S的結構,服務端代碼在所有環節都可以直接透傳Exception,簡化處理流程。Exception最終由客戶端統一處理。
(注:有些地方無法完全取代Error Code,比如前臺回來的消息處理,onMessage函數。)
3.21 Form Template Method
 
重構爲:
 
(計算金額的步驟,各個子類都一樣,即算得基礎金額、再算得繳稅幾多,然後二者想加。於是把這個邏輯上升到父類。子類僅負責其中的子步驟。)
(下面再看一個例子。說明一下Template Method是怎麼回事兒。
所有的工作人員,一天的活動,步驟都差不多,無外乎,喫早餐、趕到工作地點……,但不同子類對於各個步驟的實現是不同的。
比如,上午工作,公務員可能是:編寫官樣文章、聊天、按照規定合理地拒絕刁蠻市民的無理要求……;我司員工可能是:編碼、調試、上網看新聞、到匿名論壇發牢騷……;農民工兄弟可能是:扛包、抽菸休息一會、扛包、喝水休息一會……。
比如,喫午餐,公務員是:免費、或者象徵性收費的豪華自助餐;我司員工是:別無選擇的XLX;農民工兄弟是:其他農民工在廉價出租棚子裏製作的不乾不淨吃了可能得病的方便盒飯。)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章