当前位置: 首页 > news >正文

UML类图六大关系详解:从依赖到组合,掌握面向对象设计核心

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。常见的场景有:

  1. 类B作为类A中某个方法的参数。
  2. 类B作为类A中某个方法的局部变量。
  3. 类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方法,让它能开TruckMotorcycle,只要这些类都有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 聚合与组合的抉择:一个关键的设计考量

如何决定用聚合还是组合?我总结了一个简单的决策流程:

  1. 问:部分对象能否在逻辑上独立于整体对象存在?它是否具有独立的意义?

    • -> 考虑聚合。例如,Wheel(轮胎)可以单独生产、销售、库存,它可以属于不同的Car
    • 不能-> 考虑组合。例如,Engine(引擎)虽然物理上可以拆下,但在业务逻辑和设计语境下,这台特定的引擎就是为这辆特定的Car制造的,它们是一个不可分割的完整实体。更典型的例子是Window(窗口)和Frame(窗体),窗口不能脱离窗体存在。
  2. 问:整体对象是否独占部分对象?部分对象是否被多个整体共享?

    • 独占,不共享-> 倾向于组合。例如,一个Order(订单)包含多个OrderItem(订单项),这些订单项专属该订单,不会被其他订单共享。
    • 可共享-> 倾向于聚合。例如,多个Professor(教授)可以属于同一个Department(院系),同时一个教授也可能参与多个科研项目(Project),这里DepartmentProfessor之间就更适合用聚合。
  3. 看代码创建方式(这是一个很强的提示,但非绝对):

    • 部分在整体的构造器/初始化块内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(可序列化)。BirdAirplane本质不同,但都能飞。
  • 当你需要为一些紧密相关的类提供一个公共的基类,其中包含一些共享的状态(属性)或行为(方法实现)时,用抽象类。比如游戏中的GameCharacter(游戏角色)抽象类,可能包含health(血量)、position(位置)属性和move()方法的默认实现,然后Player(玩家)和Enemy(敌人)来继承它。

注意事项:在现代Java开发中,由于接口可以拥有默认方法(default method),其能力得到了很大增强。一个常见的趋势是:优先使用接口。因为接口能提供最大的灵活性(支持多重实现),并通过默认方法也能提供一些基础实现。只有当确实需要定义非静态、非final的成员变量,或者需要控制子类构造过程时,才考虑使用抽象类。遵循“接口定义行为,抽象类提供部分实现”的原则,能让你的系统更松耦合、更易扩展。

8. 综合案例:用六大关系设计一个简易电商系统

纸上得来终觉浅,我们把这些关系放到一个具体的、简化的电商系统场景里,看看它们是如何协同工作的。假设我们要设计用户下单的核心领域模型。

8.1 领域模型类图设计

我们识别出以下几个核心类:

  1. User(用户)
  2. Address(地址)
  3. Product(商品)
  4. Order(订单)
  5. OrderItem(订单项)
  6. PaymentService(支付服务接口)
  7. 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 核心代码片段解析

我们聚焦于OrderOrderItemPaymentService,看看组合、关联和依赖在代码中如何体现。

// 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):定义了支付能力的契约,让具体的支付方式可以灵活扩展。

避坑技巧:在设计类似系统时,一个常见的错误是把OrderProduct直接关联(比如在Order里放一个List<Product>)。这忽略了购买数量、单价快照等关键信息。正确的做法是引入OrderItem这个中介类,它既关联了Product,又记录了本次交易的具体信息(数量、成交价)。这体现了“组合”模式的精髓,也是领域驱动设计(DDD)中聚合根和实体概念的雏形。画对类图,能帮你提前发现这类设计缺陷。

http://www.jsqmd.com/news/1380612/

相关文章:

  • 留学生课程论文的MLA和APA格式怎么选
  • CMake project命令详解:从基础语法到跨平台构建实战
  • 2026邯郸外墙漏水避坑指南 - 企业资讯
  • 构建以Request_ID为核心的全链路追踪与智能诊断系统
  • 2026旅游行业找GEO优化厂家全攻略:正规服务商盘点、选型核心标准与签约避坑FAQ,附艾奇在线(27GEO.com)行业适配深度解析 - U渠道
  • CE实战:从浮点数原理到双精度修改,攻克内存逆向核心难点
  • 武汉装修公司怎么选?从设计、施工到售后,意米装饰全方位测评 - 品牌红黑榜
  • Python包管理革命:uv如何实现比pip快5-9倍的极速安装
  • 这家福州猎头公司在Mini LED显示领域抢挖工艺大师,成败细节全解析 - 榜单推荐
  • 爱普生RTC小批量采购10问:选型、拿货、避坑一篇讲透
  • 世人皆赞冬梅香,谁识三伏梅无声
  • UE5蓝图开发者C++进阶指南:从节点到代码的性能优化与功能扩展
  • 2026邢台外墙漏水避坑指南 - 企业资讯
  • 2026外贸邮件获客工具选型参考:主流服务商对比、避坑指南与适配推荐
  • 2026服装零售企业GEO优化厂家盘点:选靠谱服务商的标准、避坑指南及行业适配厂商推荐 - 商业大观
  • Python动态因子模型实战:高维时间序列降维与预测
  • 全志A40i学习笔记(一):烧写镜像
  • 武汉装修公司红黑榜:意米装饰凭啥连续被业主群推荐?我跑了3个工地 - 品牌红黑榜
  • 通信原理第6章-数字基带传输
  • MATLAB多峰高斯拟合实战:从原理到三峰分离的完整指南
  • 详解TSRF(热力学仿真辅助随机森林)的实现步骤
  • 2026昆明外墙漏水避坑指南 - 企业资讯
  • ARM64 Linux中断向量表与汇编处理全解析:从硬件响应到C语言入口
  • 2026曲靖外墙漏水避坑指南 - 企业资讯
  • 北京网站建设学校揭秘:普通人如何低成本逆袭互联网高薪赛道
  • 2026海关数据获客工具选型指南:跨境魔方等主流外贸平台对比评估
  • 2026 年新消息:钟楼值得关注的工业节能吊扇生产商哪家强,车间用电突然降了三成,原来秘密藏在这台大家伙身上! - 行业鉴选官
  • VSCode配置C/C++开发环境:从零搭建轻量级高效编程平台
  • 科研课题查新报告可以加急办理吗?一般多久能拿到?
  • 26款主流Agent工具大全:从办公型Agent、Agent搭建平台、行业垂直Agent、手机 Agent