类继承中的耦合问题从哪来
很多人在写面向对象代码时,一上来就用继承。比如做电商系统,看到商品有普通商品、秒杀商品、团购商品,立马就想建一个基类 Product,再让 SecKillProduct 和 GroupBuyProduct 去继承它。初衷是复用代码,结果往往事与愿违。
问题出在子类对父类的强依赖上。一旦父类改了方法签名或逻辑,所有子类都得跟着动。更糟的是,有些子类可能根本不需要父类的某些功能,但因为继承关系,它们还是被绑上了。这就像租房子,你只想要个单间,结果房东硬塞给你一套三居室,还让你交全额房租。
用组合代替继承
与其让类之间形成“我是你”的关系,不如换成“我有你”的关系。比如,把促销逻辑从商品类里拆出来,变成独立的策略对象。
class DiscountStrategy {
apply(price) {
return price;
}
}
class SecKillDiscount extends DiscountStrategy {
apply(price) {
return Math.max(price - 20, 0);
}
}
class Product {
constructor(name, price, discountStrategy) {
this.name = name;
this.price = price;
this.discountStrategy = discountStrategy || new DiscountStrategy();
}
getFinalPrice() {
return this.discountStrategy.apply(this.price);
}
}现在要加新的促销方式,不用动 Product 类,只要新增一个策略类就行。原来那种层层继承的方式,加个新类型就得改类结构,现在只需要拼装组件。
接口隔离比继承更灵活
有时候我们继承只是为了实现某个接口行为。比如日志模块,有控制台输出、文件输出、网络上报等多种方式。如果写一个 LogBase 再让各种日志类去继承,很快就会陷入维护泥潭。
更好的做法是约定行为接口,不靠继承来强制统一结构。
class ConsoleLogger {
log(message) {
console.log('[Console]' + message);
}
}
class FileLogger {
log(message) {
// 写入文件逻辑
}
}只要保证都有 log 方法,具体怎么实现各自负责。调用方只关心能不能 log,不关心你是谁的儿子。
避免深度继承链
见过最夸张的项目有六层继承:BaseEntity → DataEntity → BusinessEntity → OrderEntity → UserOrderEntity → SpecialUserOrderEntity。改最底层的类,整个链条全得测一遍。
浅一点的结构更容易看懂。两层以内基本还能接受,超过三层就要警惕了。真需要复用,可以把共用逻辑抽成工具函数或者 mixin,而不是一味往上堆继承层级。
优先使用依赖注入
把依赖关系通过参数传进去,而不是在类内部直接 new 或引用全局类。这样测试也方便,mock 起来轻松。
class UserService {
constructor(logger, userRepository) {
this.logger = logger;
this.userRepository = userRepository;
}
createUser(data) {
this.logger.log('创建用户开始');
const user = this.userRepository.save(data);
this.logger.log('用户创建成功');
return user;
}
}这个类不再依赖具体的日志或数据库实现,换哪种都不用改代码。比起继承固定的父类,这种方式明显更松散、更可控。