FEATURED · 精选文章

UML包图全解析:从建模基础到架构依赖治理实践

发布时间 / 2026/9/17 16:04:27
来源 / 创域科博编辑部
栏目 / 资讯中心
UML包图全解析:从建模基础到架构依赖治理实践 1. 从“画包图”到“用包图”包图到底在建模什么1.1 包图的真正价值不是“分组”先说个我在项目里见过无数次的场景方案评审会上架构师打开一张包图图上画着六七个方框每个方框里写了一行注释“业务层”“数据层”“工具类”箭头稀疏地连着几根。旁边的人点头表示看懂了但散会之后开发人员写代码时根本不会照着这个图走三个月后这张图连画图的人自己都懒得维护了。这不是包图没用而是大多数人对包图的定位搞错了。包图不是用来给代码“拍一张组织照”的它是系统建模时用来做架构决策与依赖治理的核心工具。在UML统一建模语言的全家桶里类图管的是“某个类长什么样”时序图管的是“一次调用怎么流转”而包图管的是“整个系统的高层结构如何被组织、边界划在哪里、模块之间允不允许互相依赖”。换句话说类图回答的是“微观”问题包图回答的是“宏观”问题。在计算机系统建模的教学和实践中包图Package Diagram被归入静态结构图Static Structure Diagrams这一类别。它描述的是模型本身的组织方式你可以把一组类、接口、用例、组件甚至嵌套的子包放进一个包里然后用依赖箭头表达它们之间的关系。这个“装东西”的机制让包图天然成为连接高层设计与底层实现的桥梁。我个人的体会是包图在系统建模中的真正价值有三点。第一它是系统复杂度的“收纳盒”几十上百个类如果不分组任何新成员进来都会陷入“大海捞针”的状态。第二它是架构原则的“可视化载体”依赖方向、环形依赖、层级边界这些抽象概念只有画成图才容易被讨论和评审。第三它是团队协作的“契约文档”大家约定好“业务层只能依赖接口层、不能直接碰基础设施”这个约定写进文档没人看画成包图配合依赖检查工具就成了机器可验证的硬约束。1.2 包图在系统建模中的位置有读者可能会问包图跟部署图、组件图有什么区别这个话题确实容易混淆。我做一个简单的区分UML图类型关注层次典型问题类图实现层某个类有哪些属性、哪些方法类之间是什么关系包图架构层系统被划分为哪些模块模块之间的依赖方向是什么组件图运行单元层系统由哪些可部署的组件组成组件通过什么接口交互部署图物理环境层组件部署在哪些节点上网络连接如何配置包图更抽象、更“顶层”。在完整的需求分析与系统设计流程中包图往往出现在架构设计阶段需求分析已经产出了用例图和领域模型接下来要决定“代码库怎么分包”“模块边界怎么划”这时候包图就该登场了。它也贯穿后续的迭代过程每当架构调整、新功能引入时包图都是评估影响范围的第一站工具。举个真实例子。我在做一个订单中台项目时系统拆分成了商品、库存、订单、支付、会员五个业务域每个域又分应用层、领域层、基础设施层。刚开始没有认真设计包依赖方向结果两个月后代码库里出现了“订单域直接调用了支付域的Mapper类”这种跨层穿透调用。后来我专门组织了一次包图评审把所有跨包依赖列出来才发现这种破坏架构边界的调用有二十多处。把包图画清楚、把依赖规则定下来这类问题的处理才有了依据。2. 包图核心元素拆解别再把包图当“方框加箭头”2.1 包Package的表示与嵌套包在UML中的标准图形是一个类似文件夹的图标主体是一个矩形左上角有一个小的“标签页”。如果你用专业建模工具如StarUML、Enterprise Architect新建一个Package时工具会自动画出这个样式。如果只是白板上手绘画一个大矩形、左上角标注包名大家也能看懂——毕竟沟通的目的是清晰不是形式合规。包里面可以放什么几乎UML里的所有模型元素都可以被放进包里包括类、接口、用例、组件、数据表等。更关键的是包还可以嵌套包形成树状的层级结构。这种嵌套在表达“子模块划分”时非常有用比如订单域这个包里可以嵌套应用服务、领域服务、仓储接口三个子包。关于包的一个核心语义是命名空间。在UML中包是一个命名的容器包内的元素具有唯一标识例如带路径的com:order:domain:Order。这一点跟Java的package、Python的目录包在概念上完全一致。也就是说你在建模阶段画出的包结构几乎可以一对一映射到后续代码的包名设计。这也是为什么我觉得包图是所有UML图中“落地性最强”的一种。需要注意的是包本身不描述业务逻辑它只提供一种“物理”或“逻辑”上的组织手段。物理上的包对应代码目录逻辑上的包可以对应业务域、架构层、技术支持组件等不同视角。实践中我倾向于同一个包图里混用这两种视角时要格外小心——比如com.x.order.application是架构视角com.x.order.shared是技术视角它们并存没有错但依赖规则必须分别说明清楚否则看图的人会被绕晕。2.2 四种依赖关系不要看到虚线就画带箭头的包图最常见的关系是依赖UML里用一条带箭头的虚线表示。但很多人不知道的是依赖关系其实分了好几种构造型stereotype语义完全不同。«use»使用最泛化的依赖表示一个包中的元素使用了另一个包中的元素。比如订单应用服务使用了订单领域对象那就是application包依赖domain包。这个依赖是最常见的通常也是架构约束重点盯的对象。«import»导入表示一个包将另一个包中的元素导入自己的命名空间本包可以直接使用被导入包的公开元素且不需要加“包名限定”前缀。说人话就是我不仅用了你而且把你当成自己的命名空间一部分来用了。这比use的耦合程度更高。举个Java的例子import java.util.List;在包图上就是util包中的List类被导入到了当前包。«access»访问这是一种“私有导入”表示包A的私有元素需要访问包B的公开元素但A并不把B的元素暴露给自己的子包或依赖方。简单说就是“我内部悄悄借用了你但不希望我的下游知道这回事”。例如一个工具包依赖了另一个日志框架但只在自己的私有方法里用对外不暴露相关类型时适合用access来表达。«merge»合并这是最“重”的一种关系表示两个包的元素需要合并成一个包通常用在泛化体系和元模型扩展的场景普通业务系统建模很少用到。初学阶段可以理解为“包级别的继承扩展”知道有这回事即可。搞清楚这几种依赖的类型对画图的严谨性很重要。很多时候架构评审不过关并不是依赖方向错了而是依赖类型标错了。我曾经在一个核心领域包和工具包之间标了import实际上领域包只是“偶尔借用了一下工具类”这个import会让下游误以为领域包暴露了工具包的API结果没人敢随意引用了反而增加了沟通成本。2.3 可见性与导入规则包图的“封装边界”从哪来包也有可见性。类似类中成员的可见性包的可见性用加号表示公开、减号-表示私有、井号#表示受保护、波浪号~表示包内可见。虽然普通业务建模很少用到这么细但理解“包也有可见性”这一概念非常重要包不仅是分组容器还是一种封装机制。举个例子。如果domain包中的OrderRepository接口被标记为公开那么infrastructure包的实现类就可以对它实现如果某个只供内部调试用的工具类被标记为私有-那么任何其他包都不能通过import依赖它只能通过access在受限条件下使用。这种边界控制在大型系统里能有效防止“内部实现细节泄漏到外部”。在UML规范中还有一个“包导入”的语义细节值得注意当你import一个包时你导入的是该包中所有可见元素而当你use一个包时你只是使用了该包中的某些元素并不导入整个命名空间。这意味着import会造成更高程度的耦合设计时应尽量减少跨包的import只暴露真正需要共享的接口级别元素。3. 包依赖的方向才是架构设计的主战场3.1 依赖图里读出的架构健康度单向、无环、有层次包图画完之后真正值钱的是对“依赖方向”的分析。我总结了一套自己在评审时必看的“包图三大原则”这也是我在实际项目里反复验证过确实能救命的判断标准。第一原则单向依赖。两个包之间不应该存在双向依赖。如果包A依赖包B包B又依赖包A那么在代码层面几乎必然意味着你需要在编译、初始化或运行时处理循环引用轻则增加耦合重则导致启动失败或逻辑混乱。在包图上看到双向箭头我通常第一时间标记为“待重构”。第二原则无环依赖。把包当作节点、依赖当作有向边整个依赖图应该是一个有向无环图DAG。如果出现了环A→B→C→A那就是架构级的“坏味道”。这种环会让包之间的职责边界变得模糊也让后续的独立开发、独立测试、独立发布变得几乎不可能。举个例子订单包依赖了支付包支付包又依赖了账单包账单包再依赖订单包这时候你要单独测试订单模块就得把支付和账单都拉起来复杂度成倍增加。第三原则依赖稳定方向。这是我最看重的一条。依赖应该指向“更稳定”的方向——也就是说频繁变化的模块应该依赖不常变化的模块而不是反过来。在包图里通常体现为“业务规则层依赖领域模型层基础设施层依赖业务规则层但业务规则层不依赖任何外部实现细节”。这三条原则画成文字很干但用包图表达出来就一目了然。我在方案评审时经常做一件事把团队画的包图拷贝出来用箭头笔把所有依赖画出来然后逐条检查是否有双向箭头、是否有环、是否有“高层依赖低层具体实现”的情况。基本上每个项目都能查出几个问题。3.2 分层架构与包图几种经典的包依赖布局不同架构风格在包图上的依赖方向是有固定“套路”的这里分享三种最常见的。**分层架构Layered Architecture**是最普及的架构风格典型的包依赖方向是自上而下表示层依赖应用层应用层依赖领域层领域层依赖基础设施层。不过这里有一个关键设计点经常被忽略公开的接口比如仓储接口XXXRepository通常定义在领域层包里而实现类放在基础设施层包里。这样的话领域层本来应该不依赖基础设施层但基础设施层的实现类又必须“实现”领域层的接口——这个方向是基础设施层依赖领域层依赖方向自然就“倒转”过来了避免了低层被高层拖累的问题。**端口与适配器架构Ports and Adapters也称六边形架构**在包图上的呈现也很有辨识度核心业务逻辑在最中间外部通过端口Ports与内部交互适配器Adapters负责对接外部系统。包图上的依赖方向通常是“外部适配器依赖内部端口内部核心不依赖任何外部包”。这种画法特别适合微服务或需要对接多个第三方系统的项目因为它能直观地看出哪些包是“内核”、哪些包是“插件”。领域驱动设计DDD的分层包图是我日常用得最多的。按DDD的分层一般会划分为接口层接口层可以是Controller、API、应用层Application、领域层Domain和基础设施层Infrastructure。包图上的依赖规则是应用层依赖领域层基础设施层依赖领域层接口层只依赖应用层。领域层是整个系统的“心脏”不允许依赖任何其他层。把这条依赖规则用包图固定下来再配合第5章要讲的依赖检查工具一个微服务的架构边界就真正落地了。4. 实战演练从零设计一个“订单下单”核心链路的包图4.1 场景拆分与包划分的思路纸上谈兵这么久了我拿一个实际可上手的例子完整走一遍包图设计流程。假设我们现在要为电商系统的“订单下单”核心链路做系统建模。需求背景是用户从前端发起下单请求系统需要校验商品库存、计算价格含优惠、创建订单、扣减库存、发起支付请求。这个场景跨多个子域适合用包图来拆分模块边界。在设计包图之前我通常会先用一句业务描述来做职责分解再决定“包”怎么划分。这里把系统拆为四个顶层包webHTTP接口层负责协议接入、参数校验和响应封装。application应用服务层负责用例编排——它像“导演”把下单流程的各个步骤串起来但不关心具体怎么实现。domain领域层包含核心业务规则订单实体、商品库存规则、计算价格的领域服务等。infrastructure基础设施层负责与数据库、第三方支付系统、消息队列等外部资源交互。把顶层包确定之后还可以进一步细分。比如domain包下嵌套model实体与值对象、service领域服务、repository仓储接口三个子包。infrastructure包下嵌套persistence数据库实现、client第三方客户端、mq消息队列生产者与消费者等子包。这种嵌套层次不必太深两到三层足够再深就是自找麻烦了。4.2 依赖关系与接口方向设计接下来是最关键的一步确定每个包之间的依赖关系。我画包图时习惯先画包再画接口方向最后检查有没有违反依赖原则。下面是我们这只下单链路的包依赖设计web依赖application接口层调用应用服务的方法不直接触碰领域对象。因为应用层对外暴露的是用例级别的接口比如OrderApplicationService.submit(SubmitOrderCommand)web只能看到这个“门面”。application依赖domain应用服务在编排流程时使用领域的实体、领域服务和仓储接口。注意这里依赖的是接口不是基础设施的具体实现。infrastructure依赖domain基础设施层如持久化实现要实现domain包中的仓储接口因此产生依赖。它的依赖方向是“向上”的但这条向上依赖在架构上是健康的因为核心业务规则不依赖任何外部东西。domain不依赖任何其他包这是领域层的“铁律”。用UML的表示法来写web包和application包之间的依赖是«use»application对domain的依赖也是«use»。而infrastructure里某个OrderRepositoryImpl类对domain里的OrderRepository接口的关系是“实现”Realization在包级别通常也简化为依赖关系但在图上最好能标注出“实现接口”的语义避免后续阅读者产生误解。这里有个我在实际项目中踩过的坑一开始我把infrastructure包和domain包的依赖方向画反了也就是基础设施层被“依赖”了——原因是当时application直接通过Spring的Autowired注入了OrderRepositoryImpl的实现类。这种情况在包图上就是application指向infrastructure而domain完全不与基础设施交互。重构之后我在domain里定义了OrderRepository接口application只依赖这个接口infrastructure通过实现该接口被注入。简单说用依赖倒置原则把依赖方向拧了过来。这张包图画完之后再叠加我的“三大原则”检查所有依赖单向依赖图无环基础设施层依赖方向指向稳定但又不污染核心的领域层。架构边界立即清晰了。4.3 画包图的工具选型与实用模板包图到底用什么工具画这里我按使用场景分类推荐几个都是自己实测过的。StarUML老牌UML建模工具对包图支持非常完善支持创建嵌套包、定义依赖构造型适合个人设计建模。缺点是界面相对朴素部分高级功能需要授权。PlantUML用文本描述UML图只要写几行“package”和“package ... { ... }”就能生成包图。这套工具最适合把包图纳入文档仓库做版本管理因为文本可以diff、可以合并、可以CI校验。draw.iodiagrams.net免费好用图形元素齐全适合快速画草图和白板协作。很多团队用它做技术方案书里的包图导出SVG/PNG都非常方便。Enterprise Architect功能强大适合企业级建模中心支持从包图直接生成代码骨架。缺点是商业授权较贵不是所有团队都能接受。Logseq / Obsidian 的插件如果你习惯用笔记软件维护设计文档可以用Markdown加文本图如PlantUML块来内嵌包图实现“文档即代码”。下面给一个PlantUML的简单示例方便你快速上手startuml package web { [OrderController] } package application { [OrderApplicationService] } package domain { package model { [Order] [OrderItem] } package repository { interface OrderRepository } } package infrastructure { [OrderRepositoryImpl] [PaymentClient] } web -- application : use application -- domain : use infrastructure -- domain : use infrastructure .. domain : implement repository enduml注意PlantUML里对包的显示样式和依赖关系的精细语义支持有限如果想做严格的UML规范建模建议用StarUML等完整工具。如果是日常沟通、文档沉淀PlantUML性价比最高。5. 包图落地从设计图到代码库与自动化检查5.1 设计图如何“翻译”成代码库结构包图画好只是第一步真正值钱的是把它一步步变成团队日常开发的代码库结构。从这个角度讲包图是“架构设计”和“实际编码”之间的翻译器。还是以上面的电商下单系统为例。包图和代码包名的映射可以是这样的UML包图Java包名示例说明webcom.demo.order.web.controllerHTTP接口和控制层applicationcom.demo.order.application.service应用服务层用例编排domain.modelcom.demo.order.domain.model实体与值对象domain.repositorycom.demo.order.domain.repository仓储接口infrastructure.persistencecom.demo.order.infrastructure.persistence数据库实现MyBatis、JPAinfrastructure.clientcom.demo.order.infrastructure.client第三方客户端支付、物流做这种映射时有一个关键原则包的方向必须是代码依赖方向的显式表达。也就是说你在代码里不允许出现跨包反向依赖。实践路径是在代码评审的标准里加入“是否违反包依赖规则”一项评审时站在包图面前扫一眼凡是出现箭头反向的一律打回重构。如果你使用Java的模块化特性JPMS也就是module-info.java包图甚至可以直接变成模块图的骨架。我在一个中大型项目里就是包图驱动module-info的编写每个UML包对应一个Java模块requires语句必须与包图的依赖箭头一致这样编译器就在最底层帮我们守住了架构边界。5.2 用依赖工具在CI里守住包依赖红线在代码库里约束依赖方向光靠人工评审是不够的人总有疏忽的时候。我的做法是引入自动化依赖检查工具把包图的依赖规则写成代码测试在持续集成CI里跑。如果你是Java技术栈推荐ArchUnit。它可以用单元测试的方式断言“包A只能访问包B的哪些类”“包C不能依赖包D”。举个例子AnalyzeClasses(packages com.demo.order) public class ArchitectureRuleTest { Test void domainShouldNotDependOnInfrastructure() { JavaClasses classes new ClassFileImporter().importPackages(com.demo.order); ArchRule rule noClasses() .that().resideInAPackage(..domain..) .should().accessClassesThat() .resideInAPackage(..infrastructure..); rule.check(classes); } Test void webShouldOnlyAccessApplication() { JavaClasses classes new ClassFileImporter().importPackages(com.demo.order); ArchRule rule classes() .that().resideInAPackage(..web..) .should().onlyDependOnClassesThat() .resideInAnyPackage(..web.., ..application.., java.., org.springframework..); rule.check(classes); } }写完之后把这些测试挂到CI流水线里。只要有人写出了“domain依赖infrastructure”的代码测试直接红掉构建就失败。这是我用过最有效的架构纪律执行方式甚至比开会强调一百遍“不要逆向依赖”都有用。虽然ArchUnit是Java生态的工具类似的思路在C#里可以用NetArchTest在Python里可以用import-linter在TypeScript里可以用dependency-cruiser都能实现同样效果。对于已经存在的系统还推荐用JDepend或Structure101这类工具做一次全量扫描自动生成包的依赖矩阵和环状依赖报告。扫描完你会惊讶地发现系统里居然藏了那么多“隐藏的环”和“越级依赖”。5.3 包图怎么保持“常新”活文档化实践画任何设计图最大的敌人就是“画完就不更新”。包图尤其容易过期因为代码库每天都在演进架构师不可能每次改动都手动去维护一张图。这里我分享三个能让包图“活”起来的小技巧。第一采用“图模合一”的方案。尽可能用PlantUML或代码生成流程图等“文本化”的手段来维护包图把它放到代码仓库的docs/architecture目录下。代码评审时凡是涉及跨包调用的改动要求同时更新对应的PlantUML包图描述这样图就和代码同步演进。第二让包图由真实依赖自动生成。像Structure101、IntelliJ IDEA的“Dependencies”分析、Visual Studio的“Code Map”都能根据代码自动生成包的依赖结构。我们可以把“官方理想图”改为“人肉维护”把“当前真实图”由工具生成两者定期对比差距就是“架构债”。如果团队精力有限这是最省力也最客观的方式。第三把包图评审纳入日常开发流程。每次迭代结束用自动生成的依赖图和上次对比哪些包新增加了依赖哪些环新增了这些问题应该在迭代回顾会上过一遍而不是等到架构大review时才发现。包图不是交付物它是团队对系统结构的共同认知。6. 包图设计中的常见问题与避坑建议6.1 包粒度到底该多粗、多细一个非常实际的问题是包到底划多大才算合适这件事没有绝对标准但我有一套经验法则。包的作用是“控制认知复杂度”如果每个包里只有两个类包就失去了分组的价值如果每个包里塞了几十个类那它又退化成了一团浆糊。一般来说一个顶层包内的直接子元素包括单类与子包控制在5~15个之间比较合适这样一屏能看懂又不至于过度拆碎。另外要注意一个问题包的拆分依据是“内聚性”不是“技术类型”。比如一个包叫utils里面又放工具类、又放常量类、又放异常定义就很糟糕——工具类之间根本没有业务上的内聚关系。更好的做法是按业务能力或变更频率分组例如order这个包把订单相关的一切高内聚的东西都放一起。这样当需求变化时受影响的类大概率集中在同一个包里修改的爆炸半径就被控制住了。6.2 循环依赖的识别与重构循环依赖是包图里最让人头疼的问题。我的排查思路是先画出依赖图再寻找环找到环之后定位环上的“薄弱节点”也就是那个“其实并没有真正需要对方、只是方便了才引过来”的依赖把它抽掉。举个例子。假设order包依赖payment包的PaymentService而payment包又依赖order包的OrderQuery接口来查询订单状态。这两个包互相依赖代码层面很容易改但在架构上不可取。重构方案通常有三条路其一将OrderQuery接口下沉到domain包或其独立共享包让两个包都依赖第三方其二把一方依赖的代码从接口调用改成事件驱动比如order发布“订单已创建”的领域事件payment监听事件后自行查询这样就切断了编译期依赖其三通过依赖倒置让双向依赖变成“单向增强”。我会优先考虑事件驱动的方案因为它天然符合业务的异步性顺便也提升了系统的弹性。6.3 过度设计的代价包图也不是越精细越好最后想说一个反向提醒包图建模不要走向“过度设计”。我见过有人把包图细化到每个DTO、每个枚举都单独建一个包结果模型比代码还难维护包图反而成了团队的负担。架构建模的重点是“边界清晰、依赖有序”而不是“细节完整”。在实际开发中我养成了一个习惯包图只维护到“足以回答关键架构问题”的粒度。关键架构问题包括新功能改动会落在哪些包新增第三方SDK应该在哪个包里封装某个模块要复用时带走哪些包回答得了这些问题这张包图就是合格的。至于更细的类级关系交给类图或直接看代码就好不必塞进包图里。回到最初的话题——包图的身份是被低估的。它看起来只是几个方框但它的价值在于把“架构方向”这样感性的认知变成了可评审、可检查、可自动验证的工程产物。如果你所在的项目已经出现跨模块纠缠、依赖混乱、测试困难的征兆不妨从画一张当前系统的包依赖图开始你很可能在第一张图上就找到症结的入口。根据我的经验一次真正有效的包图评审往往比十次代码走查更能暴露系统性的设计问题。也建议你把ArchUnit这样的依赖守门人尽早放进CI让包图上的每一条箭头都不只是一张图上的线条而是代码库里真实存在的约束。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻