條件邏輯增加了程序的完整性,但同樣也增加了程序的復(fù)雜度。本章會通過分解條件表達(dá)式、合并條件表達(dá)式以及用衛(wèi)語句取代嵌套條件表達(dá)式等方法來簡化復(fù)雜的表達(dá)式,以表達(dá)更清晰的用意。
分解條件表達(dá)式(Decompose Conditional)
- 動機
代碼中,條件邏輯是最常導(dǎo)致復(fù)雜度上升的方式之一。編寫代碼來檢查不同的條件分支,根據(jù)不同的條件做不同的需求,這樣久而久之,很快會獲得一個相當(dāng)長的函數(shù)。大型函數(shù)本身就會讓代碼的可讀性下降,而條件邏輯則會讓代碼更難理解。
和任何大型函數(shù)一樣,將各個條件中的行為分解成多個獨立的函數(shù),從而更清楚的表達(dá)不同條件需要的行為需求。
- 范例
當(dāng)在計算購買某樣商品的總價(總價 = 數(shù)量 * 單價),而這個商品在冬季和夏季的單價是不同的:
if (![aDate isBefore:plan.summerStart]
&&![aDate isAfter:plan.summerEnd]) {
charge = quantity * plan.summerRate;
} else {
charge = quantity * plan.regularRate + plan.regularServiceCharge;
}
首先將判斷條件提煉到一個獨立的函數(shù)中:
- (BOOL)inSummer {
return ![aDate isBefore:plan.summerStart]
&&![aDate isAfter:plan.summerEnd];
}
再將各個條件分支內(nèi)的行為分別進(jìn)行提煉:
- (CGFloat)summerCharge {
return charge = quantity * plan.summerRate;
}
- (CGFloat)regularCharge {
return quantity * plan.regularRate + plan.regularServiceCharge;
}
....
if ([self isSummer]) {
charge = [self summerCharge];
} else {
charge = [self regularCharge];
}
以上的代碼將不同的行為放置在對應(yīng)的函數(shù)中,也便于后續(xù)的擴展。當(dāng)然到這一步很多開發(fā)者喜歡使用三元運算符以到達(dá)一行代碼模式:
charge = [self isSummer] ? [self summerCharge] : [self regularCharge];
合并條件表達(dá)式(Consolidate Conditional Expression)
- 動機
有時在代碼中會發(fā)現(xiàn)一串條件檢查邏輯:檢查條件各不相同,但最終的行為卻一致。如果發(fā)現(xiàn)這種情況,就應(yīng)該使用"邏輯與"和"邏輯或"將它們合并為一個條件表達(dá)式。
因為這樣不僅讓檢查的用意更清晰,合并后的條件代碼會表達(dá)出"實際只有一次條件檢查,只不過有多個并列條件需檢查";還對之后提煉函數(shù)做好了準(zhǔn)備。
當(dāng)然如果這些檢查確實彼此獨立,那么不應(yīng)該被視為同一次檢查,不要使用本項重構(gòu)手段。
- 范例
在蔬菜入庫時,計算需要購買的數(shù)量:
- (NSInteger)vegetableWarehousing:(Vegetable *)vegetable {
if (vegetable.storageCapacity <= vegetable.hasCount) return 0;
if (vegetable.buyingPrice >= vegetable.sellingPrice) return 0;
if (vegetable.BlacklistedVendors) return 0;
// 具體需購買數(shù)量計算
...
}
以上函數(shù)中有一連串的條件檢查,都指向了相同的結(jié)果。將檢查全都合并成一個條件并且提煉函數(shù):
- (BOOL)isNotNeedToBuy:(Vegetable *)vegetable {
return vegetable.storageCapacity <= vegetable.hasCount
|| vegetable.buyingPrice >= vegetable.sellingPrice
|| vegetable.BlacklistedVendors;
}
- (NSInteger)vegetableWarehousing:(Vegetable *)vegetable {
if ([self isNotNeedToBuy:vegetable]) {
return 0;
}
// 具體需購買數(shù)量計算
}
從 vegetableWarehousing: 開發(fā)的角度來看,后續(xù)需求變動只需要明確是"需要更新不能購買條件" 還是"更新具體購買數(shù)量",代碼閱讀量降低,提高了開發(fā)效率。
以衛(wèi)語句取代嵌套條件表達(dá)式(Replace Nested Conditional with Guard Clauses)
- 動機
條件表達(dá)式通常有兩種風(fēng)格:
① 兩個條件分支都屬于正常開發(fā)行為
② 只有一個條件分支是正常開發(fā)行為,另一個分支則是異常情況。
如果兩條分支都是正常行為,就應(yīng)該使用形如 if... else... 或 switch... case...(多條件)的條件表達(dá)式;但是當(dāng)其中一個條件分支是處理異常情況時,就應(yīng)該單獨檢查該條件,并在該條件為真時立刻從函數(shù)返回。這樣單獨檢查常常被稱為"衛(wèi)語句"(Guard clauses)。
理解"衛(wèi)語句"所表達(dá)的含義:
"這種情況不是本函數(shù)的核心邏輯所關(guān)心的,如果它真發(fā)生了,請做一些必要的整理工作,然后退出。"
- 范例
計算需支付給員工Employee的工資,只有還在公司上班的員工才需要支付工資,所以這個函數(shù)需檢查"員工是否在公司上班中"的情況:
- (NSDictionary *)payAmount(Employee *)employee {
NSDictionary *result;
if (employee.isSeparated) {
result = @{@"amount": @(0), @"reasonCode": @"SEP"};
} else if (employee.hasInduction) {
result = @{@"amount": @(0), @"reasonCode": @"UNE"};
} else {
if (employee.isRetired) {
result = @{@"amount": @(0), @"reasonCode": @"RET"};
} else {
// 計算員工工資
result = [self someFinalComputation];
}
}
return result;
}
嵌套的條件邏輯復(fù)雜,無法快速了解代碼真實的含義。只有當(dāng)前三個條件表達(dá)式均不為真時,函數(shù)中才真正的開始它主要的工作。所以,引入衛(wèi)語句來取代嵌套條件:
- (NSDictionary *)payAmount(Employee *)employee {
if (employee.isSeparated) {
return @{@"amount": @(0), @"reasonCode": @"SEP"};
}
if (employee.hasInduction) {
return @{@"amount": @(0), @"reasonCode": @"UNE"};
}
if (employee.isRetired) {
return @{@"amount": @(0), @"reasonCode": @"RET"};
}
// 計算員工工資
return [self someFinalComputation];
}
以上改動后便可對核心邏輯一目了然了。
作者還提供了一個思路:
通過將條件表達(dá)式反轉(zhuǎn),以實現(xiàn)用衛(wèi)語句取代嵌套條件表達(dá)式:
- (NSInteger)adjustedCapital:(Instrument *)anInstrument {
NSInteger result = 0;
if (anInstrument.capital > 0) {
if (anInstrument.interestRate > 0 && anInstrument.duration > 0) {
result = [self someComputation];
}
}
return result;
}
采用 衛(wèi)語句取代嵌套條件表達(dá)式的手段,通過條件反轉(zhuǎn)、邏輯或?qū)l件合并,明確區(qū)分兩段代碼的作用:
- (NSInteger)adjustedCapital:(Instrument *)anInstrument {
if (anInstrument.capital <= 0
|| anInstrument.interestRate <= 0
|| anInstrument.duration <= 0) return 0;
return [self someComputation];
}
引入特例(Introduce Special Case)
- 動機
當(dāng)一個數(shù)據(jù)結(jié)構(gòu)的使用方都在檢查某個特殊的值,并且當(dāng)這個特殊值出現(xiàn)時所做的處理也都相同,這時候就能通過創(chuàng)建一個特例元素,用以表達(dá)對這種特例的共同行為的處理。
特例有幾種表現(xiàn)形式:如果只需從這個對象讀取數(shù)據(jù),可以提供一個字面量(literal object);當(dāng)除了簡單的數(shù)值之外還需更多的行為,可以通過封裝一個特殊的類結(jié)構(gòu)、或定義函數(shù)等方式來實現(xiàn)。
- 范例
在不同的調(diào)用位置,通過下發(fā)的數(shù)據(jù)type字段處理:
調(diào)用位置1
NSString *objType;
if (!data.type
|| [data.type isEqualToString: @""]
|| [data.type isEqualToString: @"Unknown"]) {
objType = @"unknown";
}
調(diào)用位置 2:
if (!data.type || [data.type isEqualToString: @""]) {
return [PlaceholderCell class];
}
// 更加type定制不同的 cell
.....
調(diào)用位置3:
NSString *objType;
if (!data.type
|| [data.type isEqualToString: @""]
|| [data.type isEqualToString: @"Unknown"]) {
objType = @"Unknown";
}
調(diào)用的位置都針對"不支持的數(shù)據(jù)類型"的情況做了處理,并且在觀察時可知對于"不支持"的認(rèn)定均相同,所以這種情況下,開發(fā)時可直接在 數(shù)據(jù)源DataModel中提供一個特例值:
DataModel.h
@property (nonatomic, assign) BOOL isSupportedType;
DataModel.m
- (BOOL)isSupportedType {
return !data.type
|| [data.type isEqualToString: @""]
|| [data.type isEqualToString: @"Unknown"]
}
當(dāng)然如調(diào)用的位置在"不支持的數(shù)據(jù)類型" 和 "判斷后的處理行為"均一致時,如各個調(diào)用點只做了 objType 的設(shè)置,那么可以將判斷和行為都提煉到一個獨立的數(shù)據(jù)結(jié)構(gòu)中:
xxxLog.h
// 入?yún)?@property (nonatomic, strong) NSString *dataType;
....
// 根據(jù)入?yún)⒂嬎憬Y(jié)果
@property (nonatomic, strong, readonly) NSString *objType;
...
引入斷言
- 動機
常常會有這樣的一段代碼邏輯:只有當(dāng)某個條件為真時,該段代碼才能正常運行。如:除法中的除數(shù)不能為0,某個對象中存儲的數(shù)據(jù)必須都大于200。
以上這些情況有時候并沒有明確的表現(xiàn)出來,必須閱讀完整個算法才能看出。有時開發(fā)者會通過注釋來標(biāo)注,但注釋本身只是簡單標(biāo)識并不能強制認(rèn)知,所以引入本節(jié)的手段 ---- 斷言。
斷言是一個條件表達(dá)式,應(yīng)該總是為真。如果它失敗,表示開發(fā)者犯了錯誤。整個程序的行為在沒有斷言出現(xiàn)時都應(yīng)該完全一樣。
- 范例
計算顧客,在獲得折扣率(discount rate)后得到的購買價格:
- (CGFloat)applyDiscount:(CGFloat)price {
return (self.discountRate) ? ((1 - self.discountRate ) * price): price;
}
以上代碼表達(dá)出:折扣率 discount rate 必須是正數(shù)。這種情況可以使用斷言明確的標(biāo)識:
- (CGFloat)applyDiscount:(CGFloat)price {
if (!self.discountRate) return price;
NSAssert(self.discountRate >= 0, @"折扣率為負(fù)數(shù)");
return (1 - self.discountRate ) * price;
}
以上代碼中使用斷言,是因為符合檢查"必須為真"的條件,而不只是"我認(rèn)為應(yīng)該是真"的條件。
斷言是一個雙刃劍?
在團隊開發(fā)工作中,大家負(fù)責(zé)的模塊不同,通過斷言可以更快的為模塊調(diào)用方提供一些字段認(rèn)知(比如 A字段必須 > 200)。但是如上所言,并不是所有場合都適合加入斷言。
對于一些數(shù)據(jù)源,如通過數(shù)據(jù)下發(fā)的type選擇顯示不同的cell類:
switch(data.type) {
case 1: {
return [LZCell1 class];
}
break;
case 2: {
return [LZCell2 class];
}
break;
default: {
NSAssert(NO, @"不支持的數(shù)據(jù)類型");
}
break;
}
以上的NSAssert依賴于數(shù)據(jù)源,而數(shù)據(jù)源本身就無法保證絕對不會出錯,所以如果在這種情況下添加,會導(dǎo)致開發(fā)其他模塊的同學(xué)無意間觸發(fā)時,還需要耗費時間了解斷點的位置、原因和解決方案,大大印象自己的開發(fā)時間。所以斷言還是需要謹(jǐn)慎使用。