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

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

斷點是幫助我們跟蹤bug的最后一招,只有當(dāng)認(rèn)為斷言絕對不會失敗的時候才使用斷言。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容