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