一、类图设计方法论:构建稳健的面向对象模型

类图建模的本质是将现实世界的业务概念转化为计算机可理解的面向对象结构。遵循科学的方法论是确保模型质量的基础,核心包含四大环节:元素识别、关系构建、属性定义与模型优化。

1.1 元素识别:精准定位核心建模单元

元素识别是类图设计的起点,需从业务需求中提取关键概念并转化为 UML 元素。识别过程需遵循 "单一职责原则",确保每个元素职责清晰、边界明确。

核心元素类型及识别方法

元素类型 识别特征 表示符号 应用场景
类 (Class) 具有相同属性和行为的对象集合 矩形(分三层:类名 / 属性 / 方法) 业务实体(如 User、Order)、控制逻辑(如 OrderService)、工具组件(如 DateUtils)
接口 (Interface) 定义行为契约,无具体实现 棒棒糖形状或矩形(标注 <>) 服务契约(如 PaymentGateway)、模块边界(如 UserRepository)
抽象类 (Abstract Class) 不能实例化,包含抽象方法 类名斜体或标注 <> 公共基类(如 AbstractPaymentMethod)、模板方法模式中的模板类
枚举 (Enumeration) 固定取值集合的类型 矩形(标注 <>) 状态定义(如 OrderStatus)、类型分类(如 PaymentType)

识别实战示例:电商订单系统

从 "用户下单" 需求中识别核心元素:

  1. 业务概念提取:用户、订单、商品、支付记录、库存
  2. 元素类型判定
  • User(类):具有属性(id/name/phone)和行为(login/pay)

  • Order(类):包含订单状态、金额等属性,及创建订单、取消订单等行为

  • Product(类):商品基本信息及库存查询行为

  • PaymentRecord(类):支付相关记录,关联订单和支付方式

  • OrderStatus(枚举):包含 PENDING、PAID、SHIPPED、DELIVERED 等固定状态

  • PaymentGateway(接口):定义支付接口,由不同支付方式实现

1.2 关系构建:清晰表达元素间关联

类图中的关系是模型的 "骨架",需严格遵循 UML2.5 规范,准确区分不同关系类型的语义差异,避免混淆使用。

五大核心关系类型及应用场景

关系类型 语义定义 表示符号 区分要点 实战案例
泛化 (Generalization) 继承关系(is-a) 带空心三角的实线(子类→父类) 子类继承父类的属性和方法,可重写父类方法 User 类泛化为 Customer 和 Admin 类
实现 (Realization) 类实现接口契约 带空心三角的虚线(类→接口) 实现类必须提供接口中所有方法的具体实现 AlipayGateway 类实现 PaymentGateway 接口
关联 (Association) 元素间结构化连接(has-a) 实线(可标注关联名和多重度) 双向或单向的对象引用关系,不强调整体 - 部分 User 类与 Order 类关联(一个用户有多个订单)
聚合 (Aggregation) 松散的整体 - 部分关系(part-of) 带空心菱形的实线(整体→部分) 部分可独立于整体存在,整体销毁不影响部分 Order 类(整体)与 Product 类(部分)的聚合关系(订单包含商品,商品可独立存在)
组合 (Composition) 紧密的整体 - 部分关系(part-of) 带实心菱形的实线(整体→部分) 部分生命周期依赖整体,整体销毁则部分也销毁 Order 类(整体)与 OrderItem 类(部分)的组合关系(订单条目不能脱离订单存在)
依赖 (Dependency) 元素间临时的、弱关联(use-a) 带箭头的虚线(依赖方→被依赖方) 一方使用另一方的服务或资源,通常是局部变量、方法参数或静态调用 OrderService 类依赖 Logger 类(记录日志)

关系构建常见错误及规避方法

错误 1:将关联与依赖混淆

  • 错误表现:用依赖表示长期的对象引用关系

  • 规避方法:判断是否存在属性级别的引用 —— 存在则用关联,仅方法内部使用则用依赖

错误 2:聚合与组合误用

  • 错误表现:用聚合表示生命周期强依赖的关系

  • 规避方法:执行 "整体销毁测试"—— 整体销毁后部分是否仍有意义,有则为聚合,无则为组合

错误 3:多重度定义不准确

  • 错误表现:所有关联均标注 "1-N",未根据业务规则精确定义

  • 规避方法:根据业务规则明确多重度(如 User 与 Order 的关联:1(User)→*(Order),表示一个用户可创建多个订单)

1.3 属性定义:规范描述元素特征

属性是类的 "血液",需遵循统一的命名规范和可见性规则,确保模型的可读性和一致性。

属性定义规范

命名约定:采用驼峰命名法(camelCase),首字母小写,如userName、orderId

可见性标识**:严格遵循 UML 可见性规则

  • +:公有(public)—— 外部可访问

    -:私有(private)—— 仅类内部可访问

  • #:保护(protected)—— 类及其子类可访问

  • ~:包可见(package)—— 同一包内可访问

完整格式:可见性 名称: 类型 [= 默认值] {约束条件}

  • 示例 1:- userId: String(私有属性,字符串类型)

  • 示例 2:+ orderStatus: OrderStatus = PENDING(公有属性,枚举类型,默认值为 PENDING)

  • 示例 3:# totalAmount: Double {readonly}(保护属性,浮点类型,只读约束)

方法定义规范

命名约定:动词开头的驼峰命名法,如createOrder()、calculateTotal()

完整格式:可见性 名称(参数列表): 返回类型 {约束条件}

  • 示例 1:+ addProduct(product: Product): void(公有方法,接收 Product 参数,无返回值)
  • 示例 2:- calculateDiscount(): Double(私有方法,无参数,返回 Double 类型折扣值)
  • 示例 3:# validateOrder(): Boolean {abstract}(保护抽象方法,无参数,返回布尔值)

二、代码结构分析

首先,我们对给定的 C++ 代码进行结构梳理,明确其中的类、继承关系、成员变量和成员函数,这是绘制类图的基础。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#include <math.h>
#include <iostream>
#include <string>
using std::cout;
using std::endl;
using std::string;

class Figure{
public:
virtual string getName() const = 0;
virtual double getArea() const = 0;
};

class Rectangle//矩形
: public Figure
{
public:
Rectangle(double len,double wid)
: _length(len)
, _width(wid)
{}

string getName() const override
{
return "矩形";
}
double getArea() const override
{
return _length * _width;
}
private:
double _length;
double _width;
};

class Circle
: public Figure
{
public:
Circle(double r)
: _radius(r)
{}

string getName() const override
{
return "圆形";
}
double getArea() const override
{
return PI * _radius * _radius;
}
private:
double _radius;
static constexpr double PI = 3.14;
};

class Triangle
: public Figure
{
public:
Triangle(double a,double b,double c)
: _a(a)
, _b(b)
, _c(c)
{}

string getName() const override
{
return "三角形";
}
double getArea() const override
{
double p = (_a + _b + _c)/2;
return sqrt(p * (p -_a) * (p - _b)* (p - _c));
}
private:
double _a,_b,_c;
};

void display(Figure & fig) {
cout << fig.getName()
<< "的面积是:"
<< fig.getArea() << endl ;
}

void test0()
{
Rectangle rec(10,20);
Circle cl(3);
Triangle tri(3,4,5);
display(rec);
display(cl);
display(tri);
}

int main(void)
{
test0();
return 0;
}

2.1 类的层级关系

代码中存在一个核心的抽象基类Figure,以及三个继承自它的具体子类,分别是Rectangle(矩形)、Circle(圆形)和Triangle(三角形),形成了 “抽象基类 - 具体子类” 的继承结构。

2.2 类的核心成员

我们通过表格清晰展示每个类的成员变量和成员函数:

类名 成员变量 成员函数 特殊说明
Figure(抽象基类) 纯虚函数:getName() const(获取图形名称)、getArea() const(获取图形面积) 无法实例化,仅用于定义接口
Rectangle _length(double,长度)、_width(double,宽度) 构造函数:Rectangle(double len, double wid);重写函数:getName() const、getArea() const 计算面积公式:长度 × 宽度
Circle _radius(double,半径)、PI(static constexpr double,圆周率,值为 3.14) 构造函数:Circle(double r);重写函数:getName() const、getArea() const 计算面积公式:π× 半径 ²
Triangle _a(double,边长 1)、_b(double,边长 2)、_c(double,边长 3) 构造函数:Triangle(double a, double b, double c);重写函数:getName() const、getArea() const 用海伦公式计算面积:√[p (p-a)(p-b)(p-c)],其中 p=(a+b+c)/2
全局函数 display(Figure & fig)(打印图形名称和面积)、test0()(测试函数,创建图形对象并调用display)、main()(程序入口,调用test0()) 用于业务逻辑实现,非类成员

三、StarUML 绘制类图步骤

StarUML 是一款专业的 UML 建模工具,支持类图、时序图等多种 UML 图表绘制。以下是基于上述代码绘制类图的详细步骤:

3.1 新建类图项目

  1. 打开 StarUML,点击菜单栏「File」→「New Project」,创建一个新的项目(如命名为 “图形面积计算”)。
  2. 在项目目录下,右键点击「Model」→「Add Diagram」→「Class Diagram」,新建一个类图(如命名为 “FigureClassDiagram”)。

3.2 创建抽象基类Figure

  1. 在 StarUML 左侧「Toolbox」(工具箱)中,选择「Class」工具,在类图画布上点击,创建一个类,将类名修改为Figure。
  2. 设置类为抽象类:右键点击Figure类→「Properties」(属性),在「Stereotype」(构造型)中选择「abstract」,此时类名将显示为斜体(符合 UML 抽象类的表示规范)。
  3. 添加纯虚函数:
    • 右键点击Figure类→「Add」→「Operation」(操作),添加第一个操作,命名为getName(),返回值类型设为string,访问修饰符设为public。
    • 由于getName()是纯虚函数,需要在操作名后添加=0:双击操作名,将其修改为getName(): string {abstract}(StarUML 中用{abstract}标识纯虚函数)。
    • 重复上述步骤,添加第二个纯虚函数getArea(),返回值类型设为double,同样标记为{abstract}。

3.3 创建子类并建立继承关系

以Rectangle类为例,其他子类(Circle、Triangle)操作类似:

  1. 用「Class」工具在画布上创建Rectangle类,无需设置为抽象类(具体子类可实例化)。

  2. 建立继承关系:在「Toolbox」中选择「Generalization」(泛化,即继承)工具,先点击Rectangle类,再点击Figure类,此时会生成一条从Rectangle指向Figure的箭头,表示Rectangle继承自Figure。

  3. 添加成员变量:

    • 右键点击Rectangle类→「Add」→「Attribute」(属性),添加第一个属性,命名为_length,类型设为double,访问修饰符设为private(代码中成员变量为私有)。_
    • _重复步骤添加_width属性,类型和访问修饰符与_length一致。
  4. 添加构造函数和重写函数:

    • 添加构造函数:右键点击Rectangle类→「Add」→「Operation」,命名为Rectangle,参数设为len: double, wid: double,返回值类型设为void(构造函数无返回值),访问修饰符设为public。
    • 添加重写函数getName():操作类型设为string,访问修饰符public,在「Properties」中勾选「Override」(表示重写父类方法)。
    • 添加重写函数getArea():操作类型设为double,访问修饰符public,同样勾选「Override」。

3.4 处理Circle类的静态常量

Circle类中存在静态常量PI,绘制时需特殊设置:

  1. 按照上述步骤创建Circle类,建立与Figure的继承关系,添加_radius私有成员变量。

  2. 添加静态常量PI:右键点击Circle类→「Add」→「Attribute」,命名为PI,类型设为double,访问修饰符设为private。

  3. 设置静态和常量属性:在PI属性的「Properties」中,勾选「Static」(静态)和「Final」(常量,UML 中用Final表示常量),并在属性值中填写3.14。

3.5 添加全局函数

代码中的display、test0和main是全局函数,在 UML 类图中可通过「Class」工具创建一个 “全局函数类”(如命名为GlobalFunctions)来管理:

  1. 创建GlobalFunctions类(无需继承任何类)。

  2. 添加全局函数:右键点击GlobalFunctions类→「Add」→「Operation」,分别添加display(fig: Figure)(参数类型为Figure引用,返回值void)、test0()(无参数,返回值void)、main()(无参数,返回值int),访问修饰符均设为public,并勾选「Static」(全局函数可视为静态函数)。

3.6 调整类图布局

为了使类图清晰易读,可拖动类的位置,调整箭头方向,确保继承关系和类成员不重叠。最终布局建议:将Figure类放在顶部,三个子类在下方围绕Figure,GlobalFunctions类放在右侧或下方单独区域。