21、大型重构(下):分离领域与表现、提炼继承体系

好,咱们接着聊大型重构。上一章我们讲了如何拆解模块、梳理依赖关系。这一章,我重点说说两个我特别常用的手法——分离领域与表现,以及提炼继承体系

说白了,这两招都是用来对付“代码长成一团乱麻”的。你想想看,一个系统跑了两三年,业务逻辑和界面代码早就你中有我、我中有你了。改个按钮颜色,结果把计算逻辑改崩了——这种事我见过太多次了。

为什么要把领域和表现分开?

先问个问题:你的业务逻辑,到底能不能脱离UI独立运行?

如果能,那恭喜你,你的架构还不错。如果不能,那你就得小心了——每次换UI框架、改前端技术栈,你都得把业务逻辑重写一遍。我在项目中遇到过一家公司,他们的核心计算逻辑直接写在Vue组件的methods里。后来要迁移到React,整个团队崩溃了三个月。

所以,分离领域与表现的核心目标就一个:让业务逻辑不依赖任何UI框架

核心原则:领域层只关心“做什么”,表现层只关心“怎么展示”。

怎么分离?一个实战例子

假设我们有一个订单系统。原来的代码可能是这样的:

// 糟糕的写法:领域逻辑混在UI里
class OrderPage {
  calculateTotal(items) {
    let total = 0;
    for (let item of items) {
      total += item.price * item.quantity;
      if (item.isDiscountable) {
        total *= 0.9; // 10%折扣
      }
    }
    return total;
  }

  render() {
    // 渲染逻辑...
  }
}

嗯,这里要注意。这个calculateTotal方法,它其实是一个领域逻辑——它决定了订单的总价怎么算。但它却寄生在UI类里。如果哪天你要加个新折扣规则,你得到处找这些散落的计算逻辑。

重构之后,我们应该这样做:

// 领域层:纯业务逻辑,不依赖任何UI
class Order {
  constructor(items) {
    this.items = items;
  }

  calculateTotal() {
    let total = 0;
    for (let item of this.items) {
      total += item.price * item.quantity;
    }
    total = this.applyDiscount(total);
    return total;
  }

  applyDiscount(total) {
    const discountableItems = this.items.filter(i => i.isDiscountable);
    if (discountableItems.length > 0) {
      total *= 0.9;
    }
    return total;
  }
}

// 表现层:只负责展示和用户交互
class OrderPage {
  constructor(order) {
    this.order = order;
  }

  render() {
    const total = this.order.calculateTotal();
    // 渲染total到页面...
  }
}

你看,现在Order类可以单独测试,可以复用到后台服务、API接口,甚至命令行工具里。而OrderPage只负责一件事:把数据展示出来。

我的习惯:写任何新功能之前,先问自己一句——“这个逻辑如果离开UI,还能不能跑?” 如果答案是“不能”,那就说明你还没分离干净。

提炼继承体系:什么时候用?

分离完领域和表现之后,你可能会发现另一个问题:领域层里有很多相似但又不完全相同的类。比如,我们有普通订单会员订单批发订单,它们都有计算总价的方法,但折扣规则完全不同。

这时候,提炼继承体系就派上用场了。

我曾经接手过一个项目,里面有一个OrderProcessor类,足足有3000行。里面全是if-else:

// 重构前:一个巨大的类,用if-else区分不同类型
class OrderProcessor {
  process(order) {
    if (order.type === 'normal') {
      // 普通订单处理逻辑...
    } else if (order.type === 'vip') {
      // VIP订单处理逻辑...
    } else if (order.type === 'wholesale') {
      // 批发订单处理逻辑...
    }
    // 还有更多...
  }
}

这种代码,说白了就是“面向过程”的写法。每加一种新订单类型,你就得改这个类,改着改着就改崩了。

重构之后,我们提炼出一个继承体系:

// 基类:定义公共接口
class Order {
  process() {
    throw new Error('子类必须实现process方法');
  }
}

// 子类:各自实现自己的逻辑
class NormalOrder extends Order {
  process() {
    // 普通订单处理
  }
}

class VipOrder extends Order {
  process() {
    // VIP订单处理
  }
}

class WholesaleOrder extends Order {
  process() {
    // 批发订单处理
  }
}

这样一来,新增一种订单类型,你只需要新建一个子类,完全不用动已有的代码。这就是开闭原则的体现——对扩展开放,对修改关闭。

避坑指南:我曾经犯过一个错误——为了“复用”而强行提炼继承体系。结果子类之间差异太大,基类里塞满了抽象方法,子类实现起来痛苦不堪。记住:继承体系只适用于“is-a”关系,不要为了复用而硬凑。

两者结合:一个完整的重构流程

在实际项目中,我通常会把这两招结合起来用。先分离领域与表现,再在领域层里提炼继承体系。下面这张图展示了这个流程:

原始代码 领域逻辑混在UI中 一个类处理所有类型 第一步 分离领域与表现 领域层:纯业务逻辑 表现层:只负责展示 第二步 提炼继承体系 基类定义接口 子类各自实现 领域层内部结构 Order(基类) + calculateTotal() NormalOrder VipOrder WholesaleOrder ...更多子类

什么时候不该用继承?

提炼继承体系虽然好用,但也不是万能的。我总结了几种“不该用”的场景:

场景 问题 更好的做法
子类之间只有少量代码不同 继承体系太浅,反而增加复杂度 用策略模式或参数化
子类需要覆盖基类大部分方法 基类形同虚设,子类各自为政 考虑接口分离或组合
继承层次超过3层 代码难以理解和调试 用组合代替继承

我的建议:如果你发现自己在写“抽象类A继承抽象类B,B又继承抽象类C”,那大概率是过度设计了。记住一句话:组合优于继承。能用组合解决的问题,就别硬上继承。

总结一下

这一章我们聊了两个大型重构手法:

  • 分离领域与表现:让业务逻辑脱离UI,独立存在。这样换UI框架、加新功能,都不会影响核心逻辑。
  • 提炼继承体系:把一个大而全的类,拆成基类+子类的结构。每种子类型有自己的实现,互不干扰。

这两个手法,说白了都是在做一件事——降低耦合,提高内聚。代码的每个部分只关心自己该关心的事,改起来才不慌。

嗯,今天就先到这里。记住,重构不是一蹴而就的,慢慢来,一点点拆,代码会越来越干净的。


公众号:蓝海资料掘金营,微信deep3321