10重构(第2版)

简化条件逻辑

让逻辑更清晰

Decompose ConditionalReplace Conditional with Polymorphism

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第10章 简化条件逻辑(Simplifying Conditional Logic)

一、导读

1.1 本章学习目标

通过本章的学习,你将能够:

理解复杂条件逻辑对代码可读性和可维护性的负面影响

掌握分解条件表达式(Decompose Conditional)的技术,将复杂的条件拆分为有意义的函数

学会合并条件表达式(Consolidate Conditional Expression),将多个产生相同结果的条件合并

掌握以卫语句取代嵌套条件表达式(Replace Nested Conditional with Guard Clauses),减少嵌套层级

学会以多态取代条件表达式(Replace Conditional with Polymorphism),消除基于类型的条件判断

掌握引入特例(Introduce Null Object)的技术,消除null检查

理解断言在条件逻辑中的作用,学会使用断言文档化假设

能够在实际项目中识别和简化复杂的条件逻辑

1.2 为什么本章如此重要

条件逻辑是编程的基础,但复杂的条件逻辑是代码难以理解的主要原因之一。深层嵌套的if-else、冗长的条件表达式、重复的条件判断都会使代码变得混乱。

本章的重要性体现在以下几个方面:

首先,条件逻辑直接影响代码的可读性。清晰的條件可以使代码像自然语言一样易读,而复杂的条件会让读者迷失在分支中。

其次,条件逻辑是bug的高发区。每个分支都是一个潜在的执行路径,路径越多,测试覆盖越困难,遗漏的边界情况也越多。

第三,随着业务的发展,条件往往会不断增加。如果不及时重构,条件逻辑会逐渐膨胀,最终变得无法维护。

第四,现代编程语言和范式提供了多种替代传统条件逻辑的方式,如多态、策略模式、表驱动等。掌握这些替代方案可以写出更优雅的代码。

第五,在函数式编程的影响下,越来越多的开发者倾向于用组合和映射来替代条件分支。理解这些方法对于编写现代JavaScript代码非常重要。

最后,简化条件逻辑往往能揭示更深层次的设计问题。例如,大量的类型检查可能表明需要使用多态;频繁的null检查可能表明需要引入Null Object模式。

对于任何希望编写清晰代码的开发者来说,本章都是必读的。它不仅告诉你如何简化条件,更告诉你为什么某些条件模式是有问题的。

1.3 核心问题引导

在阅读本章之前,请思考以下问题:

你最多见过多少层的if-else嵌套?阅读这样的代码是什么感受?

当一个条件表达式超过一行时,你会怎么做?

你是否曾经因为漏掉一个else分支而导致bug?

switch语句总是坏味道吗?在什么情况下switch是合适的?

null/undefined检查占你的代码多大比例?有没有办法减少这些检查?

多态真的能完全替代条件逻辑吗?有什么局限性?

在React组件中,条件渲染的最佳实践是什么?

短路求值(&&、||)是好的实践吗?它有什么陷阱?

带着这些问题阅读本章,你会对条件逻辑有更深刻的理解。


二、核心概念详解

2.1 条件逻辑的问题

条件逻辑本身不是问题,问题在于复杂度。

复杂条件的表现:

深层嵌套: if里面套if,再套if...

冗长条件: 一行包含多个&&、||、!的组合

重复条件: 相同的条件在多处出现

魔法数字: 条件中使用没有解释的数字

隐式逻辑: 依赖布尔值的隐式转换

影响:

  • 难以理解执行路径
  • 难以测试所有分支
  • 修改一个分支可能影响其他分支
  • 容易遗漏边界情况

2.2 分解条件表达式(Decompose Conditional)

分解条件表达式是将复杂的条件拆分为有名字的函数调用。

动机:

  • 条件表达式太长,难以一眼看懂
  • 条件中包含复杂的计算或比较
  • 希望通过命名来文档化条件的含义

识别机会:

javascript
// Before - 复杂条件
if (date.before(SUMMER_START) || date.after(SUMMER_END)) {
  charge = quantity * winterRate + winterServiceCharge;
} else {
  charge = quantity * summerRate;
}

// After - 分解条件
if (isWinter(date)) {
  charge = winterCharge(quantity);
} else {
  charge = summerCharge(quantity);
}

function isWinter(date) {
  return date.before(SUMMER_START) || date.after(SUMMER_END);
}

function winterCharge(quantity) {
  return quantity * winterRate + winterServiceCharge;
}

function summerCharge(quantity) {
  return quantity * summerRate;
}

机制:

识别复杂条件: 找出难以理解的条件表达式。

提取条件为函数: 将条件部分提取为有名字的函数。

提取分支逻辑: 如果分支逻辑复杂,也提取为函数。

替换原代码: 用函数调用替换原条件和分支。

测试: 确保行为不变。

命名建议:

  • 条件函数通常以is、has、can、should开头
  • 名称应该表达条件的业务含义,而非实现细节

2.3 合并条件表达式(Consolidate Conditional Expression)

合并条件表达式是将多个产生相同结果的条件合并为一个。

动机:

  • 多个条件检查都返回相同的结果
  • 合并后可以提取为有意义的函数
  • 减少代码重复

识别机会:

javascript
// Before - 多个条件返回相同结果
function disabilityAmount(employee) {
  if (employee.seniority < 2) return 0;
  if (employee.departments < 2) return 0;
  if (employee.isPartTime) return 0;
  
  // 计算残疾金
  return calculateDisability(employee);
}

// After - 合并条件
function disabilityAmount(employee) {
  if (isNotEligibleForDisability(employee)) return 0;
  return calculateDisability(employee);
}

function isNotEligibleForDisability(employee) {
  return employee.seniority < 2 || 
         employee.departments < 2 || 
         employee.isPartTime;
}

机制:

识别相同结果的條件: 找出返回相同值的多个条件。

合并条件: 使用&&或||将条件合并为一个表达式。

提取为函数: 将合并后的条件提取为有名字的函数。

测试: 确保行为不变。

注意事项:

  • 确保条件确实产生相同结果
  • 注意短路求值的影响
  • 合并后的条件应该有统一的业务含义

2.4 以卫语句取代嵌套条件表达式(Replace Nested Conditional with Guard Clauses)

以卫语句取代嵌套条件表达式是用早期的return语句替代深层嵌套的if-else。

动机:

  • 深层嵌套使代码难以阅读
  • 大部分情况下函数会提前返回
  • 卫语句可以清晰地表达异常情况

识别机会:

javascript
// Before - 嵌套条件
function getPayAmount(employee) {
  let result;
  if (employee.isSeparated) {
    result = { amount: 0, reasonCode: 'SEP' };
  } else {
    if (employee.isRetired) {
      result = { amount: 0, reasonCode: 'RET' };
    } else {
      // 计算工资
      result = calculateRegularPay(employee);
    }
  }
  return result;
}

// After - 卫语句
function getPayAmount(employee) {
  if (employee.isSeparated) return { amount: 0, reasonCode: 'SEP' };
  if (employee.isRetired) return { amount: 0, reasonCode: 'RET' };
  
  return calculateRegularPay(employee);
}

机制:

识别最外层条件: 从最外层的if开始。

转换为卫语句: 将条件改为早期return。

减少缩进: 删除else分支,减少一层缩进。

重复: 对剩余的嵌套条件重复上述步骤。

测试: 确保行为不变。

设计原则:

  • 常规路径放后面: 卫语句处理异常/特殊情况,主逻辑放在最后
  • 尽早返回: 一旦发现可以确定结果,立即返回
  • 保持一致: 要么都用卫语句,要么都用if-else,不要混用

2.5 以多态取代条件表达式(Replace Conditional with Polymorphism)

以多态取代条件表达式是用面向对象的多态特性替代基于类型的条件判断。

动机:

  • 基于类型码的switch/if链难以扩展
  • 添加新类型需要修改多处条件
  • 多态可以将行为分散到各自的类中

识别机会:

javascript
// Before - 基于类型的条件
class Bird {
  constructor(type) {
    this._type = type;
  }
  
  get plumage() {
    switch (this._type) {
      case 'EuropeanSwallow':
        return 'average';
      case 'AfricanSwallow':
        return 'tired';
      case 'NorwegianBlueParrot':
        return 'beautiful';
      default:
        return 'unknown';
    }
  }
  
  get airSpeedVelocity() {
    switch (this._type) {
      case 'EuropeanSwallow':
        return 35;
      case 'AfricanSwallow':
        return 40;
      case 'NorwegianBlueParrot':
        return null;
      default:
        return null;
    }
  }
}

// After - 多态
class Bird {
  get plumage() {
    throw new Error('Must implement in subclass');
  }
  
  get airSpeedVelocity() {
    throw new Error('Must implement in subclass');
  }
}

class EuropeanSwallow extends Bird {
  get plumage() {
    return 'average';
  }
  
  get airSpeedVelocity() {
    return 35;
  }
}

class AfricanSwallow extends Bird {
  get plumage() {
    return 'tired';
  }
  
  get airSpeedVelocity() {
    return 40;
  }
}

class NorwegianBlueParrot extends Bird {
  get plumage() {
    return 'beautiful';
  }
  
  get airSpeedVelocity() {
    return null;
  }
}

// 工厂函数
function createBird(type) {
  const birds = {
    EuropeanSwallow: EuropeanSwallow,
    AfricanSwallow: AfricanSwallow,
    NorwegianBlueParrot: NorwegianBlueParrot
  };
  
  const BirdClass = birds[type];
  if (!BirdClass) {
    throw new Error(`Unknown bird type: ${type}`);
  }
  
  return new BirdClass();
}

机制:

识别类型条件: 找出基于类型码的switch或if链。

创建继承体系: 为每种类型创建子类。

迁移行为: 将每个分支的逻辑移到对应的子类中。

创建工厂: 用工厂函数替代类型码的实例化。

删除条件: 删除原来的条件判断。

测试: 确保行为不变。

替代方案 - 策略模式:

如果不适合使用继承,可以使用策略模式:

javascript
const birdBehaviors = {
  EuropeanSwallow: {
    plumage: 'average',
    airSpeedVelocity: 35
  },
  AfricanSwallow: {
    plumage: 'tired',
    airSpeedVelocity: 40
  }
};

class Bird {
  constructor(type) {
    this._behavior = birdBehaviors[type];
  }
  
  get plumage() {
    return this._behavior.plumage;
  }
  
  get airSpeedVelocity() {
    return this._behavior.airSpeedVelocity;
  }
}

注意事项:

  • 多态增加了类的数量
  • 对于简单的条件,多态可能过度
  • 考虑使用对象字面量作为轻量级替代

2.6 引入特例(Introduce Null Object)

引入特例是用一个特殊的对象替代null检查,该对象提供默认行为。

动机:

  • 大量的null检查使代码混乱
  • null可能导致运行时错误
  • 需要一个"无操作"的默认行为

识别机会:

javascript
// Before - 大量null检查
class Site {
  get customer() {
    return this._customer; // 可能为null
  }
}

function billingPlan(site) {
  const customer = site.customer;
  if (customer === null) {
    return basicPlan;
  }
  return customer.billingPlan;
}

function customerName(site) {
  const customer = site.customer;
  if (customer === null) {
    return 'occupant';
  }
  return customer.name;
}

// After - Null Object
class NullCustomer {
  get billingPlan() {
    return basicPlan;
  }
  
  get name() {
    return 'occupant';
  }
  
  get isNull() {
    return true;
  }
}

class RealCustomer {
  get billingPlan() {
    return this._billingPlan;
  }
  
  get name() {
    return this._name;
  }
  
  get isNull() {
    return false;
  }
}

class Site {
  get customer() {
    return this._customer || new NullCustomer();
  }
}

function billingPlan(site) {
  return site.customer.billingPlan; // 无需null检查
}

function customerName(site) {
  return site.customer.name; // 无需null检查
}

机制:

识别null检查: 找出频繁检查null的地方。

创建特例类: 创建一个提供默认行为的类。

实现相同接口: 特例类实现与真实对象相同的接口。

返回特例: 在可能返回null的地方返回特例对象。

删除null检查: 移除客户端的null检查。

测试: 确保行为不变。

JavaScript中的可选链:

现代JavaScript提供了可选链操作符,可以减少null检查的需要:

javascript
// 使用可选链
const name = site?.customer?.name ?? 'occupant';

但Null Object模式仍然有价值,当需要更复杂的默认行为时。

2.7 引入断言(Introduce Assertion)

引入断言是在代码中添加断言语句,文档化代码的假设条件。

动机:

  • 代码依赖某些前提条件,但没有明确表达
  • 希望尽早发现违反假设的情况
  • 用断言作为文档,帮助读者理解代码

示例:

javascript
function applyDiscount(price, discount) {
  assert(price >= 0, 'Price must be non-negative');
  assert(discount >= 0 && discount <= 1, 'Discount must be between 0 and 1');
  
  return price * (1 - discount);
}

function assert(condition, message) {
  if (!condition) {
    throw new Error(`Assertion failed: ${message}`);
  }
}

断言 vs 错误处理:

  • 断言: 检查不应该发生的情况(程序bug)
  • 错误处理: 处理可能发生的情况(用户输入、网络故障)

三、程序员的视角

3.1 在实际项目中的应用

场景一:权限检查的卫语句化

在一个后台系统中,权限检查原本嵌套很深:

javascript
// Before
function handleRequest(req) {
  if (req.user) {
    if (req.user.isActive) {
      if (req.user.hasPermission(req.action)) {
        // 实际处理
      } else {
        return forbidden();
      }
    } else {
      return inactive();
    }
  } else {
    return unauthorized();
  }
}

// After
function handleRequest(req) {
  if (!req.user) return unauthorized();
  if (!req.user.isActive) return inactive();
  if (!req.user.hasPermission(req.action)) return forbidden();
  
  // 实际处理
}

场景二:支付策略的多态化

将基于支付方式的条件判断改为多态:

javascript
class PaymentProcessor {
  process(payment) { /* abstract */ }
}

class CreditCardProcessor extends PaymentProcessor { /* ... */ }
class PayPalProcessor extends PaymentProcessor { /* ... */ }
class CryptoProcessor extends PaymentProcessor { /* ... */ }

3.2 常见陷阱与误区

陷阱一:过度使用三元运算符

嵌套的三元运算符比if-else更难读。

教训: 三元运算符只用于简单的二选一。

陷阱二:忽视短路求值的副作用

javascript
// 危险
user && user.save(); // 如果user为falsy,save不会调用

陷阱三:多态过度

为简单的条件创建过多的类。

教训: 权衡复杂度和可扩展性。

3.3 与其他层级的联系

与设计模式的联系:

  • Replace Conditional → Strategy Pattern
  • Replace Conditional → State Pattern
  • Introduce Null Object → Null Object Pattern

与函数式编程的联系:

  • 条件表达式 → map/filter/reduce
  • switch → 对象查找表

四、代码实战

4.1 示例一:完整的条件简化

javascript
// Before
function calculatePrice(order) {
  if (order.items.length > 0) {
    if (order.customer.type === 'vip') {
      if (order.total > 1000) {
        return order.total * 0.7;
      } else if (order.total > 500) {
        return order.total * 0.8;
      } else {
        return order.total * 0.9;
      }
    } else if (order.customer.type === 'regular') {
      if (order.total > 1000) {
        return order.total * 0.85;
      } else {
        return order.total * 0.95;
      }
    } else {
      return order.total;
    }
  } else {
    return 0;
  }
}

// After
function calculatePrice(order) {
  if (order.items.length === 0) return 0;
  
  const discount = getDiscountRate(order.customer.type, order.total);
  return order.total * discount;
}

function getDiscountRate(customerType, total) {
  const discounts = {
    vip: { threshold: 1000, high: 0.7, mid: 0.8, low: 0.9 },
    regular: { threshold: 1000, high: 0.85, mid: 0.95, low: 0.95 }
  };
  
  const config = discounts[customerType];
  if (!config) return 1; // 无折扣
  
  if (total > config.threshold) return config.high;
  if (total > 500) return config.mid;
  return config.low;
}

4.2 示例二:React中的条件渲染

javascript
// Before - 复杂的条件渲染
function UserPanel({ user }) {
  if (user) {
    if (user.isAdmin) {
      return <AdminDashboard user={user} />;
    } else if (user.isModerator) {
      return <ModeratorPanel user={user} />;
    } else {
      return <UserProfile user={user} />;
    }
  } else {
    return <LoginPage />;
  }
}

// After - 清晰的卫语句
function UserPanel({ user }) {
  if (!user) return <LoginPage />;
  if (user.isAdmin) return <AdminDashboard user={user} />;
  if (user.isModerator) return <ModeratorPanel user={user} />;
  
  return <UserProfile user={user} />;
}

五、深入思考

5.1 历史背景

条件逻辑的简化研究可以追溯到结构化编程时代。Dijkstra的《GOTO Considered Harmful》开启了简化控制流的讨论。

多态作为条件逻辑的替代,是面向对象编程的核心贡献之一。设计模式中的Strategy和State模式进一步系统化了这一思想。

函数式编程带来了新的条件处理方式:模式匹配、代数数据类型等。

5.2 前沿方向

模式匹配:

  • TypeScript的模式匹配提案
  • Rust/Swift的match表达式
  • JavaScript的未来可能支持

类型守卫:

  • TypeScript的类型谓词
  • 运行时类型检查
  • 缩小(narrowing)技术

声明式条件:

  • CSS的条件查询
  • React的条件hook
  • 配置驱动的条件

5.3 延伸阅读

  • 《Clean Code》Chapter 3: Functions - 关于条件逻辑的讨论
  • 《Refactoring to Patterns》- Joshua Kerievsky关于条件到模式的转换
  • TypeScript Handbook - Narrowing和类型守卫

六、实验笔记

实验 1:简化一个复杂条件函数

目标: 选择一个包含复杂条件的函数,对其进行简化。

步骤:

选择函数

分析条件结构

应用适当的简化技术

验证功能正常

记录:

  • 原函数的条件复杂度:
  • 应用的简化技术:
  • 简化后的效果:

实验 2:将switch改为多态

目标: 将一个基于switch的条件逻辑改为多态实现。

步骤:

识别switch语句

设计类层次

实现多态

删除switch

记录:

  • switch的分支数:
  • 设计的类:
  • 代码量变化:
  • 可扩展性改善:

七、本章小结

本章介绍了简化条件逻辑的各种技术。

分解条件表达式通过给条件命名,使复杂条件变得易读。合并条件表达式将多个产生相同结果的条件合并,减少重复。

卫语句通过早期返回,消除了深层嵌套,使主流程更清晰。这是处理异常和边界情况的推荐方式。

多态是消除基于类型条件的强大工具。它将行为分散到各自的类中,使添加新类型变得容易。但对于简单条件,多态可能过度。

Null Object模式通过提供默认行为,消除了大量的null检查。在现代JavaScript中,可选链操作符也可以减少null检查的需要。

断言用于文档化代码的假设,帮助早期发现bug。断言应该用于检查不应该发生的情况,而非处理预期的错误。

简化条件逻辑的目标是让代码更易读、更易维护、更少bug。选择合适的技术取决于具体的场景和复杂度。


八、习题

理解题: 解释卫语句的价值。为什么它比嵌套if-else更好?

实践题: 简化以下代码:

javascript
function getStatus(user) {
  if (user) {
    if (user.active) {
      if (user.verified) {
        return 'active-verified';
      } else {
        return 'active-unverified';
      }
    } else {
      return 'inactive';
    }
  } else {
    return 'guest';
  }
}

设计题: 为一个通知系统设计多态结构,支持邮件、短信、推送等多种通知方式。

分析题: 比较多态和策略模式在替代条件逻辑方面的优缺点。

综合题: 重构以下代码,应用本章学到的技术:

javascript
function getShippingCost(order) {
  if (order.destination === 'US') {
    if (order.weight < 1) return 5;
    else if (order.weight < 5) return 10;
    else return 15;
  } else if (order.destination === 'EU') {
    if (order.weight < 1) return 10;
    else if (order.weight < 5) return 20;
    else return 30;
  } else {
    return 50;
  }
}

讨论题: "switch语句总是坏味道",你同意吗?为什么?

挑战题: 实现一个通用的条件表驱动处理器,用配置替代条件逻辑。

10/12简化条件
0%