Appearance
UML 类图关系
类图最容易被误用的地方,是把所有连线都画成普通箭头。实际上,不同线型表达的语义差别很大:有的表示“临时使用”,有的表示“长期关联”,有的表示“整体拥有部分”,有的表示“继承抽象能力”。这些差别会直接影响代码依赖、对象生命周期和模块边界。
依赖关系
依赖关系表示一个对象在某个动作中临时使用另一个对象。它画成虚线箭头,箭头指向被依赖的一方。比如 OrderApplicationService 在支付订单时调用 PaymentGateway,应用服务需要网关完成支付,但支付网关不是应用服务的组成部分。
依赖关系对应到代码里,常见形式是方法参数、局部变量、构造器注入的接口或一次调用。它的特点是“我需要你完成某件事,但你不是我的一部分”。如果只是为了发送短信、查询汇率、调用第三方支付,属于依赖,而不是聚合或组合。
关联关系
关联关系表示两个对象之间有稳定联系。它画成实线,可以带方向,也可以标注多重性。比如订单关联客户,订单需要知道购买人是谁;课程关联讲师,课程页面需要展示讲师信息。关联强调对象之间存在业务联系,但不强调生命周期归属。
在实现中,关联可以保存对象引用,也可以只保存对方标识。DDD 中跨聚合关联更推荐保存标识,例如订单里保存 customerId,而不是直接持有完整 Customer 对象。这样可以避免一个聚合被另一个聚合的生命周期拖住。
聚合关系
聚合关系表示弱整体和部分,用空心菱形指向整体。它表达“整体由部分组成,但部分可以脱离整体继续存在”。例如班级和学生可以建成聚合关系,学生离开某个班级后仍然存在;部门和员工也类似,员工转部门后仍然是同一个员工。
聚合关系在现代代码设计中要谨慎使用,因为它的语义比组合弱,容易被画成“看起来有关联”的装饰线。如果部分对象能被多个整体共享,或者生命周期不由整体控制,可以考虑聚合;如果只是普通联系,用关联关系更直接。
组合关系
组合关系表示强整体和部分,用实心菱形指向整体。它表达“部分属于整体,生命周期由整体控制”。订单和订单项就是典型例子:订单项脱离订单没有独立业务意义,删除订单时订单项也应随之失效。房屋和房间、简历和工作经历,也常适合组合表达。
组合关系在代码里意味着整体负责创建、修改和删除部分对象。外部不应绕过整体直接修改部分对象。放到 DDD 中,组合经常出现在聚合内部:聚合根控制内部实体和值对象,保证不变量不被破坏。
泛化关系
泛化关系表示继承,用实线和空心三角箭头指向父类。它表达“子类是父类的一种”。例如 CreditCardPayment、WalletPayment 可以继承抽象的 PaymentMethod,不同支付方式共享基本能力,又保留各自实现。
继承适合稳定的分类层次,不适合为了复用几行代码而随意建立父类。分类经常变化时,继承会让层级僵硬,组合或策略模式更灵活。画泛化关系时,要能读出明确的“是一个”关系,而不是“用到了”或“长得像”。
实现关系
实现关系表示类实现接口,用虚线和空心三角箭头指向接口。接口定义能力,具体类提供实现。例如 JpaOrderRepository 实现 OrderRepository,领域层只依赖仓储接口,基础设施层负责数据库实现。
实现关系特别适合表达依赖倒置。架构图中如果看到领域层接口由基础设施层实现,就能看出依赖方向没有被数据库框架反向牵引。接口应围绕业务能力命名,例如 OrderRepository、PaymentGateway,不要只围绕技术动作命名成 OrderDao 或 HttpClientWrapper。
关系选择
选择类图关系时,可以按业务语义判断。只是方法里临时调用,用依赖;对象之间有长期业务联系,用关联;整体包含部分但不控制生命周期,用聚合;部分无法脱离整体独立存在,用组合;子类确实是父类的一种,用泛化;类提供接口承诺的能力,用实现。
同一张类图中,关系线不必画满。只保留能解释当前设计的关键关系,省略工具类调用和技术细节。类图的目标是让读者看清结构,不是把 IDE 里的所有依赖搬到纸面上。
