ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从UML图到C++代码:社区健康管理系统建模实践详解

从UML图到C++代码:社区健康管理系统建模实践详解 简介这是一份基于统一建模语言的软件课程设计资源面向计算机、软件工程相关专业学生用于完成社区健康管理系统的建模与实现。资源围绕居民健康档案、预约挂号、健康咨询、健康数据分析等典型需求展示如何用用例图、类图、状态图、活动图、序列图、部署图等将业务场景转化为系统设计。压缩包共有16个文件约418KB包含6个cpp源文件与6个头文件对应登录、用户信息、健康状态、健康建议、医务管理等模块另有2个Word设计说明文档、1个txt笔记文件及1个.mdl模型文件方便对照阅读和二次修改。当前已有100人学习下载从内容预览看覆盖登录视图、用户信息视图、健康状态与建议等模块既可用于课程设计答辩也可作为学习统一建模语言和C实现的综合示例。通过该资源读者能快速掌握UML建模与分析思路并参考其代码和文档组织方式提升系统设计能力。1. 为什么一个社区健康管理系统要从UML图开始AI 时代还需要学 UML 吗这是后台经常被问到的问题。很多人觉得现在让 AI 写代码很快画出几张 UML 图反而是浪费时间。但真正动手做社区健康管理系统这类课设时你会发现源码里有Loginview、HealthCondition、HealthAdvice、OperateUser这些模块彼此依赖关系复杂如果没有模型图兜底改一个类就可能牵扯出三四个文件答辩时老师一句“你的系统边界在哪、类之间是什么关系”就能问住你。UML 的作用不是交作业而是把“谁在用、流程怎么走、状态怎么流转、消息怎么传递”提前固定下来。这篇博文以UML课设社区健康管理系统.zip为素材从用例图、类图、状态图、活动图、序列图、部署图到 C 代码骨架生成完整拆一遍建模过程适合正在准备软件工程课设、复习 UML 考试以及想用建模思维指导实际开发的工程师。2. 用例图与类图先锁定参与者再映射 C 类2.1 用例图怎么画才不是“画着玩”用例图是最容易被画敷衍的图。很多课设里用例图就是把所有按钮列出来比如“登录”“退出”“查询”然后画几个小人和椭圆这其实没有体现用例图的真正价值。用例图要解决的核心问题是“系统为谁提供什么可观察的业务价值”所以第一步是找参与者Actor它们是系统外部主动发起交互的角色。在本项目里参与者可以归纳为四类居民普通用户、医生、系统管理员、市场运营人员OperateMarketStaff对应的就是运营端操作员。按这个思路用例表可以这样拆参与者核心用例说明居民注册登录、维护健康档案、预约挂号、查看健康建议居民是系统的主要服务对象医生查看居民档案、录入健康状态、推送健康建议医生需要触达居民的健康档案数据管理员用户管理、权限分配、日志审计对应OperateUser模块运营人员管理市场信息、维护科普内容、统计分析对应OperateMarketStaff模块这里最容易犯的错是“用例里放技术动作”。比如“连接数据库”“写入 Redis”绝对不要画成用例因为这不是业务目标数据库操作是类图里的关联关系不是参与者和系统的交互。另一个常见问题是“登录”到底算不算用例。如果登录只是其他功能的前置条件可以把它提炼为“身份认证”用例而不是让每个用例都绑一个登录。我一般建议先用“用户故事 业务目标”枚举候选用例再一次性检查是否对应至少一个参与者否则就删掉。2.2 从用例到类把 Loginview、HealthCondition 变成类图用例图定下功能边界后类图负责描述这些功能背后的静态结构。解压这个项目源码文件名基本能对应到 UML 类图里的候选类C 源码文件UML 类名职责Loginview.cpp/hLoginView登录界面与认证入口Userinfoview.cpp/hUserInfoView居民个人信息展示与编辑HealthCondition.cpp/hHealthCondition健康状态记录如血压、血糖、状态标记HealthAdvice.cpp/hHealthAdvice健康建议内容及其推送逻辑OperateUser.cpp/hOperateUser管理员操作用户账号OperateMarketStaff.cpp/hOperateMarketStaff运营人员管理市场信息类图里要标明关系。比如一个居民可以有多条HealthCondition记录所以UserInfoView和HealthCondition之间是聚合关系HealthAdvice通常基于某条HealthCondition产生二者是关联关系OperateUser和OperateMarketStaff在业务上都涉及“账号”这个基础实体可以抽象一个BaseOperator基类让它们继承。套用到这个项目类图的核心骨架可以写成下面的文本形式不依赖绘图工具先做关系推演UserInfoView o-- HealthCondition : 拥有多条 HealthCondition -- HealthAdvice : 触发产生 HealthAdvice -- UserInfoView : 推送目标 OperateUser -- BaseOperator : 继承 OperateMarketStaff -- BaseOperator : 继承 LoginView -- UserInfoView : 登录成功后跳转对应到 CUserInfoView里应该有一个保存HealthCondition的容器成员而不是每次动态new出来失去所有权。下面是一段符合上述聚合关系的头文件骨架// health_condition.h class HealthCondition { private: int userId; // 所属居民 ID QString bloodPressure; // 血压值 QString bloodSugar; // 血糖值 int status; // 0健康 1亚健康 2疾病 3康复 public: int getUserId() const; void setStatus(int s); bool isValid() const; // 校验健康记录是否完整 };// user_info_view.h #include vector #include health_condition.h class UserInfoView { private: int userId; std::vectorHealthCondition* conditions; // 聚合关系拥有多条记录 public: void addCondition(HealthCondition* c); void clearConditions(); void displayHealthHistory() const; };代码里的std::vectorHealthCondition* conditions就是类图中聚合关系的落地。这里我特意用指针表示UserInfoView负责管理这些记录的生命周期如果只是临时引用应该用普通指针或引用避免误删。类图里画关系时聚合用空心菱形组合用实心菱形网上不少教程把二者混用答辩时被问就很容易露馅。判断标准很简单组合关系里部分和整体同生共死聚合关系里部分可以脱离整体独立存在。居民账号删除后健康记录会一并清除这看起来更像组合但在本项目中健康诊历也需要被医生独立调阅即使居民账号注销医疗数据通常还要保留一段时间所以这里画聚合更符合业务合规要求。3. 状态图与活动图把健康状态流转和预约流程讲清楚3.1 居民健康状态机从健康到疾病的 UML 状态图类图画完静态结构动态行为里最重要的就是状态图。对社区健康管理系统来说居民的健康状态不是固定字段而是会迁移的状态机。项目里的HealthCondition类有一个status字段这个字段取值如果只用字典表逻辑会散落在代码里调完一个函数不知道下一个状态是什么。UML 状态图正好能描述这种生命周期。以HealthCondition.status为例可以定义四个状态健康Healthy、亚健康Subhealthy、疾病Diseased、康复Recovering。状态之间的迁移触发条件可以整理成状态迁移表当前状态触发事件迁移动作目标状态健康体检指标异常记录异常指标亚健康亚健康指标持续恶化医生诊断疾病疾病治疗开始登记治疗方案康复康复再次体检指标恢复清除异常标记健康疾病病情加重转上级医院疾病自迁移这里面最关键的是“疾病”状态的自迁移。很多同学在状态图里漏掉自迁移导致实际代码里disease对象无法在同一状态下更新严重程度。状态图不是日记而是状态机的完整描述所有合法迁移都要在图上体现哪怕起点和终点相同。对应到代码状态迁移应该集中在一个方法里比如void HealthCondition::applyEvent(const HealthEvent event) { switch (status) { case HEALTHY: if (event.isAbnormalCheck()) status SUBHEALTHY; break; case SUBHEALTHY: if (event.isConfirmedByDoctor()) status DISEASED; break; case DISEASED: if (event.isStartTreatment()) status RECOVERING; if (event.isWorsening()) status DISEASED; // 自迁移 break; case RECOVERING: if (event.isRecovered()) status HEALTHY; break; default: break; } }注意这里的状态迁移表和代码是一一对应的。为什么要在状态图里做这些细化因为纯写if-else极其容易漏掉“自迁移”和“非法迁移”状态图会强迫你把所有事件和状态组合列出来连不能迁移的组合也看得一清二楚。比如“健康”状态收到“治疗开始”事件到底是不允许还是忽略状态图上不画这条线代码里就要显式处理否则行为不确定。3.2 预约挂号活动图用泳道图厘清医生、居民和系统状态图关注单个对象的转换活动图关注业务流程的完整流转。社区健康管理系统里最典型的流程是“预约挂号”这个流程涉及居民、系统、医生三个主体用带泳道的活动图能让职责边界非常清楚。活动图的核心是活动、决策点和并发分支。我通常先画无泳道版本梳理出所有步骤居民选择科室、系统查询医生排班、居民选择时段、系统锁定号源、居民确认支付/登记、系统生成挂号单并通知医生。然后再加泳道把每个活动归属到具体参与者下。改进后的步骤序列如下居民发起预约请求系统校验登录状态返回科室列表居民选择科室和医生系统查询号源标记可选时段居民选择时段并提交系统执行两个并发活动锁定号源、生成挂号单系统向医生端推送新挂号提醒医生确认接诊流程结束。为什么第 6 步要画成“并发分支”因为在实际实现里锁号源失败的话挂号单不应该生成这两个活动有依赖不该并发。更严谨的建模应该是“锁定号源”成功后才“生成挂号单”。所以活动图里要用一个分支判断节点号源是否充足然后决定走“生成挂号单”还是“释放号源并返回失败”。这里可以顺带提一下 UML 和 DFD 的区别数据流图DFD描述的是数据如何流动、在哪里存储活动图描述的是控制流的分支和并发做课设时不要把 DFD 里的“数据存储”直接搬到活动图里当活动用。活动图映射到代码时最常见的实现模式是“一个用例对应一个服务方法”。下面是一个简化的AppointmentService::createAppointment流程bool AppointmentService::createAppointment(int patientId, int doctorId, const QDateTime slotTime, QString* message) { if (!hospitalRepository_-isSlotAvailable(doctorId, slotTime)) { *message 该时段已被预约; return false; } if (!hospitalRepository_-lockSlot(doctorId, slotTime)) { *message 锁定号源失败请重试; return false; } int orderId orderRepository_-createOrder(patientId, doctorId, slotTime); if (orderId 0) { hospitalRepository_-unlockSlot(doctorId, slotTime); *message 创建挂号单失败; return false; } notificationService_-notifyDoctor(doctorId, orderId); return true; }这个服务方法把活动图里的“决策点”变成了if分支把“并发活动”变成了先锁号、后建单的时序控制。可以看到活动图如果只画“开始-动作-结束”就退化成了流程图没有建模意义只有把分支条件和并发边界画清楚写代码时才能直接照着图填逻辑。4. 序列图与部署图交互顺序和物理部署的实操4.1 序列图医生发送健康建议的完整消息序列状态图定义状态变化活动图定义流程序列图则强调多对象之间“消息的先后顺序”。以“医生通过系统向居民发送健康建议”为例这个场景涉及的对象包括DoctorApp医生端、HealthAdviceService建议服务、HealthCondition健康数据、UserInfoView居民端展示。序列图里需要明确每个消息的发送方向、方法名和返回类型。这里用表格描述消息顺序比画图更能快速落到代码序号发送者接收者消息/方法返回内容1医生端建议服务getHealthCondition(patientId)HealthCondition2建议服务健康数据对象listRecentRecords(patientId)近期健康记录列表3医生端建议服务generateAdvice(conditionId)HealthAdvice草稿4医生端建议服务publishAdvice(adviceId)bool推送结果5建议服务居民端pushAdvice(adviceId)异步通知从这张表可以看到医生端并没有直接操作HealthCondition对象而是通过HealthAdviceService间接获取数据。序列图里这叫做“通过控制对象转调”能降低医生端与数据对象的耦合。对应到代码OperateUser和HealthAdvice模块之间的调用也是这样组织的。我之前见过一个错误写法医生端直接访问数据库表把HealthAdvice当成了纯工具类结果每次修改数据库字段都要改界面代码。序列图的作用就是提前暴露这种坏味道。消息序列图绘制时注意生命线上方是对象名冒号类名下方是纵向虚线。如果两个对象之间发生方法调用用实线箭头加括号参数返回内容用虚线箭头。很多工具默认生成“同步消息”和“异步消息”两种箭头但课设里不建议混用除非你真的用到了消息队列。本项目里pushAdvice就是异步通知用异步消息箭头表示更准确。4.2 部署图与包图让课设答辩时能说清系统架构部署图解决的是“系统跑在哪些物理节点上”的问题。实际开发中社区健康管理系统至少有三种部署形态居民端PC/移动端、医生工作站、服务器集群。如果只是课设 demo可以简化成两层架构但不能不画部署图否则答辩老师会问“你的登录校验是在客户端做还是在服务端做”。一个可接受的部署图文本描述如下[ 客户端节点: Qt C 桌面应用 ] -- HTTP/JSON -- [ 应用服务器: 业务服务 ] -- JDBC -- [ 数据库服务器: MySQL ]这里的关键是画清楚网络边界。客户端只通过 HTTP 接口访问业务服务业务服务持有数据库连接池客户端直连数据库是绝不允许的。对应到项目里Loginview和Userinfoview属于客户端进程内对象OperateUser和OperateMarketStaff的数据库操作应该在服务端完成。包图则是站在模块化角度管理类图避免所有类画在一张图上失控。常见做法是按“三层架构”分包view 包 : LoginView, UserInfoView, HealthAdviceView service 包 : HealthAdviceService, OrderService model 包 : HealthCondition, UserAccount, Appointment repository 包 : UserRepository, HealthConditionRepository包图之间的依赖要严格保持“view 依赖 serviceservice 依赖 repository”不能出现 view 直接依赖 repository 的关系。这个依赖方向其实比类图本身更能看出系统是否分层。我把部署图和包图放在同一章讲是因为答辩时很多人只盯着用例图和类图遇到“系统怎么部署、模块怎么组织”就开始含糊。能画清楚部署图和包图往往比多画一张协作图更能体现工程感。5. 从 UML 模型到 C 代码校验模型一致性并自动生成骨架5.1 用 Enterprise Architect 导出代码骨架UML 模型画完不等于结束。我在实际项目中至少会把中间的类图转成代码骨架常见做法是用 Enterprise ArchitectEA16 打开.mdl或.eap文件选中类图后右键执行“Generate Code”。这个操作能一次性生成所有类的头文件和源文件避免手写造成属性名、类型不一致。生成前要重点设置两处一是语言选 C二是代码生成模板选 “C11/14/17”这样QString这类自定义类型不会被错误映射。类图里每个属性都要显式指定类型比如status:int、userId:int、bloodPressure:string否则 EA 默认映射成int或string课程设计里基础类型写错连编译都会过不了。以HealthCondition类为例类图里定义了两个私有属性和两个公有方法生成出的头文件类似下面这样// Generated from UML class HealthCondition #ifndef HEALTHCONDITION_H #define HEALTHCONDITION_H #include QString class HealthCondition { public: HealthCondition(); ~HealthCondition(); int getUserId() const; void setUserId(int value); QString getBloodPressure() const; void setBloodPressure(const QString value); private: int userId; QString bloodPressure; }; #endif这里生成的是纯数据类。如果类图里已经画了方法之间的关联比如HealthAdvice依赖HealthCondition生成时还会自动加入前置声明和成员指针。逻辑说明EA 的代码生成器会把 UML 属性映射成成员变量、方法映射成函数声明但不会自动生成函数体。函数体需要你手写所以建议把“自动生成”定位成骨架而不是最终代码。参数说明Generate Code弹窗里的 “Synchronize Directory” 选项一定不要勾选否则它会扫描目录并删除它认为多余的旧代码文件这个坑我踩过好多次。5.2 模型与代码不一致的排查技巧模型和代码必须双向同步。课设中最常见的不一致有四种不一致场景原因排查与修复类图里有方法代码里没有生成后方法被手动删除右键类图方法选择“Synchronize with Code”代码里有关联类图没有开发时临时加成员变量反向工程导入头文件生成类状态图事件没有对应方法状态图重画了但代码没改用状态迁移表比对方法名属性类型不一致QString在类图里写成string统一采用代码侧类型再同步模型我建议在答辩前做一个简单验证在 EA 中对所有类执行一次 “Model Consistency Check”它会列出缺失方法和类型不匹配。对于状态图可以把状态迁移表里的事件整理成一个头文件里的枚举并让HealthCondition的applyEvent方法接收这个枚举这样模型和代码的绑定更紧。有一个很实用的技巧藏在 EA 的代码生成模板里如果你想保留手写的函数体可以把手写部分放在类代码块的首尾自定义区域。具体是点击Project - Options - Source Code Engineering - C在 “Custom Templates” 里找到类声明模板加入//%class(%verbatim%decl(class)) //%classMembers()然后把手写逻辑放进编辑器生成的代码保护区//%code_start和//%code_end之间。这样后续再同步模型保护区外的内容会被覆盖但保护区里的手动实现会原样保留。这是我在多个项目里验证过最稳妥的方式。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进