尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
设计模式地基:六种类关系与UML判定实战
1. 类关系是设计模式的地基1.1 为什么学模式要先过类关系这道关设计模式学到一定程度很多人会卡在一个地方单个模式看讲解都能懂一画UML图就露馅两个模式往一起组合就懵。我碰到过不少读者私信问我说策略模式和状态模式到底区别在哪、装饰器模式和代理模式怎么长得这么像。其实这些问题的根源往往不在模式本身而在类与类之间的关系没拎清。23种设计模式说白了就是在编排类与类之间的互动方式。创建型模式处理的是“谁创建了谁”结构型模式处理的是“谁持有谁、谁包装谁”行为型模式处理的是“谁调用了谁”。这些“谁和谁”落到代码层面最终都能归到六种基本类关系上依赖、关联、聚合、组合、继承、实现。每一种设计模式都是这六种关系的某种组合应用。举几个例子你就明白了。策略模式的核心是Context类与Strategy接口之间的关联关系运行时通过替换不同的具体策略实现来改变行为。观察者模式里Subject持有Observer的列表这是一种聚合关系。装饰器模式里装饰器拿着被装饰对象的引用同时又继承同一个抽象类这是关联加继承的混合。工厂模式里工厂方法和具体产品之间是依赖加实现。可以说类关系不理解透彻设计模式就永远是浮在表面的名词。所以这一篇我不急着讲新模式先把类与类之间的六种关系掰开揉碎。这不仅是设计模式的共同语言也是你以后看源码、画架构图、评审别人代码时判断设计好坏的基本功。1.2 先建立六种关系的整体认知在深入细节之前先给你一张六种关系的总览表。这张表我建议你截图保存或者抄在笔记本上。后续所有模式的分析你都可以对照这张表来看。关系类型强度典型代码特征UML表示记忆锚点依赖最弱局部变量、方法参数、静态调用虚线 箭头临时借工具用完即还关联弱成员字段实线 箭头可双向长期合作伙伴互相认识聚合中成员字段对象从外部传入空心菱形 实线整体与部分可分可合组合强成员字段对象由整体new出来实心菱形 实线同生共死不可分割继承/泛化强类继承类空心三角 实线is-a我是你的一种实现中类实现接口空心三角 虚线遵守契约履行约定这里的关系强度是站在“类与类之间耦合程度”的维度去排序的。依赖最弱因为是一次性的临时接触组合最强因为两部分的生命周期完全绑定。实战中类关系从弱到强对应的代码修改影响范围也从局部到整体。越弱的关系越容易替换和测试这也是为什么设计模式一直在强调“面向接口编程”“组合优于继承”——本质上是希望你把类之间的耦合控制在一个合适的强度。接下来我按强度从弱到强把每种关系拆开讲。2. 六种关系的判定口诀与细节拆解2.1 依赖关系方法里一闪而过的临时接触依赖是最容易理解也最容易被忽视的关系。它的定义很直白一个类在某个方法的实现过程中临时使用到了另一个类使用完之后不保留任何痕迹。这个“临时使用”通常表现为三种形式方法内部new了另一个类的对象、方法参数中接收了某个类并在方法内调用其方法、直接调用某个类的静态方法。用代码说话class Logger { public: void log(const std::string msg) { // 依赖Formatter只是局部使用 Formatter fmt; std::string formatted fmt.format(msg); std::cout formatted std::endl; } };这个例子里Logger对Formatter就是依赖关系。Logger的方法里创建了Formatter对象用完就销毁Logger不持有Formatter的任何成员字段Formatter也感知不到Logger的存在。判定口诀方法里一闪而过用完就丢不存字段。为什么依赖关系值得专门讲因为它是六种关系里耦合度最低的也是你最容易无意识忽略的。很多人画类图的时候完全不画依赖关系觉得它太琐碎。但在设计模式的语境里依赖的方向往往决定了扩展点在哪里。比如工厂模式里Creator依赖于Product接口——注意是接口不是具体类。如果Creator直接依赖具体产品类那每加一种新产品就要改Creator违反开闭原则。如果依赖接口新产品的扩展就不需要动Creator了。这里的实战体会是依赖虽然弱但它直接影响你的代码能不能“拔插”。依赖具体类是死结依赖接口才是活口。这也是为什么很多设计模式强调“不要让高层模块依赖低层模块而要都依赖抽象”——说的就是依赖关系的方向问题。2.2 关联关系类与类之间的长期合作伙伴关联关系比依赖强一个档次。当一个类把另一个类作为自己的成员字段持有并且这种持有关系是长期存在的二者之间就是关联关系。关联关系可以单向也可以双向可以是一对一也可以是一对多。继续用C示例class Teacher { private: std::vectorStudent* students_; public: void addStudent(Student* s) { students_.push_back(s); } }; class Student { private: Teacher* teacher_; public: void setTeacher(Teacher* t) { teacher_ t; } };这个例子里Teacher持有Student的列表Student又持有Teacher的指针这就是双向关联。如果只有一边持有就是单向关联。判定口诀你认识我我也认识你或单方面认识关系持续存在不随方法调用结束而消失。关联关系是设计模式中出现频率最高的关系。策略模式里Context持有Strategy的引用观察者模式里Subject持有Observer的列表模板方法模式里子类持有父类的引用……这些都是关联。为什么模式偏爱关联因为它是一种“可替换”的关系——被关联的对象可以在运行时被替换成另一个实现只要接口不变就行。实操中有个容易踩坑的点双向关联到底该不该用很多新手为了图方便让两个类互相持有对方的指针结果代码耦合得死死的改一方就要动另一方。我的建议是能单向就不要双向。如果确实需要双向访问优先考虑用观察者的思路来做反向通知而不是直接互相引用。你画类图时也应该明确标注关联的方向避免别人误用。2.3 聚合关系整体与局部的松散共存聚合是关联关系加强版它多了一层语义整体与部分。聚合表达的是“整体拥有部分”的主从关系但整体和部分在生命周期上互不依赖。经典例子是公司与员工公司倒了员工还在可以跳槽去别的公司员工走了公司也还能继续招人。菱形放在整体那一端。代码上聚合和关联的写法几乎一样区别在于语义和接口。聚合关系中部分对象的创建通常不是由整体来完成的而是外部创建好之后传进来。看这个例子class Company { private: std::vectorEmployee* employees_; public: void hireEmployee(Employee* e) { employees_.push_back(e); } }; class Employee { // 员工不持有Company引用单向聚合即可 };Company的hireEmployee方法接收外部传入的Employee指针Company负责管理这些员工但不负责创建和销毁他们。这种“外部传入、整体管理”的代码结构就是典型的聚合。判定口诀整体没了部分还能活部分原本就属于外部整体只是“借用”它。聚合关系在模式中的典型代表是观察者模式。Subject被观察者持有Observer观察者的列表但Observer对象完全是外部创建、外部管理的。Subject销毁时Observer们还活得好好的。还有一个容易忽略的点聚合有个重要的放松规则——部分可以被多个整体共享。一个员工可以同时是A公司外包、又在B公司兼职这在聚合语义下是完全合法的。实战中的价值在于当你需要实现“整体-部分”结构时如果部分对象的生命周期独立于整体那就用聚合给整体提供一个添加和删除部分的接口而不是在整体内部直接new出部分对象。2.4 组合关系整体与局部的同生共死组合是聚合的进一步强化。组合关系里整体与部分是“同生共死”的绑定关系。整体被创建时部分也随之创建整体销毁时部分一并销毁。部分的存在完全依赖整体。经典例子是人跟心脏的关系人没了心脏也就不复存在反过来心脏不可能脱离人体而独立保存。代码上组合的典型特征就是“整体在内部new出部分”。构造函数里创建部分对象析构函数里销毁部分对象。看代码class Human { private: Heart* heart_; public: Human() { heart_ new Heart(); } ~Human() { delete heart_; heart_ nullptr; } };Human在自己的构造函数里创建了Heart对象在析构函数里负责销毁。Human销毁的瞬间Heart的生命也就结束了。这就是组合。判定口诀整体没了部分也得没部分由整体亲手创造也由整体亲手埋葬。组合关系在设计模式里同样无处不在。装饰器模式中装饰器对被装饰对象的关系很微妙装饰器持有被装饰对象的引用同时装饰器又继承同一个抽象组件类所以既有聚合的成分又有继承的成分。但更典型的组合是桥接模式里的Implementor以及状态模式里Context对State的管理——Context在初始化或状态切换时负责创建和替换State对象State的生命周期由Context完全掌控。区分聚合和组合有个非常实用的判断方法问自己两个问题第一部分离开整体后还能存活吗能就是聚合不能就是组合。第二部分能被另一个整体共享吗能共享更趋向聚合不能共享就趋向组合。我见过很多人在这两个关系上栽跟头画图时菱形画空的还是实心的拿捏不准。你只要记住代码特征就够了构造时new出来的是组合外部传进来的是聚合。不用纠结语义上的细微差别。2.5 继承泛化与实现两种血缘关系继承和实现放在一起讲因为它们都涉及“类和类之间最直白的关系”一个表达is-a我是你的一种一个表达“我遵守你的契约”。继承泛化在C里用class Dog : public Animal表示在Java里用extends。Animal是父类Dog是子类。继承表达的是“特化”语义子类拥有父类的一切属性和行为并可以扩展或重写。UML中继承用实线加空心三角形表示三角形指向父类。实现关系在C里体现为抽象类和派生类或者接口类和实现类在Java里体现为implements关键字。UML中实现用虚线加空心三角形表示三角形指向接口。// 继承 class Animal { public: virtual void speak() 0; }; class Dog : public Animal { public: void speak() override { std::cout Woof! std::endl; } }; // 实现 class IFlyable { public: virtual void fly() 0; }; class Bird : public IFlyable { public: void fly() override { // 实现飞行行为 } };注意在C里抽象类和接口在语言层面没有Java区分得那么干净所以“继承”和“实现”在C里都表现为公有继承。但这不影响设计层面的区分继承父类表达的是“复用与特化”实现接口表达的是“能力与契约”。判定口诀继承是“我是你的一种”实现是“我遵守你的约定”。关于继承我多说两句。继承关系是六种关系里耦合度极高的关系因为子类会无条件获得父类的所有实现细节——包括你不想暴露的那些。所以GoF在《设计模式》里特别强调“组合优于继承”。不是说完全不用继承而是要克制。模板方法模式和工厂方法模式是继承用得比较多的场景因为它们需要在父类里定义骨架算法让子类实现可变部分。但一旦你发现继承层次超过三层或者子类为了复用父类的某个方法而被迫承担了无关的职责就该停下来考虑用组合或委托替代了。3. 从代码和UML两个维度把握类关系3.1 UML类图的六种画法画类图是理解设计模式的必修课。很多教材上来就丢一张复杂类图容易劝退新手。其实UML类图里跟类关系相关的画法就六种先记箭头比记标准简单得多。关系类型UML画法方向提示依赖虚线 普通箭头箭头指向被依赖的类关联实线 普通箭头可选箭头指向被持有的类可标1*表示数量聚合空心菱形 实线菱形在“整体”那端组合实心菱形 实线菱形在“整体”那端继承实线 空心三角三角形指向父类实现虚线 空心三角三角形指向接口这里面最关键的细节是菱形和三角的摆放方向。聚合和组合的菱形永远画在整体那一侧它表示“这个类是主人”。继承和实现的空心三角永远指向父类或接口它表示“箭头指向被依赖的抽象层”。我见过不少新手画反了方向把菱形画到部分一侧这就完全颠倒语义了。画图时心里默念一遍菱形代表“我有你”所以在整体侧三角代表“我是你”所以指向你。另外我一直推荐手画UML图而不是一上来就上工具。手画可以强迫你在动笔之际把关系理一遍而工具会自动生成连线看似快了反而容易掩盖你的认知盲区。等你能随手画出六种模式以上的类图再用工具也不迟。3.2 代码里辨关系的四个信号对应到代码判断类之间是什么关系只需要看四个信号。这个方法是我在实际工作中总结出来的用来快速审查别人代码的设计意图特别实用。第一个信号目标类出现在方法的局部变量或参数里用完就丢——这是依赖。第二个信号目标类出现在成员字段里并且这个对象不是本类创建而是外部传入——这是关联或聚合。第三个信号目标类出现在成员字段里并且本类在构造函数里new了它——这是组合。第四个信号类声明时继承了某个类或者在重写某个接口的方法——这是继承或实现。在代码层面就写一个典型的组合示例帮大家巩固注意构造时new出来的就是更强的关系。class Order { private: std::vectorOrderItem* items_; public: Order() { // OrderItem在Order内部创建生命周期绑定属于组合 items_.push_back(new OrderItem(apple, 2)); items_.push_back(new OrderItem(banana, 5)); } ~Order() { for (OrderItem* item : items_) { delete item; } items_.clear(); } };在业务代码里我发现一个很有意思的规律很多类关系混淆其实都发生在“新对象到底是谁创建的”这个环节。你只要盯住new关键字出现在哪儿——如果是出现在构造函数里大概率是组合如果是外部创建后传进来大概率是聚合或关联。这个方法屡试不爽。4. 常用设计模式中的类关系实例4.1 从类关系角度重新拆解经典模式掌握了六种关系之后再回头看设计模式你会看到完全不同的风景。模式不再是一个个孤立的名词而是一张张由类关系编织成的网。我挑几个典型的模式给你画出它们内部的关系谱系。策略模式Context到Strategy是聚合关系Context持有Strategy引用运行时可替换具体策略类到Strategy接口是实现关系。所以策略模式的主干就是“聚合 实现”的组合套件。Context不会在内部new具体的策略类而是从外部注入这保证了它的可扩展性。观察者模式Subject到Observer是聚合关系Subject持有Observer列表外部注册ConcreteSubject继承SubjectConcreteObserver实现Observer接口。观察者模式的关键是Subject聚合了Observer而且这个聚合关系里的Observer可以随时注册和注销所以是典型的聚合而非组合。装饰器模式这个稍微复杂一点。装饰器类实现了Component接口或继承抽象组件同时又持有Component类型的引用。所以装饰器到组件接口是实现关系装饰器到被装饰对象是聚合关系因为被装饰对象是从外部传入的。这种“继承接口 聚合对象”的组合很有意思——既遵守了同一份契约又可以用递归的方式套一层又一层的装饰器。工厂模式Creator工厂到Product是依赖关系因为工厂方法内部会创建ProductConcreteFactory继承CreatorConcreteProduct实现Product接口。你会发现工厂模式的核心就是把“依赖具体类”转变成“依赖抽象接口”让创建对象的责任从使用者手里剥离出来。单例模式比较特殊它最核心的关系是类到自身的关系——一个类持有自己的唯一静态实例。这算是关联关系的一个变种自己认识自己。状态模式Context到State是组合关系Context内部可以创建并替换State对象具体状态类到State接口是实现关系。注意状态模式和策略模式在结构上几乎一致区别在于策略替换是客户端主动选择的状态替换是状态自身在运行时触发的。你看一旦用类关系去切分两个模式的差异立刻清楚了。组合模式Component是抽象组件Leaf继承Component实现关系Composite继承Component并且持有Component的子节点列表组合关系。组合模式是“继承 组合”的典型代表目的是让树形结构里的叶子节点和分支节点可以被一致对待。我用一张表把这几个模式的类关系汇总一下方便对照复习设计模式主要类关系关系强度策略模式Context聚合Strategy中观察者模式Subject聚合Observer中装饰器模式装饰器继承Component且聚合被装饰者中-强工厂方法模式Creator依赖Product具体工厂继承Creator弱-中状态模式Context组合State强组合模式Composite组合子节点节点继承Component中-强4.2 类关系强弱如何影响模式选型理解了类关系之后最大的收获不是在考试里认出某个模式而是你在设计一个新模块时能更精准地判断该用哪种关系。举个例子。你要做一个游戏中的角色系统角色有攻击行为、防御行为、移动行为。新手最常见的做法是定义一个超大类把所有技能都塞进去子类继承覆盖。用不了多久就发现不同角色的攻击方式完全不一样继承体系臃肿不堪。这时候如果你心里有“继承是强耦合聚合/关联是弱耦合”这个概念你自然会想到把技能抽象成接口让角色去持有技能对象——策略模式就这么出来了。再举个例子。如果一个模块需要管理一组子任务子任务的生命周期严格跟随主任务的生命周期主任务销毁子任务也销毁——这是组合关系直接用内部持有的方式实现最合适。如果子任务可以被多个主任务共享生命周期独立——这是聚合关系就应该由外部创建子任务后再传入。掌握类关系的强度其实就是在帮你做耦合度决策。弱关系带来的灵活性和可测试性高但有时候也会因为过度抽象带来理解成本强关系实现简单直接但牺牲了扩展空间。真正好的设计是在不同的场景选择恰到好处的耦合强度而不是一味追求低耦合。高内聚低耦合说的是同一层级的组件之间不是让你把所有类都拆成互相无关的碎块。5. 类关系学习的常见误区和排查经验5.1 常见的判断误区我梳理一下最近几年带人和答疑过程中见到的高频误区。第一依赖和关联分不清。判断依据很简单看那个对象有没有成为你类的成员字段。出现在字段里就是关联只出现在方法参数或局部变量里就是依赖。有人会杠方法参数里接收的对象调用了很多方法这算不算关联不算参数用完就丢没有持久化到字段里就是依赖。第二聚合和组合分不清。这个问题上面讲过判定法则就是看由谁创建。整体构造时new出来的组合外部造好了传进来的聚合。再加一个辅助判据问一句“部分离开了整体还能活吗”。活不了的是组合能活的是聚合。第三继承和实现分不清。其实在C里这俩在语法层面是同一个东西但在设计语义上有明确界限。继承父类是为了复用代码和表达“is-a”实现接口是为了表达“具备某种能力”。你要是看到某个类继承了一个只有纯虚函数的基类那它本质上是实现关系不是继承关系。第四画出双向关联就画两条线。UML规范里双向关联是一条线两个箭头不是两条线。这是很多新手画图的常见扣分点也会让图变得很乱。一条线上两端各标上多重性比如1对*一眼就能看懂。5.2 实战排查技巧五连问拿到一段不熟悉的代码判断类关系时我会在脑子里过一套问题清单你也可以试试。第一问这个类在哪些位置出现局部变量、参数还是字段第二问这个对象是谁创建的本类内部还是外部创建第三问这个对象的生命周期和本类同步吗同步则组合不同步则聚合或关联。第四问类声明语句是继承还是实现接口第五问这段代码的关注点是复用行为还是能力契约是复用行为用继承能力契约用实现。这套五连问基本可以覆盖日常会遇到的关系判断场景。我自己在代码评审时经常用这个思路去引导新人比直接告诉他答案要有效得多。5.3 我个人的学习建议最后分享一点我在实战中沉淀下来的学习心法。设计模式这个东西千万不要孤立地背名字一定要自己画图。每学一个模式就动手画它的UML类图在边上标注出每一种类关系的类型和方向。画得多了你会有一种“模式都是关系组合”的通透感。我自己当年学设计模式的时候把一个画好的架子上贴上便签写上这几种关系的记忆锚点依赖是借工具关联是交朋友聚合是搭班子组合是过命的交情继承是亲生的实现是打工仔。虽然糙了点但确实帮助我在项目里快速识别关系。你可以找你自己的记忆方式重要的是把“关系”这个概念嵌进日常写代码的思考里而不是把它当成考试的知识点。熟练之后建议你反过来做拿到一个开源项目打开一个核心类不着急看里面的逻辑先找它跟哪些类有依赖关系、跟哪些类是关联关系、构造函数里new了谁、继承了谁。把这些关系列出来再去看它的行为逻辑你会发现阅读源码的速度和理解深度都会有明显提升。这一套方法是我认为类关系除了应付考试之外最值钱的实战价值。
RELATED

相关推荐

同需求横评:Ming-Image、FLUX.2、Ideogram 4.0 谁的字最不糊、排版最稳

同需求横评:Ming-Image、FLUX.2、Ideogram 4.0 谁的字最不糊、排版最稳

同需求横评:Ming-Image、FLUX.2、Ideogram 4.0 谁的字最不糊、排版最稳 【免费下载链接】Ming-Image-0.1-Design 项目地址: https://ai.gitcode.com/hf_mirrors/inclusionAI/Ming-Image-0.1-Design 文字渲染是文生图模型的"照妖镜":风…

📅 2026/10/10 22:54:28
nginx 是否真的启动了?四层验证法告别误判

nginx 是否真的启动了?四层验证法告别误判

凌晨一点半被群里的告警吵醒,登进服务器第一件事就是敲ps -ef | grep nginx,看到几个 nginx 进程挂在进程表里,我心里踏实了一半,回了一句“nginx 没事,进程在”。结果前端同事截图过来,页面还是 502。我盯…

📅 2026/10/10 22:54:28
从懵逼到真香:Salvo 框架 24 小时上手实战

从懵逼到真香:Salvo 框架 24 小时上手实战

作为一个写了几年 Rust 却在 Web 领域反复骑墙的人,我对 Rust 后端框架的态度一直很纠结。Actix-web 性能强但路由写法让我总隔着一层,Axum 类型设计漂亮但动不动就要跟 trait 搏斗,Rocket 的宏魔法好用可又依赖 nightly 特性。直到某天刷 cr…

📅 2026/10/10 22:54:28
MORE NEWS

更多资讯

📰

让 AI 助手记住你读过的网页:Hister MCP 接口接入实战

让 AI 助手记住你读过的网页:Hister MCP 接口接入实战 【免费下载链接】hister Your own search engine 项目地址: https://gitcode.com/GitHub_Trending/hi/hister 大模型越来越擅长"回答",却很难"记得"你上周读过什么。它不…

📰

火焰识别不一定要CNN:BP神经网络从特征提取到火灾报警落地

简介:基于BP神经网络的火焰识别项目,面向机器学习初学者与图像识别研究者,提供一套完整可运行的MATLAB实现方案,用于火焰与火灾图像的自动分类。压缩包内共有八百四十五个文件,包括八百一十三张JPG样本图像、十七个MAT…

📰

LSTM城市人口预测MATLAB实战:数据处理、训练与调参避坑

简介:面向城市人口预测与时间序列建模需求,这份基于MATLAB长短期神经网络(LSTM)的完整代码包,适合本科及以上阶段的研究者、竞赛选手及工程应用人员。资源内含人口数据Excel表格、四个MATLAB脚本及三十余张运行截图&am…

📰

睡岗与玩手机检测数据集:VOC标注转YOLO训练实战

简介:这份睡岗与玩手机行为检测数据集,面向安防监控、工位值守、智慧园区等场景的算法开发与研究人员,可用于训练人员违规行为识别模型,解决长时间值班场景下的脱岗、注意力分散等安全隐患。该数据集面向4653张原始图像构建&#…

📰

Blume内容编写完全指南:Frontmatter、Markdown语法与页面Includes机制详解

【免费下载链接】blume The open-source docs framework for humans and agents. 项目地址: https://gitcode.com/gh_mirrors/blum/blume 点击查看 免费下载 Blume 是一个面向人类与 AI Agent 的开源文档框架(docs framework),它…

📰

二维Navier-Stokes方程的Fourier-Galerkin谱方法:MATLAB实现与去混叠详解

简介:这是一份用Fourier-Galerkin谱方法求解二维Navier-Stokes方程的MATLAB实现,适合流体力学数值模拟方向的研究生、科研人员及对谱方法感兴趣的MATLAB开发者。资源基于傅立叶级数展开与Galerkin变分原理,结合四阶Runge-Kutta时间推进&#…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬