简介:资源是一份软件工程课程设计报告,围绕智慧养老服务平台完整展示面向对象分析与设计(OOAD)过程,适用于计算机、软件工程专业学生及正在完成UML建模课设的开发者。报告以实验形式组织,系统涉及老人、家属、医护人员和管理员四类参与者,逐一给出角色用例图;类图部分不仅列出各类的属性和服务,还梳理了用户与家庭、老人与身体信息、线上诊室与医护人员等对象间的依赖、聚合、组合、泛化和关联关系;顺序图则选取核心业务场景展示对象间的消息传递与协作时序。通过这份报告可以快速掌握用例建模、类图设计和行为建模的规范写法,并借鉴养老平台业务分析思路,将其迁移到类似管理系统中。资料为一个doc格式文档,体积约414KB,便于下载和按需修改。目前已有103人学习浏览,适合作为课程设计参考模板或OOAD方法入门案例。
1. 智慧养老服务平台为什么绕不开面向对象分析和设计
做智慧养老服务平台这类多角色、强状态、重告警的软件工程项目,最容易翻车的地方不是代码写不出来,而是需求还没成型就开始建表写接口。面向对象分析和设计这部分,恰恰是把老人的健康数据、工单流转、设备告警这些模糊业务,翻译成用例图、类图、时序图等可评审、可追溯、可编码的设计资产。很多人把画UML当成评审时走形式的玄学,可真到业务规则一变,只有设计图上把职责分清楚的项目能少改代码。这篇笔记会把智慧养老服务平台在面向对象设计和分析阶段的落地做法拆开讲,适合带课程项目、毕业设计,或者正从零设计同类系统的开发者。先别急着写代码,设计阶段省下的时间,后面会用加倍的调试还回来。
2. 用例建模:先把业务边界画清楚,参与者是谁、系统干什么都在这张图上
2.1 识别参与者与核心用例:先从每个角色的目标倒推
用例模型首先是定边界。智慧养老服务平台里常见的参与者包括老人、家属、护理员、值班医生、平台管理员,有时还有外部的穿戴设备上报服务。注意设备在用例图里有两种建模方式:如果平台只被动接收数据,设备可以当作外部系统参与者;如果平台的告警规则完全由运营人员配置,那设备参与者的很多用例会被管理员用例替代。我一般会先做一轮“目标倒推”,相当于替每个角色回答一个问题:我打开这个系统是为了完成哪一件事。
老人侧的目标是被照护,所以用例聚焦在发起紧急呼叫、查看自己的健康档案、接收用药提醒;家属侧的目标是知情和确认,所以用例是查看老人健康周报、接收异常告警、远程确认工单;护理员侧的目标是处理问题,所以用例是处理告警、填写巡检记录、上报异常。你发现没有,这样列出来的每条用例都有“动词加宾语加目标”,而不是“健康档案管理”这种模块名。这就是用例建模和功能树最大的区别。
2.2 用include和extend梳理用例之间的关系
当用例数量超过十个,就要处理关系。include和extend是两种不要混淆的关系。include表示被包含的用例是主用例在执行过程中必然要走的一段子流程,比如“发送短信通知”大概率是“处理跌倒告警”必须调用的子流程,这就是include关系。extend表示扩展场景,只有满足特定条件才触发,比如“深夜时段无人响应,将工单升级给管理员”是“处理告警”的扩展,这就是extend。
我见过不少团队把extend画成include,导致测试时把升级路径当成主流程来跑,正常分支和异常分支混在一起。判断标准就一条:删掉这条关系,主用例的业务目标还能不能成立。能成立,就是extend;不能成立,就是include。用这个标准去画,告警处理里“通知家属”是include,“通知失败后走短信补发”才是extend。这个区分直接决定后面状态图里的分支条件和测试用例的优先级。
2.3 用用例图脚本把角色和目标画出来:PlantUML最简写法
画用例图我推荐用PlantUML,纯文本维护,改用例名或角色时不会把图形拖得乱七八糟,放进git里做diff评审也方便。下面这个脚本是“安全监护”场景的用例图,保存成txt文件,用PlantUML直接渲染即可。
@startuml left to right direction actor "老人(佩戴设备)" as Elder actor "家属" as Family actor "护理员" as Nurse actor "平台管理员" as Admin rectangle "智慧养老服务平台" { usecase "发起紧急呼叫" as UC1 usecase "接收跌倒告警" as UC2 usecase "查看健康档案" as UC3 usecase "处理告警工单" as UC4 usecase "配置告警阈值" as UC5 } Elder --> UC1 Elder --> UC3 Nurse --> UC2 Nurse --> UC4 Family --> UC2 Admin --> UC5 ' 处理告警工单包含通知家属的子流程 UC4 ..> UC1 : include @enduml这段脚本里有两处值得说明。第一,left to right direction把参与者从竖排放成横排,参与者和用例之间的连线会变短,矩形窗口里能放下更多内容。第二,UC4 ..> UC1 : include表示处理告警工单这个主用例在执行时会包含“通知家属”的子流程,这里是简化表示,实际项目里更规范的做法是单独画一个“通知家属”子用例再连线。
用例图不需要把所有角色之间的交互都画出来,画到能评审业务边界即可。如果一个用例图超过二十个用例,说明这张图的边界画大了,赶紧拆成“老人监护”“家属服务”“平台管理”三张子用例图。每张子图配一段文字说明设计意图,评审时直接讨论这张子图,效率会高很多。
2.4 用例规约模板:把一张图扩展成能被测试团队看懂的需求
用例图只是索引,真正落地的是用例规约。每条核心用例都要配一张规约表,否则开发阶段必然出现“告警推送了但工单没建”“工单建了但家属不知道”这类切需求的问题。下面以“老人发起紧急呼叫”为例,给出规约的核心字段。
| 规约字段 | 内容 |
|---|---|
| 前置条件 | 老人已注册并在平台中绑定可用设备 |
| 后置条件 | 系统生成一条紧急事件记录,并推送告警给家属与值班护理员 |
| 主成功场景 | 1. 老人按下设备呼救按钮;2. 系统生成紧急事件单;3. 系统按配置顺序通知家属与护理员;4. 护理员领取事件单并回电确认;5. 系统记录处理结果并关闭事件单 |
| 扩展场景 | 2a. 设备离线,系统发送短信并标记待确认;3a. 家属与护理员30分钟内均未响应,系统升级通知管理员;4a. 护理员回电确认老人安全,转入健康回访流程 |
| 业务规则 | 同一老人同一时段只允许存在一个未关闭的紧急事件单 |
规约里的主成功场景,写的时候要遵循“谁做什么、系统响应什么”的句式,这样后面画时序图时,步骤几乎就是时序图消息的直接来源。扩展场景是排雷的关键,它决定告警升级、短信补发、超时重试这些分支在代码里要不要写。
需要明确的是,不是每个用例都值得写满扩展场景。我会按优先级给用例规约分级:核心安全用例必须写完整,比如紧急呼叫、跌倒告警处理;普通查询用例只需要前置和后置条件。规约分级能避免文档膨胀,也能让评审精力集中在会出事故的场景上。用例建模到这里,边界、角色、分支都齐了,下一步才能进入类和对象的静态设计。
3. 静态建模:用类图把老人、工单、告警这些对象搭出可实现的骨架
3.1 三个类别的职责边界:实体、边界、控制各管一摊
面向对象设计里最经典的问题就是类怎么划分。常见做法是采用实体类、边界类、控制类三分类,它和经验里的MVC分层能对得上,但又不完全是一回事。实体类承载业务数据和关键状态,比如老人档案、健康记录、事件工单;边界类直接和外部参与者打交道,比如告警推送接口、大屏展示页面、管理后台页面;控制类承载业务规则的编排,比如“跌倒告警处理流程”这个控制类读取设备事件、创建工单、触发通知。
智慧养老平台最怕的就是把控制类省掉。老人状态变化、告警降噪、通知升级这些规则本身就是系统价值,如果直接写在页面的事件回调或者数据库的存储过程里,后面改规则就等于重构。这个平台的业务特点决定了控制类是必须单独一层的,它把设备上报到业务处置之间的所有校验和编排集中起来,测试时也可以绕过界面直接测控制类,这是三分类最大的工程收益。
3.2 从用例规约提取候选类:名词法加责任分配三步走
拿到用例规约后怎么生成候选类,我常用一套很朴素的三步法。第一步先圈名词,把所有业务名词标出来:老人、健康记录、设备事件、事件单、工单、告警消息,这些优先进入实体类候选池。第二步再圈动词,重点看“系统生成”“系统判断”“系统推送”这类动作,一个动作往往对应一个控制类的方法,多个动作围绕一个业务目标时,就合并到一个控制类里。
第三步做责任分配,这一步最关键也最容易被跳过。责任分配的标准是一个类只回答一类问题。比如“告警消息”和“消息通知”经常被建模成两个实体类,但仔细看,一个承载的是告警数据本身,一个承载的是对外发送行为,数据和行为本质上是两回事。数据归实体类,发送行为和推送策略要放进控制类或服务类。这样后面做设备供应商替换时,只需要改边界类,健康数据和事件单模型完全不动。
3.3 告警处理类图:多重性、依赖方向与两个必查参数
下面用告警处理这个核心场景展示类图的写法,依然走PlantUML纯文本。注意类图里每个类都有方法,方法名随后面时序图直接引用。
@startuml class Elder { -elderId : String -name : String -roomNo : String -emergencyLevel : int +getHealthStatus() : HealthRecord } class HealthRecord { -recordId : String -heartRate : int -bloodOxygen : int -recordTime : Date } class DeviceEvent { -eventId : String -deviceNo : String -eventType : String -eventTime : Date -isHandled : boolean } class AlarmControl { +receiveDeviceEvent(event : DeviceEvent) : void +createWorkOrder(event : DeviceEvent) : WorkOrder +notifyFamily(event : DeviceEvent) : void } class WorkOrder { -orderId : String -status : String -handlerId : String -createTime : Date +assignTo(nurseId : String) : void +complete(result : String) : void } Elder "1" -- "1" HealthRecord : 最新记录 Elder "1" -- "0..*" DeviceEvent : 产生 AlarmControl ..> DeviceEvent : 读取 AlarmControl ..> WorkOrder : 创建 DeviceEvent "1" -- "1" WorkOrder : 对应 @enduml这张图要重点检查两个参数。第一个是多重性,Elder到HealthRecord是1比1,表示一个老人只有一条最新健康状态记录,这个约束决定了表设计里可以用唯一索引去保证;Elder到DeviceEvent是1比0到多,一个老人可以产生很多次设备事件,这意味着事件表必须带elderId外键并建索引。第二个是依赖方向,AlarmControl依赖两个实体类,但DeviceEvent和WorkOrder都不依赖AlarmControl,实体层不感知控制层,这个方向反了就出问题。
在类图评审时我还会多查一眼:实体类之间的关联上有没有业务条件。DeviceEvent和WorkOrder是1比1,是因为一条设备事件只能派生出一种业务工单。如果哪天业务说“一条跌倒事件既要护理员上门又要医生远程回访”,这个1比1就要拆成一对多,并引入一个事件拆分类来维护对应关系。类图改起来不难,难的是在设计阶段就想到这层。
3.4 不要在类图里画数据库:类图和ER图的边界要分清
很多项目交付的类图,一眼看上去就是数据库ER图。类的属性全是varchar、int、datetime这种物理类型,没有任何方法名。出现这个问题的原因,是把数据库建模的惯性带进来了。类图表达的是对象在内存中的职责和协作,方法名才是类图的重点。至于落库时字段对应哪个表、用不用索引,那是物理设计阶段的事。
我评判一张类图是否合格就一个标准:把类名挡住,只看方法名和关联关系,能不能大概猜出这个类在业务里做什么。如果方法名全是get和set,那这张图对于代码编写没有指导意义。工具方面,StarUML、draw.io、PlantUML都可以,我的偏好是PlantUML,原因很简单,文本文件可以参与git diff,评审意见可以直接对应到行。
3.5 继承和接口该用在哪:设备事件的分类设计
面向对象设计里绕不开继承,智慧养老平台里最典型的继承场景就是设备事件。DeviceEvent是一个父类,它可以派生出跌倒事件、离床事件、低电量事件、SOS主动呼救事件。子类共用一个设备上报入口,但各自的处理规则不同。父类只需要保留eventId、deviceNo、eventTime、isHandled这些公共字段。这样设计的价值在于,新接入一种设备事件类型时,不需要修改告警处理控制类的核心逻辑,只要新增一个子类并注册对应规则。
接口的使用场景则是外部依赖的抽象。短信网关、语音通知、消息推送,这三类边界服务可以抽象成一个NotificationService接口,AlarmControl只依赖接口,不依赖具体短信厂商或推送厂商的实现。设备供应商有替换、发送通道有变更时,新增实现类即可,控制类和实体类一律不动。继承管分类,接口管替换,这组判断做好之后,类图的演进成本会小很多。
4. 动态建模:用时序图和状态图验证跌倒告警、工单流转这些关键流程
4.1 时序图:把主成功场景翻译成对象间的消息协作
静态类图回答“谁和谁是什么关系”,时序图回答“一个用例执行时对象之间按什么顺序发消息”。这里以“护理员处理跌倒告警”为例,对照前面的类图画出消息序列。
@startuml actor "护理员" as Nurse participant "告警页面" as Page participant "告警控制" as Control participant "事件实体" as Event participant "工单实体" as Order Nurse -> Page: 点击领取工单 Page -> Control: claimOrder(eventId) Control -> Event: lockEvent(eventId) Control -> Order: createWorkOrder(eventId, nurseId) Control -> Order: updateStatus("处理中") Order --> Control: orderId Control --> Page: 返回处理中工单 Nurse -> Page: 填写处理结果 Page -> Control: completeOrder(orderId, result) Control -> Order: updateStatus("已完成") Control --> Page: 返回操作成功 @enduml画这张图有两条硬规矩。第一,消息名必须与类图方法名一致,claimOrder、lockEvent、createWorkOrder、updateStatus全部能在第3章的类图里找到对应定义,这叫动态图与静态图对齐。第二,页面和实体没有直连消息,Page的所有请求都先打给Control,再由Control去操作Event和Order。如果评审时发现消息从页面直接改实体状态,说明控制层被穿透了,数据库事务和权限校验都会出漏洞。
时序图的粒度也有讲究。主成功场景在8到12条消息之间,信息量刚好适合评审。超过15条,说明这个控制类承担了太多责任,拆成两个控制类再画第二张图更清晰。另外,每个用例不一定都要画时序图,我的做法是优先级高的安全类用例必画,普通查询可以不画,防止文档数量失控吞掉开发时间。
4.2 状态图:工单状态的生命周期靠迁移条件约束
智慧养老平台里有两类状态模型很值得画状态图,一类是穿戴设备的状态,比如待机、告警触发、离线、低电量;另一类是业务工单状态,比如待领取、处理中、已完成、已升级、已关闭。这里重点讲工单状态。
状态图的价值在于迁移条件。待领取到处理中,条件是“护理员领取工单”;处理中到已完成,条件是“护理员提交处理结果”;处理中到已升级,条件是“超过30分钟无人领取”。这些条件只有写进状态图,测试用例才知道要构造什么样的数据才能走到升级分支。如果这些条件散落在自然语言文档里,开发和测试的理解几乎必然不一致。
注意状态图不要画成全连通。每个状态之间的迁移都要追问一句“业务上真的成立吗”。比如已关闭的工单不允许迁回处理中,即使数据库层面能update,流程上也不该存在这条路。把边界画死,等于帮后面写权限校验的人排掉了隐患。设备侧也一样,低电量状态可以被充电事件迁移回待机,但不能被业务告警直接迁移到离线状态,这两个状态之间的转换必须有硬性条件。
4.3 活动图只画并发场景:并行通知和夜间升级是典型例子
活动图在这个项目里不用多画,但有一个场景必须画,就是告警通知的并发处理。设备上报跌倒事件后,系统要同时做四件事:通知家属、通知值班护理员、生成工单、给老人播放语音确认。这四个动作之间没有严格的先后依赖,用时序图画会有强迫排序的问题,用活动图的分叉和汇合表达最合适。
活动图绘制时注意限定泳道。给家属的通知走短信网关,给护理员的通知走平台内消息,给老人的语音走设备通道,每条泳道对应的外部依赖不同。评审看到分叉和汇合时,重点确认汇合点是否有等待逻辑,比如是否要等家属确认后才关单。这个决策直接决定系统是同步通知还是异步通知。如果要求家属确认了才闭环,那就是同步等待;如果只负责发出去不管回执,那就是异步通知,后面接消息队列的取舍全看这个决定。
4.4 动态模型与静态模型的一致性核对
动态模型画完,不要急着进编码,先做一次一致性核对。我会用表格列出每个用例对应的时序图、状态图和涉及的类,逐行检查类名、方法名、状态名是否和类图、状态图完全一致。这一步是面向对象设计里最容易被偷懒的环节,也是后续编码阶段“文档与代码两张皮”的最大根源。
一致性核对在团队里可以做成评审清单项,不用开发工具,一张在线表格就够。核对时发现消息名和方法名对不上,先确认哪边才是正确的业务行为,再统一修改。一般来说以时序图为准回改类图更快,因为时序图更贴近真实交互逻辑。做完这一步,动态模型静态模型才真正成为一张网,而不是各自独立的装饰图。
5. 避坑指南:智慧养老OOA/OOD里你早晚会踩的6个高频翻车点
5.1 用例图画成功能菜单树
现象:用例图里全是“健康档案管理”“员工管理”“设备管理”,参与者只是挂在边上的装饰,看不出谁为谁服务,评审时每条用例都解释不通。
原因:设计者把用例建模当成了功能模块拆分,脑子里还是数据库表的CRUD思维,完全跳过了“角色目标”这个维度,画出来的自然是一棵功能菜单树。
解决:每个用例名改成“动词加对象加目标”,比如“护理员查看老人健康档案”“家属接收异常告警”。如果改完发现找不到明确的主参与者,说明这个用例不成立,当场删掉。这个检查项能逼着设计者回到角色视角,用例图才有评审价值。
5.2 类图里的实体类没有方法,只有属性
现象:类图交上来一看,全是id、name、time这些属性,方法栏一片空白,整体看起来像数据库表结构,根本没有面向对象的样子。
原因:把OOA/OOD做成了数据库建模,责任分配这一步被跳过了,类的行为没有经过识别,设计自然停留在数据层面。
解决:给每个实体类至少一个业务行为方法,哪怕它只是getHealthStatus、lockEvent这样的行为入口。行为方法表达的是这个类对外提供的服务,没有行为的类没有资格出现在设计文档里,这也是后续单元测试的初步划分依据。
5.3 时序图里的消息在类图里找不到对应方法
现象:时序图画得很完整,但类图里没有claimOrder、completeOrder这些方法名,评审时两张图对不上,编码时开发不知道以哪张图为准。
原因:先画图后统一,动态模型和静态模型没有放在同一轮迭代里核对,图形数量一多就开始脱节。
解决:把“时序图消息必须在类图里能找到对应方法”作为评审项。出现对不上就以时序图为准回改类图,或者反过来,无论如何不要让两张图各画各的,否则设计图就成了装饰品。
5.4 控制类变万能类,业务逻辑和状态全部塞进去
现象:系统里只有一个控制类,所有用例共用一个“大管家”,这个类有几十个方法,依赖图上拉着所有实体类,改一处就影响到全部流程。
原因:控制类在命名时没有按用例或业务目标拆分,设计者迁就了“少建类”的惰性,结果把一个高内聚的任务做成了高耦合的上帝类。
解决:按业务目标拆控制类,一个控制类负责一个完整业务闭环。比如告警处理控制类只管告警相关流程,健康档案控制类只管档案查询与统计,两者不要交叉依赖。如果两个控制类必须共享一段规则,把规则下沉到一个独立服务类里,而不是让其中一个控制类去调用另一个控制类的方法。
5.5 状态图漏了异常分支
现象:工单状态图里只有正常流转路径,评审时被问到“护理员一直不领取怎么办”“老人按了呼救但设备离线怎么办”,设计者答不上来。
原因:只顺着主成功场景画状态迁移,没有回到用例规约里的扩展场景逐条核对,异常分支全被漏掉了。
解决:把用例规约里的扩展场景逐条映射到状态图的迁移条件。30分钟未领取触发超时升级,设备离线时告警改为短信确认,这些都要在状态图里出现对应的迁移分支。排查时对着扩展场景编号一个个打勾,漏一条就补一条,直到全部覆盖。
5.6 为画图而画图,图与代码实现完全脱节
现象:文档里的设计图和最终写的代码完全是两套结构,代码里没有AlarmControl,也没有WorkOrder的lockEvent方法,程序员根本没按设计图写代码。
原因:设计评审只看文档不看代码实现,设计阶段和编码阶段彻底脱节,图变成了应付检查的文档作业。
解决:在迭代里规定“当周合并的代码必须标注对应的设计制品”,code review时抽查一个类,如果发现类名和方法与设计图对不上,要求先改设计图再改代码。设计图应该是代码的索引,而不是评审后封存的装饰品。
6. 用可追踪性矩阵收尾:证明你的智慧养老设计没有丢需求
可追踪性矩阵是面向对象设计阶段最后一道验证工具,它的作用是把用例、类、时序图、测试用例钉在同一张表上。以“护理员处理跌倒告警”这条用例为例,行内容写上它的业务目标,列里对应填入AlarmControl、WorkOrder、DeviceEvent相关的类图、处理流程时序图、工单状态图。逐行扫过去,任何一行存在空列,说明这条用例在设计里没有被完整承接,开发时就会漏功能。
我会在文档交付前做一次双向追踪。第一遍从需求往设计走,把每个用例映射到类和时序图,找“有需求但没设计”的空洞;第二遍从设计往需求走,把每个类、每个时序图回溯到它服务的用例,找“有设计但没需求”的孤儿类。孤儿类往往是过度设计的产物,比如为“未来可能出现的消息中心”先建了一整套泛型消息模型,这种类在实现阶段会持续消耗成本,还很难测。
追踪矩阵建议用在线表格维护,不要把矩阵画成图片,因为类名或方法名一旦改动,矩阵要跟着改,图片格式没法做批量更新。矩阵里只保留类名、方法名和状态名,不写描述性句子,这样改起来成本低。做完双向追踪后,整个面向对象设计分析阶段才算真正收官。我见过不少团队设计文档写了几十页,编码时却没有一张图能指导实现,后来养成追踪矩阵的检查习惯,设计文档才真正成为代码的索引,而不是评审后就被封存的黑匣子。希望帮到你。
本文还有配套的精品资源,点击获取