第一次交类图是在一个图书借阅系统的小项目里我把数据库那六张表原封不动抄了一遍字段名、主键、外键关系全搬到图上连线清一色实线加箭头自我感觉整齐得很。评审时带我的老哥盯了三秒问了一句读者逾期三天要罚款这条规则落在哪个类的哪个属性上我当场卡壳。那张图里只有数据没有规则。后来才慢慢想明白面向对象分析阶段的类图跟数据库 ER 图、跟详细设计阶段的类图压根不是一回事。ER 图回答数据怎么存分析类图回答业务里有哪些概念、它们各自背什么责任、彼此怎么打交道。前者面向存储后者面向行为混在一起画出来的东西看着像图其实是张表。这篇内容我打算把面向对象分析里类图这条线从头捋一遍先讲清楚它在整个建模流程里的位置再拆属性、方法、可见性的取舍标准接着把依赖、关联、聚合、组合、泛化这五种关系的箭头画法和区别讲透然后给出 StarUML、Visio、IDEA、Eclipse 四条工具链的实际操作路径最后用一组对照实验把五种关系摆在一起看差异附上一份评审自查清单。刚从需求转向建模的人、要交课程设计的学生、想把团队画法统一起来的技术负责人都能直接拿去用。1. 分析阶段类图到底该承载什么信息1.1 分析类图和设计类图的边界在哪同样一张方框加连线站在分析阶段和设计阶段看取舍标准完全不同。分析阶段面对的是需求文档和业务访谈记录这时候画类图的目标是把业务领域里的概念和职责固化下来让业务方和开发方对同一套名词达成共识。设计阶段面对的是已经确定的技术栈类图要回答的是接口怎么切、依赖怎么注入、缓存放在哪一层。判断自己画的是哪一种有个很实用的土办法看图上有没有框架的痕迹。如果出现了ServiceImpl、Mapper、Controller、Repository、CacheManager这类名字说明已经滑到设计阶段了分析类图里出现的应该是读者借阅记录逾期罚金规则图书副本这种业务语言。我见过太多团队在需求评审会上甩出一张满是XxxDTOXxxVO的图业务方全程沉默因为这个图对他们来说等于天书。分析阶段还有个经典做法是引入边界类、控制类、实体类三层分类也就是常说的 BCE。实体类对应业务里那些需要长期记住状态的东西比如图书“读者”边界类负责系统和外部角色的交互界面比如借阅登记窗口控制类承接一次用例里的业务编排比如借阅处理器。三层分开画好处是职责一眼能看出错位——如果你发现某个实体类里塞了一堆校验输入调用外部服务的动作那基本可以断定职责被放错了地方。需要提醒的是分析类图不必追求完整。它的价值在于暴露分歧不在于画得好看。一张只画了八个类、但每个类的职责都讨论清楚的图比一张塞了六十个类、没人看得懂的图有用得多。1.2 活动图管流程类图管结构两者怎么衔接热词里有个面向对象分析之活动图说明很多人是在同一个建模任务里同时遇到这两张图的。它们的关系其实很直白活动图描述的是时间维度上的动作序列类图描述的是空间维度上的静态结构。活动图里每一个动作节点理论上都应该能对应到某个类的一个操作类图里每一个操作理论上都应该能在某个活动图或时序图里被调用到。我在实际项目里常用的衔接手法是反向点名。画完活动图之后把图里所有动作节点列成一张清单逐个问这个动作是谁在做如果答不上来说明这个动作要么是遗漏的类要么是系统自动触发的定时任务需要单独标记。反过来画完类图之后把每个类的公开操作列出来问这些操作在哪个流程里被用到用不到的操作要么是设计过度要么是流程漏了分支。举个具体例子。读者提交借阅申请这个动作主语是读者提交这个动作实际执行者是读者类的一个方法参数是图书副本返回是借阅结果。顺着这条线往下推活动图里的系统校验可借数量就落到图书副本类的方法上生成借阅记录落到借阅记录类的构造过程上。这样一推两张图就咬合住了而不是两张各自独立的画。1.3 从需求文本里捞出候选类的实操方法新手最常问的问题就是我怎么知道该画哪些类。有一个流传很广但确实好用的做法名词短语抽取法。把需求文档里的名词和名词短语全部圈出来然后做三轮筛选。第一轮剔除同义词和近义词。比如读者用户借阅人在同一段需求里指同一个角色就只保留一个把另外两个作为别名记在注释里。这一轮不做后面就会出现三个类互相引用同一份数据的荒唐局面。第二轮剔除系统外部和无关词。项目名、公司名、部门名、纯粹的时间词、纯形容词一律不要。判断标准很简单——问一句这个东西需要在系统里被记住状态吗。不需要就删掉。第三轮做属性还是类的分流。留下来的候选词里有一部分其实是别的类的属性。判断方法我后面第 2 节会细讲简单说就是看它有没有自己的行为和生命周期。三轮之后留下来的就是初步的候选类。拿一个电商下单的需求片段试一下原始文本里的名词有用户、收货地址、商品、商品规格、订单、订单明细、优惠券、支付方式、物流单号。筛完之后收货地址要不要单独成类取决于它是不是需要校验、格式化、支持多个地址标签——如果只是下单时填一行字符串它就是订单的一个属性如果要做地址簿、默认地址、地址自动补全它就是一个独立的类。这个判断没有标准答案取决于需求深度但必须显式讨论并留下结论不能糊弄过去。2. 属性、方法、可见性的取舍标准2.1 什么该升级成类什么该留在属性里属性还是类这个问题我总结的判断顺序是先看它有没有独立的行为再看它有没有独立的生命周期最后看它有没有多值或者可复用的需求。三条占一条就倾向成类三条都不占就老老实实当属性。还拿地址举例。如果它只是一串文本用户输入完存下来展示那它就是Order上的一个address: String成类反而是过度设计。但如果需求里出现了地址格式校验根据地址计算运费一个用户维护多个地址并设置默认地址地址里的省市区要单独统计那它就有了行为、有了多值需求必须升级成Address类让User与它形成一对多。另一个高频误判是金额。很多图里会看到Money类也很多图里直接写amount: double。这里的关键不是金额是不是类而是要看业务复杂度。如果只有一个币种、不需要精度处理规则、不需要汇率换算BigDecimal类型的属性足够如果涉及多币种、含税不含税、四舍五入规则那Money类就是必要的甚至要拆成金额加币种两个部分。判断依据永远是业务复杂度而不是别人图里怎么画。还有个容易忽略的点属性尽量不要跨类重复。如果你发现订单号同时出现在Order、OrderItem、Payment三个类上那说明OrderItem和Payment只是缺一条指向Order的关联而不是真的需要存三份。属性冗余往往是从 ER 图思维带过来的后遗症。2.2 分析阶段要不要写方法写多少我的做法是分析阶段写职责设计阶段写签名。也就是说类框里可以出现计算逾期罚金()这样的中文或英文方法名但不写参数类型、不写返回类型、不写异常声明。理由很实在——分析阶段参数类型大概率是错的写死了反而会限制后面的设计空间而一个方法名足以表达职责归属评审时看的是这件事该谁干不是参数是 List 还是 Set。有一种情况例外如果这个方法就是核心对外契约参数和返回值已经和业务方确认过了那写完整反而更好因为它能防止后面实现时悄悄改语义。至于可见性符号分析阶段的纪律可以概括成一句话默认私有必要时才公开。-表示 private表示 public#表示 protected~表示 package-private。分析类图上大面积出现属性是危险信号那意味着任何外部类都可以直接改它的内部状态封装等于没做。我的习惯是把属性全部标-只把真正需要对外暴露的行为标其余方法一律-。这个习惯带来的好处在后面写代码时会非常明显——你不需要再纠结该不该开 getter。2.3 控制类的体重和上帝类的识别分析类图里最常见的结构性病症叫上帝类一个类挂了十几个方法、连了七八条线、什么流程都从它这里过。这种类在图上看着很壮观落地的时候就是灾难因为所有人都要改它一改就冲突。识别它有个量化办法数一下这个类的关联数量。如果一个类直接关联的对象超过七个这个数字来自经验不是铁律基本可以判定它承担了过多职责。另一个信号是看它的方法名里有没有大量管理处理调度综合这类词——OrderManager、DataProcessor这种名字八成是个筐。拆解思路是按变化原因切分。同一个类里如果一部分方法随着促销规则变化另一部分随着物流策略变化那它们就属于两个不同的类因为它们的变更节奏不一样。这一点其实和单一职责原则是一回事但在图上看比在代码里看直观得多——图上多一根线代码里可能就是一个耦合点。3. 五种关系的画法与区别一次说清楚这一节是类图里最容易翻车的地方。箭头画反、菱形画错端、实线虚线混用几乎每个团队的新人都会犯一轮。先把结论摆出来下面逐条展开。关系线条形态箭头/菱形位置语义生命周期依赖虚线 开放箭头指向被依赖方临时使用无关联实线可带开放箭头箭头指向被导航方长期持有引用各自独立聚合实线 空心菱形菱形在整体端弱拥有部分可独立组合实线 实心菱形菱形在整体端强拥有同生共死泛化实线 空心三角三角指向父类is-a 继承—实现虚线 空心三角三角指向接口契约实现—3.1 依赖和关联被合并得最多的一对依赖是五种关系里最弱的一种表示我用一下你但我不留着你。典型场景是方法参数、方法内的局部变量、静态工具调用。图上画成虚线加一个开放箭头箭头指向被使用的那一方。关联表示我长期知道你并且我持有你的引用通常是类的属性。图上画实线如果要表达单向可见性就在指向被导航方的那一端加开放箭头。为什么这两个最容易混因为它们在代码里可能长得一模一样。看下面两段// 依赖只在方法参数里出现用完就走 class ReportPrinter { void print(Report report) { System.out.println(report.toText()); } } // 关联作为字段长期持有 class ReportPrinter { private Report defaultReport; // 长期持有 }区别在于持有时间。参数里的对象方法执行完就没了字段里的对象只要宿主对象活着它就活着。判断的时候问自己一句这个引用需要跨方法保存吗需要就是关联不需要就是依赖。依赖画得过多是分析类图虚胖的典型原因。我见过一张图几乎每两个类之间都有虚线理由是反正都要用到。这种画法等于没画因为依赖太普遍就失去了信息量。我的建议是只画那些值得注意的依赖具体的、跨模块的、容易引起变更传播的依赖优先画同一模块内部的琐碎调用可以省略。3.2 聚合和组合菱形画在谁那边的生死问题聚合和组合都表示整体-部分用的都是实线区别只在菱形是空心还是实心以及部分能不能脱离整体独立存在。聚合是空心菱形菱形画在整体那一端表示弱的拥有关系。班级和学生是典型聚合班级解散了学生还在还能转到别的班级去。组合是实心菱形同样画在整体那一端表示强的拥有关系部分的生命周期由整体决定整体没了部分也就没意义了。订单和订单明细就是典型组合订单被删掉明细没有独立存在的价值。订单和收货地址更接近聚合地址是可以复用的。代码上大概是这样// 组合宿主自己创建部分不对外暴露 class Order { private final ListOrderItem items new ArrayList(); void addItem(Product p, int qty) { items.add(new OrderItem(p, qty)); // 部分由整体创建 } } // 聚合部分从外部传入可以被共享 class Clazz { private final ListStudent students; Clazz(ListStudent students) { // 部分由外部提供 this.students students; } }注意菱形永远画在整体那一端。我见过太多人把菱形画在部分那一端读图的人会把语义完全理解反。检查方法很简单——读一遍整体包含部分菱形应该靠近整体那个框。还有一个常见误区是把组合当成万能药。有同学觉得组合听起来更强就到处用实心菱形。结果落到代码里明明需要共享的对象被强制私有创建想换一个实现都换不了。组合是约束聚合是自由约束多的地方意味着变更成本高别随便给自己上锁。3.3 泛化与实现空心三角的两个版本泛化表示 is-a 关系实线加空心三角三角指向父类。实现表示对接口契约的履行虚线加空心三角三角指向接口。两者的共同点是三角都是空心的区别只在线条是实线还是虚线。这两个关系本身不难画难的是判断该不该用。判断标准我一般用里氏替换来测如果子类实例能替换父类实例而不改变程序正确性那继承是合理的。如果子类里有一堆方法抛不支持该操作的异常那说明这个继承关系是硬凑的应该改用组合。另一个实用的检查是问是不是同一个业务家族的成员。储蓄账户和信用卡账户继承自账户是合理的订单继承自用户就是胡来它们根本没有共同的语义。3.4 多重性、角色名和导航性别只画根线就完事一根关联线只画线是不够的至少要补三个信息多重性、角色名、导航性。多重性写在线的两端常用的有1、0..1、*、1..*、2..5。一个订单对应几个客户写1。一个客户对应几个订单写*。这些数字不是装饰它们直接决定代码里是单个引用还是集合也直接决定数据库里要不要加唯一约束。角色名写在线的两端用来描述对方在这个关系里扮演什么角色。同一个Person类在订单-客户关系里叫customer在订单-审核员关系里叫reviewer如果两个关系都连到Person而不标角色名读图的人根本分不清哪根线是哪根。导航性用箭头表示。有箭头表示单向可见无箭头表示双向可见在 UML 里无箭头其实表示未指定但实践中绝大多数团队把它当双向理解。我的习惯是尽量标单向因为在代码里双向引用会带来同步问题——一边改了另一边忘了改bug 就来了。4. 四条工具链StarUML、Visio、IDEA、Eclipse 怎么画4.1 StarUML 从零画一张类图StarUML 是学生和自由项目里用得最多的工具原因是它对 UML 的原生支持比较全聚合组合泛化都有独立工具栏按钮不像通用绘图工具需要自己找形状。完整路径大概是这样File → New在模板对话框里选 UML 相关的模板新版本会直接给UMLMinimal、UMLComplete这类选项。左侧 Model Explorer 里右键根节点选择Add Diagram → Class Diagram画布就出来了。画类框左下角 Toolbox 里选Class在画布上点一下双击框体改名字。加属性右键这个类Add → Attribute或者直接在框里按回车逐行敲。加操作右键Add → Operation。属性的可见性在右侧属性面板改Visibility下拉有 Public/Private/Protected/Package 四个选项选完框里会自动显示 - # ~符号。画关系Toolbox 里选Association从一个类按住拖到另一个类松手后线就出来了。然后选中这条线右侧属性面板里有Aggregation下拉默认是None改成Shared就是空心菱形的聚合改成Composite就是实心菱形的组合。泛化用 Toolbox 里的Generalization工具实现用Interface Realization。依赖用Dependency。多重性怎么加选中线的一端右侧属性面板里有End1、End2两组属性各自的Multiplicity填1或*就行角色名填在Role里。导出File → Export Diagram As可以选 PNG、SVG、JPEG。要交作业的话建议导出 SVG 或者高分辨率 PNG不然投影出来糊成一片。提示StarUML 不同大版本的菜单文案会有出入但属性面板的逻辑一直没变——关系类型靠属性面板改不靠换工具按钮。找不到某个线型时先画成普通关联再去属性面板改。4.2 Visio 画 UML 类图的坑Visio 的优势是同事电脑上大概率已经装了直接共享文件就能看不用额外装软件。劣势也很明显——它是通用绘图工具UML 只是其中一个模板很多行为需要手工维护。起步路径文件 → 新建在模板分类里找软件和数据库选UML 类图部分版本叫UML 模型图。打开后左侧形状模具里会有一整套 UML 形状包括类接口关联聚合组合泛化依赖。第一个坑是连接线的粘附方式。Visio 默认用动态连接线会粘到形状边界上形状一移动线就自动绕路几张类框挪一挪图就变得歪七扭八。解决办法是在视图选项卡里勾上连接点然后用连接线工具从形状上的连接点拖到另一个形状的连接点这样线会按静态连接点固定。第二个坑是类形状的编辑。Visio 的类形状分三段类名、属性、操作。双击类名区改名字双击中间区加属性双击底部加操作。但它的可见性符号需要自己手打 - #不会自动根据某个属性字段生成。第三个坑是关系的端点在视觉上容易搞反。Visio 的组合形状和聚合形状是两种不同形状菱形位置由形状本身决定所以选错形状就会画错。画完之后一定要读一遍语义再做检查。第四个坑是Visio 不生成代码。它只是一张图不要指望从图里反向同步代码。这也是我建议正式项目里用 StarUML 或者 IDEA 的原因——至少有一边是自动化的人工维护的图迟早会和代码脱节。4.3 IDEA 反向生成类图IDEA 的类图功能是从已有代码反向生成不是正向建模。这一点要先搞清楚不然会白折腾。操作入口有两个在项目树里右键某个类或者某个包选Diagrams → Show Diagram或者选中类之后按CtrlAltShiftUMac 上对应CommandOptionU。第一次打开会让你选是弹窗显示还是新建标签页选哪个都行。图出来之后默认只显示类名想看属性和方法点图上方的工具栏按钮或者在设置里调Fields、Methods、Constructors、Properties的显示开关。想加类进图直接从项目树里拖进来或者右键图空白处选Add Class to Diagram。想加某个类的父类、子类、依赖右键这个类选Show Parents、Show Children、Show Dependencies。导出右键图的空白处选Export to File可以导 PNG、SVG装了 PlantUML 插件的话还能导.puml文本格式方便进版本库。注意IDEA 的类图功能只在 Ultimate 版本提供社区版没有。社区版想要类似效果得装 PlantUML Integration 插件手写 PlantUML 文本再预览。这个插件的好处是图能用文本保存跟代码一起做 diff团队协作时比二进制图片文件友好太多。4.4 Eclipse 查看和生成类图的几条路Eclipse 原生不带 UML 类图功能需要装插件。常用的有两个方向一是Papyrus这是 Eclipse 建模项目里的正式 UML 工具装了建模相关组件包之后就有。它支持完整的 UML 语义画出来的图规范但上手成本偏高界面偏重。二是ObjectAid UML Explorer这个插件的好处是能直接在 Eclipse 里反向生成类图安装完在项目上右键新建Class Diagram然后从包资源管理器里把类拖进图里就行。它对关系的识别是自动的——代码里是继承就画空心三角是字段引用就画关联用起来很省事。需要留意的是免费版对图里的元素数量有限制类比较多的项目会提示超限。如果只是想看某个类的继承层次或者调用关系其实不用装 UML 插件Eclipse 自带的CtrlT快速类型层次和F4打开类型层次视图就够用了看继承链非常快。工具正向建模反向生成关系线型支持导出格式适用场景StarUML强支持部分语言完整PNG/SVG/JPEG课程设计、需求评审Visio中无完整靠形状PNG/SVG/PDF公司已有 Office 生态IDEA弱强完整PNG/SVG/PUMLJava 项目代码梳理Eclipse插件中强ObjectAid完整PNGJava 项目、老工程5. 五种关系对照实验同一段业务画五遍把五种关系摆在一起看差异最快的办法是做一次对照实验。我选了一个足够简单的场景一家书店里的店员、书、书架、订单、支付方式。下面把五种关系各自铺开每一组都给出图形特征、代码落点和观察到的差异。5.1 实验设计先固定业务场景再动手实验用的五个类Bookstore书店、Staff店员、Book书、Order订单、Payment支付。业务规则固定为书店雇佣店员书店里放着一批书书是从出版社买来的、可以调拨到别的书店去订单一旦生成就独占它的付款信息订单取消付款记录也随之作废店员处理订单时会用一个打印工具把订单打出来支付分为现金支付和扫码支付两种。固定场景的意义在于五种关系的差异就只剩图上的画法其他变量都被控制住了。很多人做这个练习的时候每次换不同场景画完根本比不出什么因为差异被业务复杂度掩盖了。5.2 逐种关系的画法与观察结论第一组依赖。店员用打印工具打印订单。画法是虚线加开放箭头从Staff指向Printer。观察点在于如果哪天换成另一种打印方式只需要改Printer这一端Staff的结构完全不动。依赖的耦合度最低代价是每次用都要重新建立联系没法缓存状态。第二组关联。书店和店员之间。画法是实线Bookstore端标多重性1..*Staff端标1店员端加角色名employer。观察点这是双向的长期关系代码里就是字段引用。如果要做连锁书店的人员调配这条关联就是数据模型的基础。第三组聚合。书店和书。书是从外面进的货可以调拨走书店倒闭了书还能退给出版社或者转给别的店。画法是实线加空心菱形菱形在Bookstore这一端。观察点聚合的对象通常从外部创建再注入所以构造函数里会接收一个集合参数而不是内部new出来。第四组组合。订单和支付信息。订单取消付款记录跟着作废没有独立价值。画法是实线加实心菱形菱形在Order这一端。观察点代码里Payment应该由Order自己创建构造函数可以直接new甚至可以做成内部类或者不可变对象。第五组泛化。现金支付和扫码支付继承自支付。画法是实线加空心三角三角指向Payment。观察点泛化改变的不是对象之间的引用方式而是类型层次它影响的是多态分派。这一组和前四组不冲突实际项目里经常是前四组和泛化混着用。实验做完之后最容易发现的一个现象是同一个业务对象在不同上下文里可能对应不同的关系。书和书店之间是聚合书和出版社之间可能更接近组合版次作废对应的书也就下架了。所以关系的选择不是给类贴永久标签而是针对某一段具体业务语义下的判断。这也解释了为什么两个团队画同一个系统的类图答案可能都对但都不一样——他们关心的业务侧面不同。5.3 从图到代码的映射校验画完图之后我习惯做一个小的校验动作照着图把关键代码骨架写出来看有没有写不出来的地方。写不出来的地方往往是图上缺信息写起来别扭的地方往往是关系选错了。比如图上画了一条关联但没有标多重性写代码的时候就会卡住——是单个引用还是List比如图上画了组合但代码里发现这个部分对象需要被别的模块共享那说明组合选错了应该改成聚合。再比如图上画了泛化但写的时候发现子类要覆写父类七成以上的方法那说明继承关系太勉强。这个校验不用写完整实现写空方法体就够了成本很低但能拦住不少返工。6. 评审自查清单和最常见的返工原因6.1 交图前过一遍这份清单评审会上被问到的问题八成集中在下面这些点上。交图之前自己先过一遍能省掉一轮返工。检查项合格标准常见问题类名业务名词单数首字母大写出现 DTO、ServiceImpl 等实现术语属性可见性属性一律 private大量公开属性方法粒度只写职责不写签名分析阶段就写死参数类型关系类型与业务语义一致聚合组合混用菱形画在部分端多重性每根关联线两端都有大量关联线光秃秃角色名同一类被多次关联时必标两根线连到同一个类分不清导航性尽量标单向全部省略读图靠猜孤岛类没有零关联的类存在完全孤立、说不清归属的类上帝类单类关联数不超过七个一个类连了十几根线命名一致同一概念全文一个名字读者/用户/借阅人混用6.2 三个反复出现的返工原因第一个是把数据库外键当组合画。一个订单关联一个客户代码里有customer_id字段很多同学看到外键就画实心菱形。但客户不是订单的一部分删掉订单客户还在这明明是关联。判断方法还是那句话——看生命周期谁决定谁。第二个是粒度不统一。同一张图上有些类粗到系统级别有些类细到某一行配置项级别。这种图读起来极累因为读者要在不同抽象层次之间反复跳。解决办法是一张图只画一个抽象层次需要展示更细的粒度就单独开一张图用包或者子图的方式组织。第三个是图和代码长期脱节。需求变了代码改了图还是三个月前那一版。这几乎是所有手工维护 UML 图的团队的通病。缓解办法有两条一是项目里至少保证一份自动生成的图IDEA 或者 Eclipse 插件生成它天然跟着代码走二是手工画的分析类图明确标注版本和日期只在需求评审时使用不作为长期维护对象。搞清楚每张图的生命周期比纠结画得对不对更重要。我个人在实际项目里踩过最大的一个坑是早期把分析类图当成设计文档一路维护到项目结束结果每改一次需求就改一次图改到最后图比代码还复杂没人愿意看。后来调整做法分析类图只在需求阶段产出、只服务于业务对齐讨论完就归档真正跟代码同步的那份图完全交给 IDEA 反向生成。两套图分工明确之后团队里再没人抱怨图又过期了。最后分享一个小技巧如果团队里对箭头和菱形的画法总是记不牢可以在白板上写一句话贴在旁边——实线看持有虚线看使用空心三角指父类菱形贴着整体。这四句念顺了五种关系基本就不会画反了。