1. 项目概述:为什么UML类图是程序员必备的“设计蓝图”?
干了这么多年开发,我见过太多因为前期设计没想清楚,导致后期代码改得面目全非、牵一发而动全身的项目。很多时候,问题不是出在编码能力上,而是团队成员之间、模块与模块之间的“关系”没理清。这时候,一张清晰的UML类图,价值就凸显出来了。它就像建筑师的施工蓝图,在动工(写代码)之前,先把各个“房间”(类)的功能、以及它们之间如何“走动”(关系)规划得明明白白。
今天要聊的,就是UML类图里最核心、也最容易让人混淆的部分——六大关系:依赖、泛化、实现、关联、聚合、组合。别被这些术语吓到,它们本质上描述的就是我们代码里对象之间最常见的几种“打交道”的方式。弄懂它们,你就能看懂别人画的复杂架构图,也能自己画出清晰、准确的设计图,避免在代码评审时被问得哑口无言,或者写出高耦合、难维护的“屎山”代码。
这篇文章适合所有阶段的开发者。如果你是新手,它能帮你建立面向对象设计的思维框架;如果你是有经验的程序员,它能帮你系统梳理这些概念,解决实际设计中模棱两可的困惑。我会用大量贴近实战的代码例子和生活化的类比,把这六种关系掰开揉碎了讲清楚,让你下次画类图时,不再纠结那条线到底该用实线还是虚线,箭头该不该填实。
2. 核心关系总览:一张图理清六大关系的本质区别
在深入每个关系之前,我们得先有个全局观。这六大关系,根据它们的耦合强度(一个类的变化对另一个类的影响程度)和语义,可以清晰地分为三个层次。理解这个层次,是准确使用它们的关键。
为了方便你快速理解和记忆,我把这六大关系的核心特征整理成了下面这个表格。你可以把它当作一个“速查手册”,在画图或者看代码时,如果对某个关系拿不准,回来对照一下,就能豁然开朗。
| 关系类型 | 英文 | 耦合强度 | 图形表示(箭头/线) | 代码体现(典型形式) | 生活化类比 |
|---|---|---|---|---|---|
| 依赖 | Dependency | 最弱 | 虚线 + 箭头(指向被依赖者) | 局部变量、方法参数、静态方法调用、返回值 | 临时借用:像去咖啡馆问店员借支笔,用完即还,关系短暂。 |
| 关联 | Association | 较弱 | 实线 + 箭头(可选,表示导航方向) | 成员变量(引用) | 熟人关系:你知道他的联系方式(持有引用),可以随时联系,但你们彼此独立。 |
| 聚合 | Aggregation | 中等 | 实线 + 空心菱形(菱形在整体端) | 成员变量(引用),整体和部分可独立存在 | 整体与部分:电脑和它的配件(显示器、键盘)。电脑坏了,配件可以拆下来给别的电脑用。 |
| 组合 | Composition | 最强 | 实线 + 实心菱形(菱形在整体端) | 成员变量(引用),整体负责部分的生灭 | 强整体与部分:人和他的心脏。人存在,心脏存在;人消亡,心脏也随之消亡。 |
| 泛化 | Generalization | 强(继承) | 实线 + 空心三角箭头(箭头指向父类) | extends关键字 (Java) | “是一种”关系:猫是一种动物。子类继承父类的特征和行为。 |
| 实现 | Realization | 强(契约) | 虚线 + 空心三角箭头(箭头指向接口) | implements关键字 (Java) | “能做什么”契约:飞行员能驾驶飞机。类实现接口定义的能力。 |
注意:耦合强度是一个非常重要的设计考量。原则是:在满足功能的前提下,优先使用耦合度低的关系。比如,能用依赖(虚线)解决的问题,就不要轻易用关联(实线);能用关联(普通实线)表达的,就不要升级为聚合或组合(带菱形的实线)。这直接关系到代码的灵活性和可维护性。
从上表可以看出,依赖、关联、聚合、组合这四种关系,主要描述的是对象之间结构上的连接方式,而泛化和实现描述的是类与类、类与接口之间在概念层次上的关系。接下来,我们就从最弱的“依赖”开始,逐一深入。
3. 依赖关系:最松散的“临时合作”
依赖关系是UML中使用最广泛,也是耦合度最低的一种关系。它描述的是这样一种情况:一个类(客户类)在某个特定场景下,需要“用到”另一个类(供应类),但这种使用是临时的、偶然的,并不持有对它的长期引用。
3.1 依赖的代码表现与图形表示
在代码层面,只要一个类A用到了另一个类B,但B不是A的成员属性,那么A就依赖于B。常见的场景有:
- 类B作为类A中某个方法的参数。
- 类B作为类A中某个方法的局部变量。
- 类A调用了类B的静态方法。
图形表示:一条虚线箭头,从客户类指向被依赖的供应类。
让我们看一个经典的例子:司机开车。司机并不“拥有”车,他只是在需要驾驶的时候,临时获得一辆车来开。
// 被依赖的类:汽车 public class Car { public void run() { System.out.println("Car is running..."); } } // 依赖Car的类:司机 public class Driver { // 依赖关系体现方式1:方法参数 public void drive(Car car) { // Car作为参数传入 car.run(); } // 依赖关系体现方式2:局部变量 public void drive() { Car myCar = new Car(); // Car作为局部变量创建 myCar.run(); } // 依赖关系体现方式3:静态方法调用 (假设Car有静态方法) // public static void staticMethod() {...} // Driver类中调用:Car.staticMethod(); }对应的UML类图很简单:
[Driver] -----(依赖)----> [Car] 虚线 箭头这张图清晰地告诉我们:Driver类在它的drive方法执行期间,会临时性地用到Car类。
3.2 依赖关系的设计意义与常见误区
依赖关系的核心价值在于其低耦合性。因为Driver并不长期持有Car的引用,所以Driver类的设计非常灵活。今天可以开Car,明天我可以轻易修改drive方法,让它能开Truck、Motorcycle,只要这些类都有run方法(或者通过接口,后面会讲到)。这符合“面向接口编程,而非实现”的原则。
实操心得:在画设计图时,很多初学者容易把“使用”关系都画成关联(实线)。一个简单的判断方法是:问问自己,这个对象是不是当前类“固有”的属性?它的生命周期是否与当前类实例紧密绑定?如果答案是否定的,比如只是某个方法里用一下,那么用依赖(虚线)更准确。过度使用关联线,会让图变得复杂,并暗示了不必要的强耦合,误导后续开发。
在实际项目中,依赖关系无处不在。例如,一个OrderService(订单服务)在生成订单时,可能会临时使用一个IdGenerator(ID生成器)来创建订单号,或者使用一个EmailSender(邮件发送器)来发送确认邮件。OrderService并不需要一直持有这些工具的实例,只需在需要时通过参数传入或内部创建即可。这种设计使得OrderService更容易测试(我们可以传入一个模拟的IdGenerator),也更容易替换具体的实现(比如换一种ID生成算法)。
4. 关联关系:稳定的“熟人”联系
如果依赖是“临时借用”,那么关联就是“长期相识”。关联关系描述的是一个类知道另一个类,并持有对它的长期引用。这种关系比依赖更稳定,耦合度也更高一些。
4.1 关联的代码表现与导航性
在代码中,关联通常体现为一个类的成员变量是对另一个类的对象引用。
图形表示:一条实线,可以带有箭头表示导航方向(知道对方),也可以没有箭头表示双向知晓(相互持有引用)。
继续用司机和车的例子,但这次我们让司机“拥有”一辆车(知道他的车是哪一辆):
public class Car { ... } // 同上 public class Driver { // 关联关系:Driver持有对Car的长期引用 private Car myCar; // 成员变量 // 通常通过构造方法或Setter方法建立关联 public Driver(Car car) { this.myCar = car; } public void setCar(Car car) { this.myCar = car; } public void drive() { if (myCar != null) { myCar.run(); } } }对应的UML类图:
[Driver] ——————> [Car] 实线 箭头这里的箭头从Driver指向Car,表示Driver知道Car(单向关联)。Driver对象一旦被创建并与某个Car关联,在它的生命周期内,只要myCar引用没变,它就一直知道这辆车。
4.2 单向关联与双向关联
关联可以是单向的,也可以是双向的。
- 单向关联:就像上面的例子,只有
Driver知道Car,但Car不知道是哪个Driver在开它。图形上用带箭头的实线表示。 - 双向关联:如果
Car类里也有一个Driver类型的成员变量(比如currentDriver),那么它们就是双向关联。图形上可以用一条没有箭头的实线,或者两条方向相反的实线表示。public class Car { private Driver currentDriver; // ... setter/getter }[Driver] —————— [Car] 无箭头实线
注意事项:双向关联要慎用。因为它增加了耦合度,使得两个类互相依赖,修改其中一个可能会影响另一个,也更容易导致循环引用的问题(特别是在一些序列化或垃圾回收场景下)。在设计时,应优先考虑单向关联,只在确实需要双向查找时才使用它。例如,在订单(
Order)和商品(Product)系统中,订单需要知道包含哪些商品(单向关联足矣),除非你有强烈的需求要从商品快速反查所有包含它的订单,否则不必在Product里维护一个Order列表。
关联关系是面向对象设计中非常基础且重要的一环。它奠定了对象之间协作的结构基础。数据库中的外键映射、MVC模式中控制器(Controller)持有服务(Service)的引用、视图(View)持有模型(Model)的引用等等,都是关联关系的典型应用。
5. 聚合与组合:整体与部分的“生死之交”
聚合和组合是两种特殊的关联关系,它们都用来描述“整体-部分”的关系。但它们的强弱程度和语义有本质区别,是设计中最容易混淆的一对概念。区分它们的关键在于:部分对象的生命周期是否由整体对象控制。
5.1 聚合关系:可分离的“拥有”
聚合表示一种“has-a”的关系,整体对象由多个部分对象组成。但是,部分对象可以脱离整体对象而独立存在。整体和部分的生命周期是独立的。
生活化类比:电脑(整体)和它的外设,如显示器、键盘、鼠标(部分)。电脑组装好了,它“拥有”这些外设。但即使电脑报废了,显示器、键盘依然可以拆下来,接到另一台电脑上继续使用。部分不依赖于整体而存在。
图形表示:实线 + 空心菱形,菱形连接在整体一端。
代码体现:整体类中包含对部分类对象的引用,但通常不负责创建和销毁这些部分对象。部分对象往往是从外部传递进来(通过构造方法或Setter)。
// 部分:车轮 public class Wheel { private String brand; public Wheel(String brand) { this.brand = brand; } // ... getter/setter } // 整体:汽车 public class Car { // 聚合关系:Car由4个Wheel组成,但Wheel是独立存在的 private Wheel[] wheels; // 注意:Wheel不是在Car内部创建的,而是外部传入 public Car(Wheel frontLeft, Wheel frontRight, Wheel rearLeft, Wheel rearRight) { this.wheels = new Wheel[]{frontLeft, frontRight, rearLeft, rearRight}; } // 也可以更换轮胎 public void changeWheel(int position, Wheel newWheel) { if (position >= 0 && position < wheels.length) { wheels[position] = newWheel; // 替换一个部分 } } } // 使用场景 public class Test { public static void main(String[] args) { // 先创建独立存在的“部分” Wheel w1 = new Wheel("Michelin"); Wheel w2 = new Wheel("Michelin"); Wheel w3 = new Wheel("Bridgestone"); Wheel w4 = new Wheel("Bridgestone"); // 再将它们“聚合”到“整体”中 Car myCar = new Car(w1, w2, w3, w4); // 即使myCar销毁了,w1, w2, w3, w4这些Wheel对象依然存在(如果还有其他引用的话) } }类图表示:
[Car] <>————— [Wheel] 空心菱形 实线5.2 组合关系:同生共死的“包含”
组合是一种比聚合更强的关系,也表示“has-a”,但它是一种严格的整体与部分关系,部分不能脱离整体而独立存在。整体的生命周期完全控制部分的生命周期:整体被创建时,部分随之被创建;整体被销毁时,部分也随之被销毁。
生活化类比:公司(整体)和部门(部分)。公司成立了,才会设立市场部、研发部等部门。如果公司倒闭了,这些部门也就不复存在了。部门不能脱离公司独立存在(作为该公司部门的概念)。
图形表示:实线 + 实心菱形,菱形连接在整体一端。
代码体现:整体类中包含对部分类对象的引用,并且通常负责创建这些部分对象。部分对象在整体对象的构造方法内部new出来。
// 部分:引擎 public class Engine { public void start() { System.out.println("Engine started."); } } // 整体:汽车 public class Car { // 组合关系:Engine的生命周期由Car管理 private Engine engine; // 关键:Engine在Car的构造方法内部创建 public Car() { this.engine = new Engine(); // Car负责创建Engine } public void start() { engine.start(); System.out.println("Car started."); } // 当Car对象被垃圾回收时,其内部的engine对象也随之无法被访问,符合“同生共死”的语义。 }类图表示:
[Car] ◆————— [Engine] 实心菱形 实线5.3 聚合与组合的抉择:一个关键的设计考量
如何决定用聚合还是组合?我总结了一个简单的决策流程:
问:部分对象能否在逻辑上独立于整体对象存在?它是否具有独立的意义?
- 能-> 考虑聚合。例如,
Wheel(轮胎)可以单独生产、销售、库存,它可以属于不同的Car。 - 不能-> 考虑组合。例如,
Engine(引擎)虽然物理上可以拆下,但在业务逻辑和设计语境下,这台特定的引擎就是为这辆特定的Car制造的,它们是一个不可分割的完整实体。更典型的例子是Window(窗口)和Frame(窗体),窗口不能脱离窗体存在。
- 能-> 考虑聚合。例如,
问:整体对象是否独占部分对象?部分对象是否被多个整体共享?
- 独占,不共享-> 倾向于组合。例如,一个
Order(订单)包含多个OrderItem(订单项),这些订单项专属该订单,不会被其他订单共享。 - 可共享-> 倾向于聚合。例如,多个
Professor(教授)可以属于同一个Department(院系),同时一个教授也可能参与多个科研项目(Project),这里Department和Professor之间就更适合用聚合。
- 独占,不共享-> 倾向于组合。例如,一个
看代码创建方式(这是一个很强的提示,但非绝对):
- 部分在整体的构造器/初始化块内
new出来 ->强烈提示组合。 - 部分通过外部传入(参数)设置给整体 ->强烈提示聚合。
- 部分在整体的构造器/初始化块内
常见问题与排查:在团队协作中,对聚合和组合的误用是设计争议的常见来源。例如,把本该是聚合的关系画成了组合,会误导开发者认为部分必须由整体创建,限制了设计的灵活性。反之,把组合画成聚合,则可能忽略了整体对部分生命周期的管理责任,导致内存泄漏或状态不一致(例如,整体销毁了,但部分还被其他地方引用着)。我的经验是,当你不确定时,优先使用聚合,因为它的约束更少,给未来留出的变更空间更大。只有当确有必要表达“同生共死”的强所属关系时,才使用组合。
6. 泛化关系:经典的“是一种”继承
泛化关系就是面向对象编程中的继承。它描述的是类与类之间“一般”与“特殊”的关系,即“is-a”关系。子类(派生类)是父类(基类)的一种特殊形式,它继承了父类的属性和方法,并可以添加自己特有的属性和方法,或重写父类的方法。
图形表示:实线 + 空心三角形箭头,箭头从子类指向父类。
代码体现:使用extends关键字(在Java、PHP等语言中)。
// 父类(基类、超类):动物 public class Animal { private String name; public void eat() { System.out.println(name + " is eating."); } // ... getter/setter } // 子类(派生类):猫,它是一种特殊的动物 public class Cat extends Animal { // 泛化关系 public void meow() { System.out.println("Meow!"); } // 可以重写父类方法 @Override public void eat() { super.eat(); // 调用父类方法 System.out.println("... and it's fish!"); } } // 子类:狗,它也是一种特殊的动物 public class Dog extends Animal { public void bark() { System.out.println("Woof!"); } }类图表示:
[Cat] ——————▷ [Animal] 空心三角箭头 [Dog] ——————▷ [Animal]这个箭头方向很容易记:箭头指向更一般、更抽象的方向(父类)。
6.1 泛化的核心价值与使用陷阱
泛化的最大好处是代码复用和多态。通过将公共的属性和行为抽取到父类,避免了重复代码。多态则允许我们以统一的接口(父类类型)操作不同的子类对象,极大地提高了程序的扩展性。
Animal myPet = new Cat(); myPet.eat(); // 输出:... is eating. ... and it's fish! (多态,调用Cat的eat) // myPet.meow(); // 编译错误!父类引用看不到子类特有方法 Animal[] pets = {new Cat(), new Dog()}; for (Animal pet : pets) { pet.eat(); // 同一个调用,不同行为 }实操心得:继承是一把“双刃剑”。它虽然强大,但滥用会导致设计僵化,最典型的问题是脆弱的基类问题和继承层次过深。
- 脆弱的基类问题:修改父类可能会无意中破坏所有子类的功能。因此,设计父类时要格外谨慎,优先考虑将类设计为
final或提供稳定的API。- 过度继承:不要为了复用一点点代码就轻易使用继承。要严格遵循“is-a”原则。例如,
Administrator(管理员)继承User(用户)是合理的,但Circle(圆)继承Rectangle(矩形)来获得面积计算功能就是不合理的(圆不是矩形)。在这种情况下,应该使用组合(将一个AreaCalculator作为属性)或者接口。- 优先使用组合而非继承:这是很多设计模式(如策略模式、装饰器模式)的核心思想。组合提供了更大的灵活性,降低了类之间的耦合度。在不确定是否用继承时,问问自己:“子类真的是父类的一种吗?未来会不会有不符合‘is-a’逻辑的新子类加进来?” 如果答案模糊,用组合更安全。
7. 实现关系:履行“契约”的承诺
实现关系描述的是一个类实现了一个接口。接口定义了一组方法签名(契约),而实现类则负责提供这些方法的具体实现。这是一种“can-do”关系。
图形表示:虚线 + 空心三角形箭头,箭头从实现类指向接口。
代码体现:使用implements关键字。
// 接口:定义“可飞行”的契约 public interface Flyable { void fly(); // 只有方法声明,没有实现 } // 实现类:鸟,它能飞行 public class Bird implements Flyable { // 实现关系 @Override public void fly() { System.out.println("Bird is flying with wings."); } } // 实现类:飞机,它也能飞行 public class Airplane implements Flyable { @Override public void fly() { System.out.println("Airplane is flying with engines."); } }类图表示:
[Bird] - - - - - ▷ [Flyable] 虚线 空心三角箭头 [Airplane] - - - ▷ [Flyable]注意,这里用的是虚线,以区别于继承的实线。
7.2 接口与抽象类的选择:何时用实现,何时用泛化?
这是面向对象设计中的一个经典问题。接口和抽象类都可以用于定义抽象类型,但它们有显著区别:
| 特性 | 接口 (Interface) | 抽象类 (Abstract Class) |
|---|---|---|
| 定义关系 | 实现关系(can-do, 能力) | 泛化关系(is-a, 本质) |
| 方法 | 全是抽象方法 (Java 8前),可有默认/静态方法 (Java 8+) | 可包含抽象方法和具体实现方法 |
| 属性 | 只能是public static final常量 | 可以有各种类型的成员变量 |
| 构造器 | 没有 | 有,但不能实例化 |
| 多重继承 | 一个类可实现多个接口 | 一个类只能继承一个抽象类 |
| 设计目的 | 定义行为契约,实现多态 | 提供代码复用的模板,定义部分实现 |
选择策略:
- 当你需要定义一种能力或角色,并且毫不相关的类都可能具备这种能力时,用接口。比如
Flyable(可飞)、Serializable(可序列化)。Bird和Airplane本质不同,但都能飞。 - 当你需要为一些紧密相关的类提供一个公共的基类,其中包含一些共享的状态(属性)或行为(方法实现)时,用抽象类。比如游戏中的
GameCharacter(游戏角色)抽象类,可能包含health(血量)、position(位置)属性和move()方法的默认实现,然后Player(玩家)和Enemy(敌人)来继承它。
注意事项:在现代Java开发中,由于接口可以拥有默认方法(
default method),其能力得到了很大增强。一个常见的趋势是:优先使用接口。因为接口能提供最大的灵活性(支持多重实现),并通过默认方法也能提供一些基础实现。只有当确实需要定义非静态、非final的成员变量,或者需要控制子类构造过程时,才考虑使用抽象类。遵循“接口定义行为,抽象类提供部分实现”的原则,能让你的系统更松耦合、更易扩展。
8. 综合案例:用六大关系设计一个简易电商系统
纸上得来终觉浅,我们把这些关系放到一个具体的、简化的电商系统场景里,看看它们是如何协同工作的。假设我们要设计用户下单的核心领域模型。
8.1 领域模型类图设计
我们识别出以下几个核心类:
User(用户)Address(地址)Product(商品)Order(订单)OrderItem(订单项)PaymentService(支付服务接口)AlipayPaymentService(支付宝支付服务)
它们之间的关系如下:
User拥有Address(一个用户可以有多个收货地址)。这是聚合关系,因为地址可以独立存在(即使用户注销,地址信息作为历史记录可能仍需保留)。User是整体,Address是部分。Order属于一个User。这是关联关系(单向即可,订单知道用户)。Order由多个OrderItem组成。这是组合关系,因为订单项不能脱离订单存在(订单删除,其项也应删除)。Order是整体,OrderItem是部分。OrderItem关联一个Product。这是关联关系,订单项引用商品信息。Order在支付时依赖于PaymentService。这是依赖关系,因为支付服务只是在Order.pay()方法中被临时使用。AlipayPaymentService实现了PaymentService接口。这是实现关系。- (假设我们还有
VipUser继承自User,这是泛化关系)。
用类图表示出来,就是一个综合运用了多种关系的设计:
[User] <>————— [Address] (聚合) [User] ◁————— [Order] (关联) [Order] ◆————— [OrderItem] (组合) | | —————— [Product] (关联) [Order] ..> [PaymentService] (依赖) △ | (实现) | [AlipayPaymentService] [VipUser] ——————▷ [User] (泛化)8.2 核心代码片段解析
我们聚焦于Order、OrderItem和PaymentService,看看组合、关联和依赖在代码中如何体现。
// 1. 接口与实现(实现关系) public interface PaymentService { boolean pay(BigDecimal amount); } public class AlipayPaymentService implements PaymentService { @Override public boolean pay(BigDecimal amount) { System.out.println("Paid " + amount + " via Alipay."); // 调用支付宝SDK... return true; } } // 2. 商品与订单项(关联关系) public class Product { private Long id; private String name; private BigDecimal price; // ... getters/setters } public class OrderItem { private Product product; // 关联关系:持有Product的引用 private Integer quantity; public OrderItem(Product product, Integer quantity) { this.product = product; this.quantity = quantity; } public BigDecimal getItemTotal() { return product.getPrice().multiply(new BigDecimal(quantity)); } // ... getters/setters } // 3. 订单(组合 + 依赖) import java.util.ArrayList; import java.util.List; public class Order { private String orderId; private List<OrderItem> items; // 组合关系:OrderItem的生命周期由Order管理 private BigDecimal totalAmount; public Order() { this.items = new ArrayList<>(); // 组合的关键:整体负责创建部分集合 this.totalAmount = BigDecimal.ZERO; } // 组合:添加订单项,通常在Order内部逻辑中创建OrderItem public void addItem(Product product, Integer quantity) { OrderItem item = new OrderItem(product, quantity); // Order创建OrderItem items.add(item); calculateTotal(); } private void calculateTotal() { this.totalAmount = items.stream() .map(OrderItem::getItemTotal) .reduce(BigDecimal.ZERO, BigDecimal::add); } // 依赖关系:pay方法依赖PaymentService接口 public boolean pay(PaymentService paymentService) { // PaymentService作为参数传入 if (paymentService == null) { throw new IllegalArgumentException("Payment service is required."); } // 临时使用paymentService完成支付 return paymentService.pay(this.totalAmount); } // 当Order对象被销毁时,其内部的items列表以及列表中的每个OrderItem对象 // 如果没有其他引用,也会被垃圾回收,这体现了组合“同生共死”的语义。 }8.3 设计思路复盘与经验总结
通过这个案例,我们可以清晰地看到不同关系如何各司其职:
- 组合(Order-OrderItem):确保了订单数据的完整性和一致性。订单项是订单不可分割的一部分,它们的生命周期绑定在一起,这符合业务逻辑。
- 关联(OrderItem-Product):订单项需要知道它所购买的商品信息(快照),但这只是一个引用。商品可以独立于任何订单存在,被多个订单项引用。
- 依赖(Order-PaymentService):订单不需要持有一个固定的支付服务实例。它可以在支付时接受任何实现了
PaymentService接口的对象。这带来了巨大的灵活性:今天用支付宝,明天可以轻松换成微信支付,只需传入不同的实现类,而无需修改Order类的代码。这是依赖倒置原则和策略模式的体现。 - 实现(AlipayPaymentService-PaymentService):定义了支付能力的契约,让具体的支付方式可以灵活扩展。
避坑技巧:在设计类似系统时,一个常见的错误是把
Order和Product直接关联(比如在Order里放一个List<Product>)。这忽略了购买数量、单价快照等关键信息。正确的做法是引入OrderItem这个中介类,它既关联了Product,又记录了本次交易的具体信息(数量、成交价)。这体现了“组合”模式的精髓,也是领域驱动设计(DDD)中聚合根和实体概念的雏形。画对类图,能帮你提前发现这类设计缺陷。