夜雨聆风学习资料网

ARTICLE · 1124357

第20讲:软件设计:组件与UML组件图

第20讲:软件设计:组件与UML组件图
UML 中的组件是一种具有明确封装边界,并能够通过接口与其他组件或者外部环境进行交互的软件结构单元。组件通常具有以下特征。
第一,具有明确的边界
组件内部包含自己的实现,外部不需要了解其所有内部细节。
第二,具有明确的接口
组件通过接口向外提供服务,也可以通过接口使用其他组件提供的服务。
第三,具有相对独立的实现
组件内部可以包含多个类、接口以及其他设计元素。
第四,具有一定的可替换性
如果两个组件遵循相同的接口契约,在满足其他约束的情况下,可以使用一个组件替换另一个组件。
因此,组件的核心思想可以概括为:将一组实现封装起来,并通过明确的接口契约与外部协作。
组件不是传统结构化方法中的“模块”,组件是在这种面向对象设计基础上形成的更高层次结构。
例如,订单组件内部可能包含:Order、OrderItem、OrderService、OrderRepository,这些对象和类通过协作共同实现订单能力,然后通过:IOrderService(订单服务接口)向外提供服务。
因此,组件关注的不只是:“这个部分负责什么功能?” 还关注:
  • “哪些实现被封装在一起?”
  • “外部通过什么接口访问它?”
  • “它依赖哪些接口?”
  • “它是否可以被其他实现替换?”
这才是面向对象语境下理解组件的关键。
1. 组件与类、接口、包和服务
组件并不是孤立存在的。理解组件图的关键,是理解组件与面向对象开发中其他重要概念之间的关系。
1.1 组件与类
一个组件内部通常可以包含多个类。
例如,订单组件内部可能包含:Order、OrderItem、OrderService、OrderRepository。其中,Order 、OrderItem 是领域类,OrderService 是服务实现类,OrderRepository 是持久化相关类,它们共同构成订单组件的一部分实现。
因此,组件的抽象层次高于类。类图可以进一步展开组件内部的设计,而组件图则可以隐藏这些内部细节。
1.2 组件与接口
组件需要通过接口与外部环境进行协作。
例如:订单组件包含:OrderService、 IOrderService (订单服务接口),其中,IOrderService 可以规定实现:createOrder()、getOrder()、cancelOrder()这些操作。外部组件只需要依赖这个接口,而不需要知道OrderService 的内部实现。
因此,接口是组件对外暴露的契约,组件是接口背后的封装实现。
1.3 组件与包
组件与包也容易混淆。包(Package)主要用于组织和管理模型元素。
包可以包含:类、接口、组件、其他包、用例等模型元素。包主要解决的是:如何组织模型元素?而组件主要解决的是:哪些实现被封装在一起,以及它们如何通过接口与外部协作?
例如:
表示这些元素Order、OrderItem、OrderService、OrderRepository被组织在order 包中。
而:
强调的是一个具有封装边界和接口契约的组件。因此,包主要强调组织和分类,组件主要强调封装、接口和协作。
1.4 组件与服务
组件和服务也不能简单画等号。组件强调:谁封装了实现?服务强调:向外提供什么能力?例如:
订单组件可能向外提供:创建订单、查询订单、取消订单、修改订单状态。这些能力可以被称为订单服务。
因此,组件提供服务,而服务通常通过接口进行表达组件实现接口(比如,IOrderService),接口定义了订单服务能力。
这里还需要注意:接口和服务也不是完全相同的概念。接口是一种软件设计契约,规定外部如何与组件协作;服务则更多地从使用者角度描述组件能够提供的能力。
2. 组件图的基本概念与表示方法
UML 中通常使用带有组件标识的矩形表示组件。例如:
也可以用类的构造型《component》表示。
基本元素与 PlantUML 表示如下表:

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》;
  • 端口:HTTP、JDBC;
  • 依赖:组件间及组件与数据库之间的使用《use》关系。
3. 从面向对象设计到组件图
组件图不是凭空设计出来的。在面向对象开发中,可以从已经完成的类设计和接口设计出发,进一步识别组件。
 从类图识别组件
例如,已经得到以下类:Order、OrderItem、OrderService、OrderRepository,进一步分析:
  • 这些类之间存在较强的协作关系;
  • 它们共同完成订单相关的软件能力;
  • 外部主要使用OrderService通过接口IOrderService 使用这些内部类;
  • 内部持久化实现不应该暴露给外部。
于是可以将它们封装为“订单组件”:
这就是从类设计走向组件设计。
确定组件边界
确定组件边界时,需要考虑:
  • 类之间的协作关系;
  • 职责的相关性;
  • 接口的稳定性;
  • 数据和实现的封装;
  • 外部依赖;
  • 可替换性。
不能简单按照功能名称机械划分。例如:订单、支付、库存这三个类并不意味着一定应该形成三个组件。需要进一步分析:订单相关的哪些类共同形成一个稳定的封装边界?支付能力通过什么接口提供?库存能力通过什么接口提供?
只有完成这些分析之后,组件划分才具有设计意义。
高内聚
组件内部的设计元素应该具有较强的相关性。例如:
订单组件
├── Order
├── OrderItem
├── OrderService
└── OrderRepository
订单组件中的这些元素共同围绕订单相关职责进行协作,具有较高的内聚性。如果一个组件同时包含:Order、Payment、ImageProcessor、EmailSender、MusicPlayer,很明显ImageProcessor、EmailSender、MusicPlayer与Order相关性不强,不能放在一个组件里。
因此,组件内部应尽可能形成紧密的协作关系。
 低耦合
组件之间则应该尽量减少不必要的依赖。
例如:可以通过接口进行依赖,订单组件→ IPaymentService、订单组件→ IInventoryService。这是有明确业务意义的依赖。而如果形成:订单组件→ 支付组件、支付组件→ 库存组件、库存组件→ 商品组件、商品组件→ 订单组件。就需要进一步分析是否存在不必要的循环依赖。
面向接口设计
组件之间最好依赖接口,而不是直接接口的具体实现,也就是说,两个组件之间不能直接依赖。例如:订单组件通过支付接口依赖支付组件,
而不能订单组件直接依赖支付组件:
因为当增加WeChatPaymentService、BankPaymentService提供支付,如果通过接口依赖,只要它们实现相同接口,订单组件就不需要知道支付组件的具体实现。这种通过接口依赖的方式明显提高了维护性和灵活性。
4. 组件、服务与微服务
现代软件开发中经常同时出现“组件”“服务”和“微服务”三个概念。它们之间存在联系,但不能简单地等同。
组件与服务
可以用下面的方式理解:组件实现封装,它通过接口提供服务, 例如:订单组件提供订单服务。
 组件与微服务
微服务(Microservice)是一种软件架构风格。在微服务架构中,一个服务通常具有:相对独立的业务职责、独立的部署能力、明确的服务接口、相对独立的运行过程。这些服务都可以在 UML 组件图中作为组件进行建模。但是,组件不等于微服务。
一个组件可以是:一个独立服务、一个应用内部的软件结构、一个软件库、一个第三方软件包、一个可替换的实现单元。
因此,微服务是具体的架构风格,而组件是更加一般的软件结构概念。实际上,组件可以是微服务的实现技术。
5. 组件图、包图及其他UML图的关系
5.1 组件图与包图
这是两个非常容易混淆的 UML 图。包图关注组织,它主要描述:模型元素如何组织和依赖。例如:shop包含user、product、order、payment、inventory。
shop
├── user
├── product
├── order
├── payment
└── inventory
包之间可以存在依赖:order ─────> payment、order ─────> inventory。
组件图关注封装和协作,组件图描述:软件实现被封装成哪些组件,以及组件如何通过接口协作。包图与组件图比较如下表:

比较内容

包图

组件图

核心目的

组织模型元素

描述软件组件及其协作

主要对象

包

组件

关注重点

分类、命名空间、依赖

封装、接口、契约、依赖

常见内容

类、接口、子包

组件、接口、端口

抽象重点

模型组织

软件结构

是否强调接口契约

不一定

非常重要

5.2 组件图与类图
类图描述:系统中有哪些类,以及类之间有什么关系。组件图描述:哪些实现被封装成组件,以及组件如何通过接口协作。类图可以帮助我们理解组件内部,组件图则隐藏组件内部的类结构。
5.3 组件图与顺序图
组件图主要描述结构,顺序图主要描述行为。因此,组件图描述“谁与谁协作”,顺序图描述“协作如何随时间发生”。
5.4 组件图与部署图
组件图回答:软件由哪些组件组成?部署图回答:这些软件组件运行在哪里?组件图描述软件结构,部署图描述软件结构与运行环境之间的映射。
6. 综合案例:网上购物系统
下面综合使用前面介绍的概念。
6.1 类设计
网上购物系统可以设计如下类:User、Product、ShoppingCart、CartItem、Order、OrderItem、Payment。此外,还可以设计一些服务和持久化相关的类:UserService、ProductService、OrderService、OrderRepository、PaymentService、InventoryService。
6.2 组件识别
进一步分析类之间的协作和封装边界,可以形成:用户组件、商品组件、购物车组件、订单组件、支付组件、库存组件。
例如,订单组件可以封装类:Order、OrderItem、OrderService、OrderRepository,并通过:IOrderService向外提供订单服务。
6.3 服务与接口设计
下表列出了所有接口定义:

接口

服务

IUserService(用户服务接口)

用户认证与用户管理服务

IProductService(商品服务接口)

商品查询服务

IOrderService(订单服务接口)

订单管理服务

IPaymentService(支付服务接口)

支付服务

IInventoryService(库存服务接口)

库存服务

6.4 组件之间的协作
购物车组件需要IUserService、IProductService,订单组件需要IPaymentService、IInventoryService,购物车组件还需要IOrderService。因此,可以形成组件之间的依赖关系。
6.5 网上购物系统组件图
为了进一步理解组件图和顺序图的区别,考虑网上购物系统中的“下订单”场景。用户提交订单之后,系统可能执行:
1. 用户提交订单
2. 订单组件检查订单信息
3. 订单组件请求库存组件检查库存
4. 库存充足
5. 订单组件创建订单
6. 订单组件请求支付组件
7. 支付成功
8. 库存组件扣减库存
9. 订单状态更新为已支付
对应的顺序图如下所示:
可以根据顺序图检查组件之间的依赖关系。
7. 组件图的设计原则与常见问题
7.1 组件职责应该清晰
组件内部的类应该具有较强的相关性,比把各种无关职责放在一起更加合理。
7.2 避免不必要的组件依赖
组件之间的依赖越多,系统的结构通常越复杂。尤其需要关注循环依赖,这种结构可能增加维护和演化的困难。
7.3 优先依赖接口
不推荐直接依赖组件,这样能够降低组件之间的实现耦合。
7.4 不要把每个类都画成组件
如果直接把每个类都画成组件,就失去了组件图比类图更高层次的意义。
7.5 不要把包直接等同于组件
包用于组织模型元素,而组件用于封装实现并通过接口进行协作。二者可以存在映射关系,但没有固定的一一对应关系。
7.6 不要把组件直接等同于微服务
微服务可以作为组件进行建模,但组件本身并不等于微服务。
小  结
UML 组件图用于描述软件系统中组件的结构、接口和依赖关系。
从面向对象开发方法的角度来看,组件并不是传统结构化方法中的“功能模块”,而是对一组协作的类、接口和实现进行进一步封装后形成的软件结构单元。组件具有明确的边界,并通过接口与外部环境进行协作。
在面向对象的软件开发过程中,可以从类和接口设计出发识别组件,再通过组件图描述更高层次的软件体系结构。
最后,通过网上购物系统可以看到:并不是简单的“图越画越大”,而是分别从对象设计、模型组织、软件封装、动态协作和运行部署等不同视角描述同一个软件系统。

相关学习资料