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”关系,不要为了复用而硬凑。
两者结合:一个完整的重构流程
在实际项目中,我通常会把这两招结合起来用。先分离领域与表现,再在领域层里提炼继承体系。下面这张图展示了这个流程:
什么时候不该用继承?
提炼继承体系虽然好用,但也不是万能的。我总结了几种“不该用”的场景:
| 场景 | 问题 | 更好的做法 |
|---|---|---|
| 子类之间只有少量代码不同 | 继承体系太浅,反而增加复杂度 | 用策略模式或参数化 |
| 子类需要覆盖基类大部分方法 | 基类形同虚设,子类各自为政 | 考虑接口分离或组合 |
| 继承层次超过3层 | 代码难以理解和调试 | 用组合代替继承 |
我的建议:如果你发现自己在写“抽象类A继承抽象类B,B又继承抽象类C”,那大概率是过度设计了。记住一句话:组合优于继承。能用组合解决的问题,就别硬上继承。
总结一下
这一章我们聊了两个大型重构手法:
- 分离领域与表现:让业务逻辑脱离UI,独立存在。这样换UI框架、加新功能,都不会影响核心逻辑。
- 提炼继承体系:把一个大而全的类,拆成基类+子类的结构。每种子类型有自己的实现,互不干扰。
这两个手法,说白了都是在做一件事——降低耦合,提高内聚。代码的每个部分只关心自己该关心的事,改起来才不慌。
嗯,今天就先到这里。记住,重构不是一蹴而就的,慢慢来,一点点拆,代码会越来越干净的。
公众号:蓝海资料掘金营,微信deep3321