ARTICLE · 1124357
第20讲:软件设计:组件与UML组件图
发布时间:2026-10-04 22:10:29 最近访问:2026-10-04 22:10:29
第20讲:软件设计:组件与UML组件图
UML 中的组件是一种具有明确封装边界,并能够通过接口与其他组件或者外部环境进行交互的软件结构单元。组件通常具有以下特征。组件内部包含自己的实现,外部不需要了解其所有内部细节。组件通过接口向外提供服务,也可以通过接口使用其他组件提供的服务。如果两个组件遵循相同的接口契约,在满足其他约束的情况下,可以使用一个组件替换另一个组件。因此,组件的核心思想可以概括为:将一组实现封装起来,并通过明确的接口契约与外部协作。组件不是传统结构化方法中的“模块”,组件是在这种面向对象设计基础上形成的更高层次结构。例如,订单组件内部可能包含:Order、OrderItem、OrderService、OrderRepository,这些对象和类通过协作共同实现订单能力,然后通过:IOrderService(订单服务接口)向外提供服务。因此,组件关注的不只是:“这个部分负责什么功能?” 还关注:组件并不是孤立存在的。理解组件图的关键,是理解组件与面向对象开发中其他重要概念之间的关系。例如,订单组件内部可能包含:Order、OrderItem、OrderService、OrderRepository。其中,Order 、OrderItem 是领域类,OrderService 是服务实现类,OrderRepository 是持久化相关类,它们共同构成订单组件的一部分实现。因此,组件的抽象层次高于类。类图可以进一步展开组件内部的设计,而组件图则可以隐藏这些内部细节。例如:订单组件包含:OrderService、 IOrderService (订单服务接口),其中,IOrderService 可以规定实现:createOrder()、getOrder()、cancelOrder()这些操作。外部组件只需要依赖这个接口,而不需要知道OrderService 的内部实现。因此,接口是组件对外暴露的契约,组件是接口背后的封装实现。组件与包也容易混淆。包(Package)主要用于组织和管理模型元素。包可以包含:类、接口、组件、其他包、用例等模型元素。包主要解决的是:如何组织模型元素?而组件主要解决的是:哪些实现被封装在一起,以及它们如何通过接口与外部协作?表示这些元素Order、OrderItem、OrderService、OrderRepository被组织在order 包中。强调的是一个具有封装边界和接口契约的组件。因此,包主要强调组织和分类,组件主要强调封装、接口和协作。组件和服务也不能简单画等号。组件强调:谁封装了实现?服务强调:向外提供什么能力?例如:订单组件可能向外提供:创建订单、查询订单、取消订单、修改订单状态。这些能力可以被称为订单服务。因此,组件提供服务,而服务通常通过接口进行表达组件实现接口(比如,IOrderService),接口定义了订单服务能力。这里还需要注意:接口和服务也不是完全相同的概念。接口是一种软件设计契约,规定外部如何与组件协作;服务则更多地从使用者角度描述组件能够提供的能力。UML 中通常使用带有组件标识的矩形表示组件。例如:UML 元素 | 说明 | PlantUML 语法 |
组件 | 可替换、可复用的模块单元 | component "名称" as 别名 |
接口 | 组件对外提供或需要的契约 | () "名称" as 别名 |
提供接口 | 棒棒糖表示,组件实现该接口 | 组件 - 接口 |
需求接口 | 插座表示,组件使用该接口 | 组件 ..> 接口 : <<use>> |
端口 | 组件与外部交互的访问点 | port "名称" as 别名 |
依赖 | 组件间的使用关系 | A ..> B : <<use>> |
包/分组 | 组织组件与接口 | package "名称" { ... } |
PlantUML 示例
@startuml
skinparam componentStyle uml2
database "订单数据库" as OrderDB
package "订单系统" {
component "订单组件" as OrderComp
component "支付组件" as PayComp
' 接口:用圆圈表示
() "IOrderService" as IOrderService
() "IPaymentService" as IPaymentService
' 端口
port "HTTP" as httpPort
port "JDBC" as jdbcPort
}
' 提供接口(棒棒糖)
OrderComp - IOrderService
PayComp - IPaymentService
' 需求接口(插座)
OrderComp ..> IPaymentService : <<use>>
' 端口连接
OrderComp - httpPort
OrderComp - jdbcPort
jdbcPort - OrderDB
' 组件依赖
PayComp ..> OrderDB : <<use>>
@enduml
- 接口:IOrderService、IPaymentService;
- 需求接口:虚线箭头指向圆圈,有时带有标签《use》;
- 依赖:组件间及组件与数据库之间的使用《use》关系。
组件图不是凭空设计出来的。在面向对象开发中,可以从已经完成的类设计和接口设计出发,进一步识别组件。例如,已经得到以下类:Order、OrderItem、OrderService、OrderRepository,进一步分析:- 外部主要使用OrderService通过接口IOrderService 使用这些内部类;
不能简单按照功能名称机械划分。例如:订单、支付、库存这三个类并不意味着一定应该形成三个组件。需要进一步分析:订单相关的哪些类共同形成一个稳定的封装边界?支付能力通过什么接口提供?库存能力通过什么接口提供?订单组件中的这些元素共同围绕订单相关职责进行协作,具有较高的内聚性。如果一个组件同时包含:Order、Payment、ImageProcessor、EmailSender、MusicPlayer,很明显ImageProcessor、EmailSender、MusicPlayer与Order相关性不强,不能放在一个组件里。例如:可以通过接口进行依赖,订单组件→ IPaymentService、订单组件→ IInventoryService。这是有明确业务意义的依赖。而如果形成:订单组件→ 支付组件、支付组件→ 库存组件、库存组件→ 商品组件、商品组件→ 订单组件。就需要进一步分析是否存在不必要的循环依赖。组件之间最好依赖接口,而不是直接接口的具体实现,也就是说,两个组件之间不能直接依赖。例如:订单组件通过支付接口依赖支付组件,因为当增加WeChatPaymentService、BankPaymentService提供支付,如果通过接口依赖,只要它们实现相同接口,订单组件就不需要知道支付组件的具体实现。这种通过接口依赖的方式明显提高了维护性和灵活性。现代软件开发中经常同时出现“组件”“服务”和“微服务”三个概念。它们之间存在联系,但不能简单地等同。可以用下面的方式理解:组件实现封装,它通过接口提供服务, 例如:订单组件提供订单服务。微服务(Microservice)是一种软件架构风格。在微服务架构中,一个服务通常具有:相对独立的业务职责、独立的部署能力、明确的服务接口、相对独立的运行过程。这些服务都可以在 UML 组件图中作为组件进行建模。但是,组件不等于微服务。一个组件可以是:一个独立服务、一个应用内部的软件结构、一个软件库、一个第三方软件包、一个可替换的实现单元。因此,微服务是具体的架构风格,而组件是更加一般的软件结构概念。实际上,组件可以是微服务的实现技术。这是两个非常容易混淆的 UML 图。包图关注组织,它主要描述:模型元素如何组织和依赖。例如:shop包含user、product、order、payment、inventory。包之间可以存在依赖:order ─────> payment、order ─────> inventory。组件图关注封装和协作,组件图描述:软件实现被封装成哪些组件,以及组件如何通过接口协作。包图与组件图比较如下表:比较内容 | 包图 | 组件图 |
核心目的 | 组织模型元素 | 描述软件组件及其协作 |
主要对象 | 包 | 组件 |
关注重点 | 分类、命名空间、依赖 | 封装、接口、契约、依赖 |
常见内容 | 类、接口、子包 | 组件、接口、端口 |
抽象重点 | 模型组织 | 软件结构 |
是否强调接口契约 | 不一定 | 非常重要 |
类图描述:系统中有哪些类,以及类之间有什么关系。组件图描述:哪些实现被封装成组件,以及组件如何通过接口协作。类图可以帮助我们理解组件内部,组件图则隐藏组件内部的类结构。组件图主要描述结构,顺序图主要描述行为。因此,组件图描述“谁与谁协作”,顺序图描述“协作如何随时间发生”。组件图回答:软件由哪些组件组成?部署图回答:这些软件组件运行在哪里?组件图描述软件结构,部署图描述软件结构与运行环境之间的映射。网上购物系统可以设计如下类:User、Product、ShoppingCart、CartItem、Order、OrderItem、Payment。此外,还可以设计一些服务和持久化相关的类:UserService、ProductService、OrderService、OrderRepository、PaymentService、InventoryService。进一步分析类之间的协作和封装边界,可以形成:用户组件、商品组件、购物车组件、订单组件、支付组件、库存组件。例如,订单组件可以封装类:Order、OrderItem、OrderService、OrderRepository,并通过:IOrderService向外提供订单服务。接口 | 服务 |
IUserService(用户服务接口) | 用户认证与用户管理服务 |
IProductService(商品服务接口) | 商品查询服务 |
IOrderService(订单服务接口) | 订单管理服务 |
IPaymentService(支付服务接口) | 支付服务 |
IInventoryService(库存服务接口) | 库存服务 |
购物车组件需要IUserService、IProductService,订单组件需要IPaymentService、IInventoryService,购物车组件还需要IOrderService。因此,可以形成组件之间的依赖关系。为了进一步理解组件图和顺序图的区别,考虑网上购物系统中的“下订单”场景。用户提交订单之后,系统可能执行:组件内部的类应该具有较强的相关性,比把各种无关职责放在一起更加合理。组件之间的依赖越多,系统的结构通常越复杂。尤其需要关注循环依赖,这种结构可能增加维护和演化的困难。不推荐直接依赖组件,这样能够降低组件之间的实现耦合。如果直接把每个类都画成组件,就失去了组件图比类图更高层次的意义。包用于组织模型元素,而组件用于封装实现并通过接口进行协作。二者可以存在映射关系,但没有固定的一一对应关系。微服务可以作为组件进行建模,但组件本身并不等于微服务。UML 组件图用于描述软件系统中组件的结构、接口和依赖关系。从面向对象开发方法的角度来看,组件并不是传统结构化方法中的“功能模块”,而是对一组协作的类、接口和实现进行进一步封装后形成的软件结构单元。组件具有明确的边界,并通过接口与外部环境进行协作。在面向对象的软件开发过程中,可以从类和接口设计出发识别组件,再通过组件图描述更高层次的软件体系结构。最后,通过网上购物系统可以看到:并不是简单的“图越画越大”,而是分别从对象设计、模型组织、软件封装、动态协作和运行部署等不同视角描述同一个软件系统。