那有什么天生如此,只是我們天天堅持。
本篇文章主要講解 《重構---改善既有代碼的設計》 這本書中的 第九章簡化條件表達式中 的知識點,
Decompose Conditional(分解條件表達式)問題:你有一個復雜的條件(if、then、else) 語句
解決:從if、then、else三個段落中分別提煉出獨立函數
//重構前 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);動機
將條件分支的代碼分解成多個獨立函數,根據每個小塊代碼的用途,為分解而得的新函數命名,并將原函數中對應的代碼改為調用新建函數,可以更清楚地表達自己的意圖。
做法將if段落提煉出來,構成一個獨立函數
將then段落和else段落都提煉出來,各自構成一個獨立函數
Consolidate Conditional Expression(合并條件表達式)問題:如果有一系列條件測試,都得到相同結果。
解決:將這些測試合并為一個條件表達式,并將這個條件表達式提煉成為一個獨立函數
//重構前 double disabilityAmount(){ if(_seniority < 2) return 0; if(_monthsDisabled > 12) return 0; if(_isPartTime) return 0; }
//重構后 double disabilityAmount(){ if(isNotEligableForDisability()) return 0; }動機
如果一串條件檢查:檢查條件各不相同,最終行為卻一致,就應該將它們合并為一個條件表達式,之所以要合并條件代碼,有兩個重要原因,
首先,合并后的條件代碼用意更清晰,其次,這項重構往往可以為使用Extract Method(提煉函數)做好準備
確定這些條件語句都沒有副作用
使用適當的邏輯操作符,將一系列相關條件表達式合并為一個
編譯,測試
對合并后的條件表達式實施Extract Method
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();動機
有助于清楚地表明哪些東西隨條件的變化而變化、哪些東西保持不變
做法鑒別出”執(zhí)行方式不隨條件變化而變化”的代碼‘
如果這些共通代碼位于條件表達式起始處,就將它移到條件表達式之前
如果這些共通代碼位于條件表達式尾端,就將它移到條件表達式之后
如果這些共通代碼位于條件表達式中段,就需要觀察共通代碼之前或之后的代碼是否改變了什么東西,如果的確有所改變,應該首先將共通代碼向前或向后移動,移至條件表達式的起始處或尾端,再以前面所受的辦法來處理
如果共通代碼不止一條語句,應該先使用Extract Method(提煉函數)將共通代碼提煉到一個獨立函數中,再以前面所說的辦法來處理
范例//重構前 if (isSpecialDeal()) { total = price * 0.95; send(); } else { total = price * 0.98; send(); }
由于條件式的兩個分支都執(zhí)行了 send() 函數,所以我應該將send() 移到條件式的外圍:
if (isSpecialDeal()) total = price * 0.95; else total = price * 0.98; send();
我們也可以使用同樣的手法來對待異常(exceptions)。如果在try 區(qū)段內「可能引發(fā)異?!沟恼Z句之后,以及所有catch 區(qū)段之內,都重復執(zhí)行了同一段代碼,我就 可以將這段重復代碼移到final 區(qū)段。
Remove Control Flag(移除控制標記)問題:在一系列布爾表達式中,某個變量帶有”控制標記”的作用
解決:以break語句或return語句取代控制標記
動機在一系列條件表達式中,你常常會用到[用以何時停止條件檢查]的控制標記。
set done to false while not done if (condition) //do something //set done to true next step of loop
用break語句和continue語句跳出復雜的條件語句
做法找出讓你跳出這段邏輯的控制標記值
找出對標記變量賦值的語句,代以恰當的break語句或continue語句
每次替換后,編譯并測試
范例 :以break取代簡單的控制標記//重構前 void checkSecurity(String[] people){ boolean found = false; for(int i = 0; i < people.length; i++){ if(!found){ if(people[i].equals("Don")){ sendAlert(); found = true; } if(people[i].equals("John")){ sendAlert(); found = true; } } } }
=>
//重構后 void checkSecurity(String[] people){ for(int i = 0; i < people.length; i++){ if(people[i].equals("Don")){ sendAlert(); break; } if(people[i].equals("John")){ sendAlert(); break; } } }范例 :以return返回控制標記
//重構前 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); }
//重構后 void checkSecurity(String[] people){ String found = foundMiscreant(people); someLaterCode(found); } String foundMiscreant(String[] people){ String found = ""; for(int i = 0; i < people.length; i++){ if(found.equals("")){ if(people[i].equals("Don")){ sendAlert(); return "Don"; } if(people[i].equals("John")){ sendAlert(); return "John"; } } } return ""; }Replace Nested Conditional with Guard Clauses(以衛(wèi)語句取代嵌套條件表達式)
問題:函數中的條件邏輯使人難以看清正常的執(zhí)行路徑
解決:使用衛(wèi)語句表現(xiàn)所有特殊情況(啟哥備注:可以減少嵌套)
//重構前 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(); }動機
條件表達式通常有兩種表現(xiàn)形式:
第一種是:所有分支都屬于正常行為
第二種是:條件表達式提供的答案中只有一種是正常行為,其他都是不常見的情況
如果某個條件極其罕見,就應該多帶帶檢查該條件,并在該條件為真時立刻從函數中返回,這樣的多帶帶檢查常常被稱為”衛(wèi)語句”
做法對于每個檢查,放進一個衛(wèi)語句,衛(wèi)語句要么從函數中返回,要么就拋出一個異常
每次將條件檢查替換成衛(wèi)語句后,編譯并測試
范例:將條件反轉//重構前 public double getAdjustedCapital(){ double result = 0.0; if(_capital > 0.0){ if(_intRate > 0.0 && _duration > 0.0){ result = (_income / _duration) * ADJ_FACTOR; } } return result; }
//重構后 public double getAdjustedCapital(){ double result = 0.0; if(_capital <= 0.0) return 0.0; if(_intRate <= 0.0 || _duration <= 0.0) return 0.0; return (_income / _duration) * ADJ_FACTOR; }Replace Conditional with Polymorphism(以多態(tài)取代條件表達式)
問題:你手上有個條件表達式,他根據對象類型的不同而選擇不同的行為
解決:將條件表達式的每個分支放進一個子類內的覆寫函數中,然后將原始函數聲明為抽象函數
//重構前 double getSpeed(){ switch(_type){ case EUROPEAN: return getBaseSpeed(); case AFRICAN: return getBaseSpeed() - getLoadFactory() * _numberOfCoconuts; case NORWEGIAN_BLUE: return (_isNailed) ? 0 : getBaseSpeed(_voltage); } throw new RuntimeException("Should be unreachable"); }
==>
//重構后
多態(tài)最根本的好處就是:如果需要根據對象的不同類型而采取不同的行為,多態(tài)使你不必編寫明顯的條件表達式
做法如果要處理的條件表達式是一個更大函數中的一部分,首先對條件表達式進行分析,然后使用Extract Method將它提煉到一個獨立函數中
如果有必要,使用Move Method(搬移函數)將條件表達式放置到繼承結構的頂端
任選一個子類,在其中建立一個函數,使之覆寫超類中容納條件表達式的那個函數,將與該子類相關的條件表達式分支復制到新建函數中,并對它進行適當調整
編譯,測試
在超類中刪除條件表達式內被復制了的分支
編譯,測試
針對條件表達式的每個分支,重復上述過程,直到所有分支都被移動子類內函數為止
將超類之中容乃條件表達式的函數聲明為抽象函數
范例請允許我繼續(xù)使用「員工與薪資」這個簡單而又乏味的例子。
繼承構:
//重構前 class Employee... int payAmount(int type){ switch(type){ case Employee.ENGINEER: return _monthlySalary; case Employee.SALESMAN: return _monthlySalary + _commission; case Employee.MANAGER: return _monthlySalary + _bonus; default: throw new IllegalArgumentException("Incorrect type code value"); } } private Employee _type; int getType(){ return _type.getTypeCode() } abstract class EmployeeType... abstract int getTypeCode(); class Engineer extends EmployeeType... int getTypeCode(){ return Employee.ENGINEER: } ...and other subclasses
switch 語句已經被很好地提煉出來,因此我不必費勁再做一遍。不過我需要將它移至EmployeeType class,因為EmployeeType 才是被subclassing 的class 。
class EmployeeType... int payAmount(Employee emp) { switch (getTypeCode()) { case ENGINEER: return emp.getMonthlySalary(); case SALESMAN: return emp.getMonthlySalary() + emp.getCommission(); case MANAGER: return emp.getMonthlySalary() + emp.getBonus(); default: throw new RuntimeException("Incorrect Employee"); } }
由于我需要EmployeeType class 的數據,所以我需要將Employee 對象作為參數傳遞給payAmount()。這些數據中的一部分也許可以移到EmployeeType class 來,但那是另一項重構需要關心的問題了。
調整代碼,使之通過編譯,然后我修改Employee 中的payAmount() 函數,令它委托(delegate,轉調用)EmployeeType :
class Employee... int payAmount() { return _type.payAmount(this); }
現(xiàn)在,我可以處理switch 語句了。這個過程有點像淘氣小男孩折磨一只昆蟲——每次掰掉它一條腿(意思就是「去掉一個分支」)。首先我把switch 語句中的"Engineer"這一分支拷貝到Engineer class:
class Engineer... int payAmount(Employee emp) { return emp.getMonthlySalary(); }
這個新函數覆寫了superclass 中的switch 語句之內那個專門處理"Engineer"的分支。我是個徧執(zhí)狂,有時我會故意在case 子句中放一個陷阱,檢查Engineer class 是否正常工作(是否被調用):
class EmployeeType... int payAmount(Employee emp) { switch (getTypeCode()) { case ENGINEER: throw new RuntimeException ("Should be being overridden"); case SALESMAN: return emp.getMonthlySalary() + emp.getCommission(); case MANAGER: return emp.getMonthlySalary() + emp.getBonus(); default: throw new RuntimeException("Incorrect Employee"); } }
接下來,我重復上述過程,直到所有分支都被去除為止:
class Salesman... int payAmount(Employee emp) { return emp.getMonthlySalary() + emp.getCommission(); } class Manager... int payAmount(Employee emp) { return emp.getMonthlySalary() + emp.getBonus(); }
然后,將superclass 的payAmount() 函數聲明為抽象函數:
class EmployeeType... abstract int payAmount(Employee emp);
a
Introduce Null Object(引入Null 對象)問題:你需要再三檢查某物是否為null value
解決:將null值替換為null對象
if (customer == null) plan = BillingPlan.basic(); else plan = customer.getPlan();
//重構后
啟哥說: 和提供一個默認的不做任何處理的空實現(xiàn)是一個意思
動機(Motivation)當實例變量的某個字段內容允許為null時,在進行操作時往往要進行非空判斷,這個工作是非常繁雜的,
所以不讓實例變量被設為null,而是插入各式各樣的空對象——它們都知道如何正確地顯示自己,這樣就可以擺脫大量過程化的代碼
空對象一定是常量,它們的任何成分都不會發(fā)生變化,因此可以使用Singleton模式來實現(xiàn)它們
做法為源類建立一個子類,使其行為就像是源類的null版本,在源類和null子類中都加上isNull()函數,前者的應該返回false,后者的應該返回true,或者建立一個nullable接口,將isNull()函數放入其中,讓源類實現(xiàn)這個接口
編譯
找出所有”索求源對象卻獲得一個null”的地方,修改這些地方,使它們改而獲得一個空對象
找出所有”將源對象與null做比較”的地方,修改這些地方,使它們調用isNull()函數
編譯,測試
找出這樣的程序點:如果對象不是null,做A動作,否則做B動作
對于每一個上述地點,在null類中覆寫A動作,使其行為和B動作相同
使用上述被覆寫的動作,然后刪除”對象是否等于null”的條件測試,編譯并測試
范例—家公用事業(yè)公司的系統(tǒng)以Site 表示地點(場所)。庭院宅等和集合公寓(apartment)都使用該公司的服務。任何時候每個地點都擁有(或說都對應于)一個顧客,顧客信息以Customer 表示:
//重構前 class Site... Customer getCustomer(){ return _customer; } Customer _customer; class Customer... public String getName(){...} public BillingPlan getPlan(){...} public PaymentHistory getHistory(){...} public class PaymentHistory... int getWeesDelingquentInLastYear() Customer customer = site.getCustomer(); BillingPlan plan; if(customer == null) plan = BillingPlan.basic(); else plan = customer.getPlan(); ...
//重構后 class NullCustomer extens Customer{ public boolean isNull(){ return true; } } class Customer... public boolean isNull(){ return false; } static Customer new Null(){ return new NullCustomer(); } class Site... Customer getCustomer(){ return (_customer == null) ? Customer.newNull() : _customer; } Customer customer = site.getCustomer(); BillingPlan plan; if(customer.isNull()) plan = BillingPlan.basic(); else plan = customer.getPlan();Introduce Assertion(引入斷言)
問題:如果某一段代碼需要對程序狀態(tài)做出某種假設
解決:以斷言明確表現(xiàn)這種假設
//重構前 double getExpenseLimit(){ return (_expenseLimit != NULL_EXPENSE) ? _expenseLimit:_primaryProject.getMemberExpenseLimit(); }
//重構后 double getExpenseLimit(){ Assert.isTrue(_expenseLimit != NULL_EXPENSE || _primaryProject != null); return (_expenseLimit != NULL_EXPENSE) ? _expenseLimit:_primaryProject.getMemberExpenseLimit(); }動機
常常會有這樣一段代碼:只有當某個條件為真時,該段代碼才能正常運行,這時應該使用斷言,把不符合條件的假設標明出來
斷言可以作為交流與調試的輔助,在交流的角度上,斷言可以幫助程序閱讀者理解代碼所做的假設;在調試的角度上,斷言可以在距離bug最近的地方抓住它們
在一段邏輯中加入斷言是有好處的,因為它迫使你重新考慮這段代碼的約束條件,如果不滿足這些約束條件,程序也可以正常運行,斷言就不會帶給你任何幫助,只會把代碼變得混亂,并且有可能妨礙以后的修改
做法如果你發(fā)現(xiàn)代碼假設某個條件始終為真,就加入一個斷言明確說明這種情況
范例下面是一個簡單例子:開支(經費)限制。后勤部門的員工每個月有固定的開支限額;業(yè)務部門的員工則按照項目的開支限額來控制自己的開支。一個員工可能沒有開支額度可用,也可能沒有參與項目,但兩者總得要有一個(否則就沒有經費可用 了)。在開支限額相關程序中,上述假設總是成立的,因此:
//重構前 class Employee... private static final double NULL_EXPENSE = -1.0; private double _expenseLimit = NULL_EXPENSE; private Project _primaryProject; double getExpenseLimit() { return (_expenseLimit != NULL_EXPENSE) ? _expenseLimit: _primaryProject.getMemberExpenseLimit(); } boolean withinLimit (double expenseAmount) { return (expenseAmount <= getExpenseLimit()); }
這段代碼包含了一個明顯假設:任何員工要不就參與某個項目,要不就有個人開支限額。我們可以使用assertion 在代碼中更明確地指出這一點:
double getExpenseLimit() { Assert.isTrue (_expenseLimit != NULL_EXPENSE || _primaryProject != null); return (_expenseLimit != NULL_EXPENSE) ? _expenseLimit: _primaryProject.getMemberExpenseLimit(); }
這條assertion 不會改變程序的任何行為。另一方面,如果assertion中的條件不為真,我就會收到一個運行期異常:也許是在withinLimit() 函數中拋出一個空指針(null pointer)異常,也許是在Assert.isTrue() 函數中拋出一個運行期異常。有時assertion 可以幫助程序員找到臭蟲,因為它離出錯地點很近。但是,更多時候,assertion 的價值在于:幫助程序員理解代碼正確運行的必要條件。
我常對assertion 中的條件式使用Extract Method ,也許是為了將若干地方的重復碼提煉到同一個函數中,也許只是為了更清楚說明條件式的用途。
//重構后 double getExpenseLimit() { Assert.isTrue (Assert.ON && (_expenseLimit != NULL_EXPENSE || _primaryProject != null)); return (_expenseLimit != NULL_EXPENSE) ? _expenseLimit: _primaryProject.getMemberExpenseLimit(); }
或者是這種手法
//重構后 double getExpenseLimit() { Assert.isTrue (Assert.ON && (_expenseLimit != NULL_EXPENSE || _primaryProject != null)); return (_expenseLimit != NULL_EXPENSE) ? _expenseLimit: _primaryProject.getMemberExpenseLimit(); }
如果Assert.ON 是個常量,編譯器(譯注:而非運行期間)就會對它進行檢查; 如果它等于false ,就不再執(zhí)行條件式后半段代碼。但是,加上這條語句實在有點丑陋,所以很多程序員寧可僅僅使用Assert.isTrue() 函數,然后在項目結束前以過濾程序濾掉使用assertions 的每一行代碼(可以使用Perl 之類的語言來編寫這樣 的過濾程序)。
Assert class應該有多個函數,函數名稱應該幫助程序員理解其功用。除了isTrue() 之外,你還可以為它加上equals() 和shouldNeverReachHere() 等函數。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/67989.html
摘要:但條件邏輯也是不能忽視的分解條件表達式問題有一個復雜的條件語句。沒什么說的動機重構代碼就是錯移除控制標志問題在一系列布爾表達式中,某個變量帶有控制標記的作用方法以語句或語句取代控制標記動機控制標記大大降低了代碼可讀性。 前言 前面已經對類,方法,字段都進行了重構。貌似看起來很完整了。但條件邏輯也是不能忽視的 分解條件表達式 問題 有一個復雜的條件(if-then-else)語句。(判斷...
摘要:函數改名問題函數的名稱未能揭示函數的用途。這些人甚至會在構造函數中使用設值函數。方法將構造函數替換為工廠函數。以上所說的情況,常會在返回迭代器或集合的函數身上發(fā)生。以異常取代錯誤碼問題某個函數返回一個特定的代碼,用以表示某種錯誤情況。 Rename Method 函數改名 問題 函數的名稱未能揭示函數的用途。 方法 修改函數名稱。 動機 好的函數需要有一個清晰的函數名。保證一看就懂 A...
摘要:重構在不改變代碼的外在的行為的前提下對代碼進行修改最大限度的減少錯誤的幾率本質上,就是代碼寫好之后修改它的設計。重構可以深入理解代碼并且?guī)椭业健M瑫r重構可以減少引入的機率,方便日后擴展。平行繼承目的在于消除類之間的重復代碼。 重構 (refactoring) 在不改變代碼的外在的行為的前提下 對代碼進行修改最大限度的減少錯誤的幾率 本質上, 就是代碼寫好之后 修改它的設計。 1,書中...
摘要:為何重構重構有四大好處重構改進軟件設計如果沒有重構,程序的設計會逐漸腐敗變質。經常性的重構可以幫助維持自己該有的形態(tài)。你有一個大型函數,其中對局部變量的使用使你無法采用。將這個函數放進一個單獨對象中,如此一來局部變量就成了對象內的字段。 哪有什么天生如此,只是我們天天堅持。 -Zhiyuan 國慶抽出時間來閱讀這本從師傅那里借來的書,聽說還是程序員的必讀書籍。 關于書的高清下載連...
閱讀 930·2021-11-22 12:09
閱讀 3704·2021-09-27 13:36
閱讀 1390·2021-08-20 09:37
閱讀 4007·2019-12-27 12:22
閱讀 2353·2019-08-30 15:55
閱讀 2359·2019-08-30 13:16
閱讀 2817·2019-08-26 17:06
閱讀 3434·2019-08-23 18:32