《重構(gòu)》第十章 - 簡(jiǎn)化條件邏輯

條件邏輯增加了程序的完整性,但同樣也增加了程序的復(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)慎使用。

斷點(diǎn)是幫助我們跟蹤bug的最后一招,只有當(dāng)認(rèn)為斷言絕對(duì)不會(huì)失敗的時(shí)候才使用斷言。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容