前言
在前端组件开发中,我发现很多设计问题的根源在于对对象关系理解不够深入。
一个组件应该依赖 还是关联 某个服务?子组件该用聚合 还是组合?这些看似简单的选择,直接影响了组件的可维护性和可测试性。
深入理解 OOP 中的 6 种对象关系,能帮助我们:
- 设计出结构清晰、职责明确的组件
- 写出易于测试和重构的代码
- 准确判断何时该解耦、何时该组合
- 看懂各种设计模式背后的原理
概述
对象关系按照耦合强度从弱到强排列:依赖 < 关联 < 聚合 < 组合 < 实现 < 继承
关系对比总览表如下:
| 关系类型 | UML符号 | 耦合强度 | 生命周期 | 关键词 | 前端典型场景 |
|---|---|---|---|---|---|
| 依赖 | - - -> |
⭐ 最弱 | 临时使用 | use | 函数参数、工具方法调用 |
| 关联 | ---> |
⭐⭐ | 持久引用 | has-a | 组件持有service、Store引用 |
| 聚合 | ◇---> |
⭐⭐⭐ | A包含B,B独立 | has-a | 购物车包含商品、播放列表包含歌曲 |
| 组合 | ◆---> |
⭐⭐⭐⭐ | A包含B,B依赖A | contains | DOM节点包含子节点 |
| 实现 | - - -▷ |
⭐⭐⭐⭐⭐ | 契约关系 | implements | Storage实现、支付网关实现 |
| 继承 | ---▷ |
⭐⭐⭐⭐⭐⭐ 最强 | 代码复用 | is-a | 子类extends父类 |
依赖 (Dependency)
定义
最弱的关系 ,表示一个类使用另一个类提供的功能/方法 。如果B修改可能影响A,则A依赖于B。这是一种临时性、使用性的关系。
UML表示

关键特征
-
生命周期:临时关系,使用完即结束
-
耦合度:最低
-
代码表现:方法参数、局部变量、静态方法调用
-
口诀:A"使用"B,用完即走
TypeScript示例
想象你在做菜:
- 你需要用刀切菜(临时使用刀的"切"的功能)
- 切完菜,刀就放回去了
- 你不拥有这把刀,只是临时借用了一下
typescript
class Chef {
// Chef依赖Knife, 临时使用Knife的cut功能
cookDish (knife: Knife, vegetable: string) { // ✅ 作为方法参数传入, ✅ 方法内创建临时对象
knife.cut(vegetable); // ✅ 调用静态方法 --- 使用knife的"切"功能
// ❌ 不作为成员变量存储 --- 方法结束,knife就没用了
}
}
class Knife {
cut (item: string) {
console.log(`切${item}`);
}
}
const chef = new Chef();
const knife = new Knife();
chef.cookDish(knife, '胡萝卜'); // 切胡萝卜
识别要点:
-
✅ 作为方法参数传入
-
✅ 方法内创建临时对象
-
✅ 调用静态方法
-
❌ 不作为成员变量存储
关联 (Association)
定义
表示类之间有持久的连接 。A对象知道B对象并能与之交互,这种关系会长期存在。关联可以视为是一种特殊类型的依赖,即一个对象总是拥有访问与其交互的对象的权限,而简单的依赖关系并不会在对象间建立永久性的联系。
UML表示

关键特征
-
生命周期:持久关系,只要对象存在就存在
-
耦合度:中等
-
代码表现:成员变量、属性
-
口诀:A"拥有"B的引用,长期持有
TypeScript示例
以"发送邮件"为例:
- 需要一个User类(用于存储email)和一个EmailServe类(负责发送邮件)
- 有三个场景需要发送邮件
- 用户注册欢迎邮件
- 用户重置密码通知邮件
- 普适性的通知邮件
首先回顾下「依赖」关系下我们会怎么写?
typescript
// 负责发送邮件的服务类
class EmailService {
send(email: string, content: string) {
console.log(`发送邮件给${email}:${content}`);
}
}
// 用户类
class User {
constructor(public email: string) {}
}
// 依赖举例
class UserControllerWithDependency {
// 注册用户
registerUser(user: User, emailService: EmailService) {
// 每次注册都要传入EmailService实例
emailService.send(user.email, 'Welcome'); // 使用EmailService发送邮件
}
// 重置密码
resetPassword(user: User, emailService: EmailService) {
// 这里又传一次
emailService.send(user.email, 'Reset your password'); // 使用EmailService发送邮件
}
notify(user: User, message: string, emailService: EmailService) {
// 这里再传一次
emailService.send(user.email, message); // 使用EmailService发送通知
}
}
const user1 = new User('8888888@163.com');
const emailService1 = new EmailService();
const userController1 = new UserControllerWithDependency();
// 每次都要传emailService,很繁琐
userController1.registerUser(user1, emailService1); // 传1次
userController1.resetPassword(user1, emailService1); // 传2次
userController1.notify(user1, 'Your profile has been updated', emailService1); // 传3次
接下来我们用「关联」改写
typescript
class UserController {
private emailService: EmailService; // ✅ 作为类的成员变量存储
constructor(emailService: EmailService) {
this.emailService = new EmailService(); // ✅ 通过构造函数注入
}
// 注册用户
registerUser(user: User) {
// ✅ 对象生命周期内持续使用 --- 直接使用
this.emailService.send(user.email, 'Welcome'); // 使用EmailService发送邮件
}
// 重置密码
resetPassword(user: User) {
// 直接使用
this.emailService.send(user.email, 'Reset your password'); // 使用EmailService发送邮件
}
notify(user: User, message: string) {
// 直接使用
this.emailService.send(user.email, message); // 使用EmailService发送通知
}
}
const user = new User('12345678@163.com');
const emailService = new EmailService(); // ❌ 外部创建,不管理被关联对象的生命周期
// 注入一次,userController的所有访问都能访问到emailService
const userController = new UserController(emailService);
userController.registerUser(user); // 注册成功,发送欢迎邮件
userController.resetPassword(user); // 重置密码,发送重置邮件
userController.notify(user, 'Your profile has been updated'); // 发送通知邮件
识别要点:
-
✅ 作为类的成员变量存储
-
✅ 在构造函数中注入
-
✅ 对象生命周期内持续使用
-
❌ 不管理被关联对象的生命周期
理解"不管理被关联对象的生命周期"
typescript
const emailService = new EmailService(); // 外部创建
let controller1: UserController | null = new UserController(emailService);
let controller2: UserController | null = new UserController(emailService); // 可以共享
controller1 = null; // controller1销毁
// ✅ emailService依然存在,controller2还能用
简单回顾:你能看出UserController依赖User吗?
聚合 (Aggregation)
定义
"整体-部分"关系 ,但部分可以独立于整体存在。这是一种松散的包含关系,用于表示多个对象之间的"一对多"、"多对多"或"整体对部分"的关系。
UML表示

关键特征
-
生命周期:部分可独立存在,不随整体销毁
-
耦合度:中等偏强
-
代码表现:成员变量,但不负责创建/销毁
-
口诀:A"包含"B,但B可以独立存在
TypeScript示例
想象一个"购物车"场景:
- 购物车可以管理着商品
- 可以添加商品到购物车,也可以删除购物车里的商品
- 还可以清空购物车
typescript
// 商品类
class Product {
constructor(
public id: string,
public name: string,
public price: number
) {}
}
class ShoppingCart {
private products: Product[] = []; // ✅ 整体包含部分 --- 购物车包含商品
// 添加商品到购物车
addProduct(product: Product) {
this.products.push(product);
console.log(`添加商品${product['name']}到购物车`);
}
// 删除商品从购物车
removeProduct(productId: string) {
this.products = this.products.filter(p => p['id'] !== productId);
console.log(`从购物车删除商品ID为${productId}的商品`);
}
// 清空购物车,但商品对象依然存在
clearCart() {
this.products = [];
console.log('购物车已清空');
}
}
// 使用
const product1 = new Product('1', 'Laptop', 999); // ✅ 部分可以独立创建
const product2 = new Product('2', 'Mouse', 29);
const cart = new ShoppingCart();
let cart2:ShoppingCart | null = new ShoppingCart();
cart.addProduct(product1);
cart.addProduct(product2);
cart2.addProduct(product1); // ✅ 部分可以属于多个整体
cart2 = null; // ✅ 整体销毁不影响部分 --- 购物车没了,但商品依然存在
cart.clearCart(); // ❌ 整体不负责部分的创建和销毁 --- 购物车清空,但product1和product2依然存在
console.log(product1.name); // 依然可访问
识别要点:
-
✅ 整体包含部分
-
✅ 部分可以独立创建
-
✅ 部分可以属于多个整体
-
✅ 整体销毁不影响部分
-
❌ 整体不负责部分的创建和销毁
组合 (Composition)
定义
强"整体-部分"关系,部分的生命周期完全由整体管理。部分不能独立存在,整体销毁时部分也会销毁。
UML表示

关键特征
-
生命周期:部分依赖整体,随整体创建和销毁
-
耦合度:强
-
代码表现:整体负责部分的创建和销毁
-
口诀:A"拥有"B,B的生死由A决定
TypeScript示例
typescript
class Engine {
constructor(public horsepower: number) {}
start() {
console.log(`引擎启动,马力为${this.horsepower}hp`);
}
}
class Car {
// ✅ 部分是private,外部无法访问 ❌ 部分不能被多个整体共享 ❌ 部分不能独立于整体存在
private engine: Engine;
// ✅ 整体在构造函数中创建部分 --- 在构造函数中创建引擎,引擎生命周期由Car管理
constructor(engineHorsepower: number) {
this.engine = new Engine(engineHorsepower);
}
startCar() {
this.engine.start();
console.log('汽车启动');
}
// ✅ 整体销毁时会主动销毁部分 --- 当汽车被销毁时,引擎也随之销毁
}
// 使用
const myCar = new Car(300);
myCar.startCar(); // 引擎启动,马力为300hp 汽车启动
// 无法直接访问engine实例,它的生命周期完全由car控制
识别要点:
-
✅ 整体在构造函数中创建部分
-
✅ 部分是private,外部无法访问
-
✅ 整体销毁时会主动销毁部分
-
❌ 部分不能被多个整体共享
-
❌ 部分不能独立于整体存在
简单回顾:根据案例你能判断什么时候用「聚合」,什么时候用「组合」吗?
- 聚合 :整体和部分可以分开,部分可以独立存在
- 组合 :整体和部分不能分开,部分依赖整体而存在
实现 (Realization / Implementation)
定义
类实现接口或抽象类,表示契约关系。实现类必须提供接口中声明的所有方法。
UML表示

关键特征
-
生命周期:编译时确定的契约
-
耦合度:强(必须实现所有方法)
-
代码表现 :
implements关键字 -
口诀:A"实现"接口B,A必须遵守B的契约
TypeScript示例
typescript
// 定义一个PaymentGateWay接口
// ✅ 接口定义行为规范 ❌ 接口不包含实现代码
interface PaymentGateWay {
processPayment(amount: number): Promise<boolean>;
refund(orderId: string): Promise<void>;
getBalance(): Promise<number>;
}
// 实现一个具体的支付网关类,例如PayPal
// ✅ 使用`implements`关键字 ✅ 必须实现接口的所有方法
class PayPalPaymentGateWay implements PaymentGateWay {
async processPayment(amount: number): Promise<boolean> {
console.log(`通过PayPal处理支付,金额:${amount}`);
// 模拟支付处理逻辑
return true;
}
refund(orderId: string): Promise<void> {
console.log(`通过PayPal处理退款,订单ID:${orderId}`);
// 模拟退款处理逻辑
return Promise.resolve();
}
getBalance(): Promise<number> {
console.log('获取PayPal账户余额');
// 模拟获取余额逻辑
return Promise.resolve(1000);
}
}
// 定义一个alipay特殊的花呗支付接口
interface HuabeiPaymentGateWay {
huabeiPay(amount: number): Promise<boolean>;
}
// 实现一个具体的支付网关类,例如Alipay
// ✅ 可以实现多个接口 --- 用,分割,比如alipay即需要实现PaymentGateWay,同时需要实现HuabeiPaymentGateWay
class AlipayPaymentGateWay implements PaymentGateWay, HuabeiPaymentGateWay {
async processPayment(amount: number): Promise<boolean> {
console.log(`通过Alipay处理支付,金额:${amount}`);
// 模拟支付处理逻辑
return true;
}
refund(orderId: string): Promise<void> {
console.log(`通过Alipay处理退款,订单ID:${orderId}`);
// 模拟退款处理逻辑
return Promise.resolve();
}
getBalance(): Promise<number> {
console.log('获取Alipay账户余额');
// 模拟获取余额逻辑
return Promise.resolve(2000);
}
async huabeiPay(amount: number): Promise<boolean> {
console.log(`通过Alipay花呗支付,金额:${amount}`);
// 模拟花呗支付处理逻辑
return true;
}
}
const paypalGateWay = new PayPalPaymentGateWay();
const alipayGateWay = new AlipayPaymentGateWay();
paypalGateWay.processPayment(150); // 通过PayPal处理支付,金额:150
alipayGateWay.processPayment(250); // 通过Alipay处理支付,金额:250
alipayGateWay.huabeiPay(300); // 通过Alipay花呗支付,金额:300
识别要点:
-
✅ 使用
implements关键字 -
✅ 必须实现接口的所有方法
-
✅ 可以实现多个接口
-
✅ 接口定义行为规范
-
❌ 接口不包含实现代码
总结来说,实现(Realization) 就是:
- 接口 定义"要做什么"(方法签名/契约)
- 类 负责"怎么做"(具体实现)
继承 (Inheritance)
定义
最强的关系,子类继承父类的属性和方法,表示**"is-a"关系**。子类可以重写父类方法,也可以添加新功能。
UML表示

关键特征
-
生命周期:编译时确定的继承结构
-
耦合度:最强(子类完全依赖父类)
-
代码表现 :
extends关键字 -
口诀:A"是一种"B,A继承B的一切
TypeScript示例
typescript
class Animal {
constructor(protected name: string) {}
move (distance: number) {
console.log(`${this.name} 移动了 ${distance} 米`);
}
makeSound () {
console.log(`${this.name} 发出声音`);
}
}
// ✅ 使用`extends`关键字 ✅ 只能继承一个父类(单继承)
class Dog extends Animal {
constructor(name: string, private breed: string) {
// ✅ 使用`super`调用父类方法
super(name); // 调用父类构造函数
}
// ✅ 可以重写父类方法
// 重写 父类 makeSound 方法
makeSound() {
console.log(`${this.name} 说: 汪汪汪`);
}
// 新增方法
fetch(item: string) {
console.log(`${this.name} 捡回了 ${item}`);
}
}
class Cat extends Animal {
// 重写 父类 makeSound 方法
makeSound() {
console.log(`${this.name} 说: 喵喵喵`);
}
// 新增方法
scratch() {
console.log(`${this.name} 抓挠家具`);
}
}
// 使用
const dog = new Dog('Buddy', 'Golden Retriever');
dog.move(10); // 继承自Animal
dog.makeSound(); // 重写的方法
dog.fetch('ball'); // Dog独有方法
const cat = new Cat('Whiskers');
cat.move(5);
cat.makeSound();
cat.scratch();
识别要点:
-
✅ 使用
extends关键字 -
✅ 子类自动拥有父类所有public/protected成员(属性和方法)
-
✅ 只能继承一个父类(单继承)
-
✅ 可以重写父类方法
-
✅ 使用
super调用父类方法 -
❌ 耦合度最高,需谨慎使用
补充知识点:访问修饰符(public/protected/private)
| 修饰符 | 自己能用 | 子类能用 | 外部能用 | 说明 |
|---|---|---|---|---|
| public | ✅ | ✅ | ✅ | 完全公开,谁都能访问 |
| protected | ✅ | ✅ | ❌ | 只有自己和子类能访问 |
| private | ✅ | ❌ | ❌ | 只有自己能访问,子类都不行 |
继承时,子类会自动获得父类的 public 和 protected 成员,不需要重新写代码!
typescript
// 父类
class Animal {
public name: string; // public - 谁都能访问
protected age: number; // protected - 自己和子类能访问
private id: string; // private - 只有自己能访问
constructor(name: string, age: number) {
this.name = name;
this.age = age;
this.id = Math.random().toString(); // 随机ID
}
public move() {
console.log(`${this.name} is moving`);
}
protected sleep() {
console.log(`${this.name} is sleeping`);
}
private breathe() {
console.log("Breathing...");
}
}
// 子类
class Dog extends Animal {
constructor(name: string, age: number) {
super(name, age);
}
bark() {
// ✅ 可以访问 public 成员
console.log(`${this.name} says Woof!`);
// ✅ 可以访问 protected 成员
console.log(`Dog age: ${this.age}`);
// ❌ 不能访问 private 成员
// console.log(this.id); // 报错!id是private的
// ✅ 可以调用 public 方法
this.move();
// ✅ 可以调用 protected 方法
this.sleep();
// ❌ 不能调用 private 方法
// this.breathe(); // 报错!breathe是private的
}
}
// 使用
const dog = new Dog('Buddy', 3);
// 外部可以访问 public 成员
console.log(dog.name); // ✅ 'Buddy'
dog.move(); // ✅ 'Buddy is moving'
// 外部不能访问 protected 成员
// console.log(dog.age); // ❌ 报错!age是protected的
// dog.sleep(); // ❌ 报错!sleep是protected的
// 外部不能访问 private 成员
// console.log(dog.id); // ❌ 报错!id是private的
// dog.breathe(); // ❌ 报错!breathe是private的
核心要点及最佳实践
核心要点图示总结

快速判断
-
代码层面
-
有关键字吗?(implements → 实现,extends → 继承)
-
是成员变量还是临时变量?(成员 → 关联/聚合/组合,临时 → 依赖)
-
谁负责创建对象?(整体创建 → 组合,外部创建 → 聚合)
-
-
生命周期
-
整体销毁时部分如何?(同时销毁 → 组合,依然存在 → 聚合)
-
关系持续多久?(临时 → 依赖,持久 → 关联/聚合/组合)
-
-
业务语义
-
是什么关系?(is-a → 继承,has-a → 关联/聚合/组合,use → 依赖)
-
部分能否独立存在?(能 → 聚合,不能 → 组合)
-
常见混淆辨析
1. 关联 vs 聚合 vs 组合
typescript
// 关联:只是持有引用
class Controller {
constructor(private service: ApiService) {} // 外部传入
}
// 聚合:包含但不管理生命周期
class Playlist {
private songs: Song[] = [];
addSong(song: Song) { this.songs.push(song); } // 外部创建的Song
}
// 组合:创建并管理生命周期
class Form {
private input = new Input(); // Form内部创建
private button = new Button(); // Form销毁时一起销毁
}
2. 依赖 vs 关联
typescript
// 依赖:临时使用
class UserService {
createUser(validator: Validator) { // 方法参数
validator.validate();
}
}
// 关联:持久持有
class UserService {
constructor(private validator: Validator) {} // 成员变量
}
判断技巧:看是否作为成员变量存储
3. 实现 vs 继承
typescript
// 实现:契约关系,只定义规范
interface Drawable {
draw(): void;
}
class Circle implements Drawable {
draw() { /* 自己实现 */ }
}
// 继承:代码复用,继承实现
class Shape {
draw() { /* 父类实现 */ }
}
class Circle extends Shape {
// 可以重写或直接使用父类实现
}
前端典型场景映射
| 场景 | 推荐关系 | 示例 | 说明 |
|---|---|---|---|
| 组件与 Service | 关联 | UserComponent 持有 ApiService |
组件调用服务时,服务需要外部定义好传入 |
| 组件与子组件 | 组合 | Dialog 包含 Header, Content, Footer |
Dialog组件内部定义子组件,当Dialog销毁时,子组件全部销毁 |
| 列表与项目 | 聚合 | TodoList 包含多个 TodoItem |
一个list由多个item聚合,list销毁时,item依然能存在 |
| Hooks 使用 | 依赖 | 组件调用 useState, useEffect |
组件只是临时调用hooks,不持有引用 |
| 组件继承 | 继承 | Button extends BaseComponent |
Button拥有BaseComponent的所有属性和方法,并可以改写,如重新onClick方法 |
| 实现接口 | 实现 | LocalStorage implements StorageInterface |
LocalStorage需要实现StorageInterface定义的所有方法 |
常见陷阱与解决方案
1. 过度使用继承
❌ 错误示例:
typescript
// 为了复用代码而继承
class UserService extends HttpClient {
getUser() { return this.get('/user'); }
}
问题:
- 违反了"is-a"关系 :UserService 不是一个 HttpClient
- 耦合度过高:继承了HttpClient的所有public/protected方法,暴露了get(),post()的接口
- 难以替换:无法切换到其他http库(如axios -> fetch)
- 违反单一职责原则:UserService 现在有两个职责:1. 业务逻辑(管理用户);2. http通信
- 无法多重继承:如果还想要日志功能,缓存功能该如何处理?
✅ 正确做法:使用聚合(依赖注入)
typescript
class UserService {
constructor(private http: HttpClient) {} // 通过构造函数注入
getUser() { return this.http.get('/user'); }
}
说明 :这里使用的是聚合而非组合:
- HttpClient 从外部创建并注入
- UserService 不管理 HttpClient 的生命周期
- 多个 Service 可以共享同一个 HttpClient 实例
- 便于测试时注入 mock 对象
原因:聚合比继承更灵活,降低耦合度。依赖注入是现代前端框架的标准做法。
2. 混淆聚合与组合
❌ 错误示例:
typescript
// 想要聚合却写成了组合
class Team {
private members = [new Employee(), new Employee()];
}
✅ 正确做法:
typescript
class Team {
private members: Employee[] = [];
addMember(employee: Employee) {
this.members.push(employee);
}
}
原因:员工应该独立创建,可以属于多个团队
3. 依赖导致的重复创建
❌ 错误示例:
typescript
class UserComponent {
loadData() {
const api = new ApiService(); // 每次都创建
api.getUsers();
}
}
✅ 正确做法:
typescript
class UserComponent {
constructor(private api: ApiService) {} // 关联
loadData() {
this.api.getUsers();
}
}
原因:频繁使用的对象应该作为关联持有
4. 循环依赖
❌ 错误示例:
typescript
class A {
constructor(private b: B) {}
}
class B {
constructor(private a: A) {} // 循环依赖
}
✅ 正确做法:引入中介者或事件系统
typescript
class EventBus {
emit(event: string, data: any) {}
on(event: string, handler: Function) {}
}
class A {
constructor(private eventBus: EventBus) {
eventBus.on('b-event', this.handleBEvent);
}
}
class B {
constructor(private eventBus: EventBus) {
eventBus.on('a-event', this.handleAEvent);
}
}
写在最后
理解 OOP 中的6种对象关系不是一蹴而就的,需要在实践中不断体会和应用。
"优秀的设计不是一开始就完美的,而是在不断重构中演化出来的。"