佛山专业的免费网站优化,网络推广外包想手机蛙软件,济阳做网站,信息门户登录入口个人首页#xff1a; VON 鸿蒙系列专栏#xff1a; 鸿蒙开发小型案例总结 综合案例 #xff1a;鸿蒙综合案例开发 鸿蒙6.0#xff1a;从0开始的开源鸿蒙6.0.0 鸿蒙5.0#xff1a;鸿蒙5.0零基础入门到项目实战 Electron适配开源鸿蒙专栏#xff1a;Electron for Open…个人首页 VON鸿蒙系列专栏 鸿蒙开发小型案例总结综合案例 鸿蒙综合案例开发鸿蒙6.0从0开始的开源鸿蒙6.0.0鸿蒙5.0鸿蒙5.0零基础入门到项目实战Electron适配开源鸿蒙专栏Electron for OpenHarmonyFlutter 适配开源鸿蒙专栏Flutter for OpenHarmony本文所属专栏鸿蒙综合案例开发本文atomgit地址小V健身小V健身助手开发手记六用服务层统一业务逻辑——KeepService 的设计、实现与架构演进一、为何需要服务层——从混乱到有序的必然选择二、KeepService 的整体设计1. **输入输出明确**2. **无状态 无副作用**3. **领域驱动**三、核心功能详解1新增运动记录insert(keepId: number, amount: number)2查询与转换selectRecordByDate(date: number)3统计信息生成calculateKeepInfo(records: RecordVO[])四、服务层与各模块的协作关系五、工程实践与最佳建议1. **错误处理策略**2. **性能优化**3. **可测试性**4. **未来扩展方向**六、总结服务层——架构成熟的标志用服务层统一业务逻辑——KeepService的设计、实现与架构演进在移动应用开发中随着功能的不断丰富代码复杂度呈指数级增长。若缺乏清晰的分层与职责划分项目将迅速陷入“面条式代码”的泥潭——UI 层充斥着数据库操作、数据模型混杂业务规则、测试难以覆盖核心逻辑。这不仅拖慢迭代速度更埋下维护隐患。在「小V健身助手」的开发进程中我们始终秉持“关注点分离”Separation of Concerns这一软件工程基本原则。前五篇中我们完成了声明式 UI 构建、关系型数据库集成、历史记录展示等关键模块。但直到引入服务层Service Layer整个系统才真正实现了从“能跑”到“健壮”的跃迁。本篇将以KeepService为核心全面阐述服务层在 HarmonyOS ArkTS 项目中的设计思路、实现细节、工程价值与最佳实践为构建可扩展、可测试、高内聚的健康类应用提供完整范式。一、为何需要服务层——从混乱到有序的必然选择在早期版本中运动记录的增删改查逻辑直接嵌入在首页组件HomePageContent中// ❌ 反模式UI 层直接操作数据库asynconComplete(keepId:number,amount:number){constrecordnewRecordPO();record.keepIdkeepId;record.amountamount;record.createTimeDate.now();constidawaitDBUtil.insert(recode,buildBucket(record),COLUMNS);if(id0){// 更新成就...// 刷新列表...}}这种写法看似简单却带来三大问题逻辑复用困难个人中心、历史页若需新增记录必须复制粘贴相同代码测试成本高昂无法对“新增记录并计算卡路里”这一业务单元进行独立测试变更风险集中一旦数据库表结构变更所有 UI 组件均需同步修改。而服务层的引入正是为了解决这些问题。它作为UI 与数据访问之间的协调者承担以下核心职责封装业务规则如“完成量等于计划量才算成功”、“卡路里 时长 × 单位消耗”协调多个数据源组合RecordModelRDB与RecordItemModel内存配置提供统一接口对外暴露语义清晰的方法如insert,selectRecordByDate隐藏底层实现细节。✅ 服务层不是简单的“方法转发器”而是业务语义的承载者。二、KeepService的整体设计KeepService是一个单例类通过new KeepService()实例化后全局共享其设计遵循以下原则1.输入输出明确所有方法参数均为原始类型或轻量 VOValue Object返回值为 Promise便于异步链式调用抛出明确异常如Error(记录ID不能为空)便于上层处理。2.无状态 无副作用不持有 UI 上下文context不直接操作 UI 状态如State所有外部依赖如RecordModel,RecordItemModel通过模块导入而非传参。3.领域驱动方法命名体现业务意图calculateKeepInfo而非getStats数据转换在服务层完成PO → VOUI 层只消费最终结果。三、核心功能详解1新增运动记录insert(keepId: number, amount: number)insert(keepId:number,amount:number):Promisenumber{constselecterDateAppStorage.Get(selecterDate)asnumber;constcreateTimeselecterDate!undefined?selecterDate:DateUtil.beginTimeOfDay(newDate());constrecordPOnewRecordPO();recordPO.keepIdkeepId;recordPO.amountamount;recordPO.createTimecreateTime;recordPO.successAmount0;// 初始未完成returnRecordModel.insert(recordPO);}设计亮点日期上下文感知通过AppStorage获取全局选中日期确保记录归属正确默认值安全successAmount显式初始化为 0避免数据库空值歧义解耦持久化调用RecordModel.insert不关心 SQL 或事务细节。 此方法被首页“完成”按钮、快捷录入弹窗等多处调用体现复用价值。2查询与转换selectRecordByDate(date: number)这是服务层最典型的“数据聚合”场景asyncselectRecordByDate(date:number):PromiseRecordVO[]{constrecordPOsawaitRecordModel.queryByDate(date);returnrecordPOs.map((rp:RecordPO){if(rp.idundefined||rp.createTimeundefined){thrownewError(记录数据不完整);}constrecordItemRecordItemModel.getById(rp.keepId);constcalorierp.amount*recordItem.calorie;returnnewRecordVO(rp.id,rp.keepId,calorie,recordItem,// 注入运动项元数据rp.amount,rp.successAmount,rp.createTime);});}关键设计决策问题解决方案如何获取运动项名称/图标通过RecordItemModel.getById(keepId)查询内存中的配置列表keeps数组卡路里何时计算在服务层实时计算避免 UI 层重复逻辑或数据库冗余存储PO 与 VO 如何区分RecordPO仅用于数据库映射RecordVO包含业务字段如calorie,recordItem供 UI 直接使用✅ 这种“PO → VO 转换”模式是服务层的核心价值之一将原始数据转化为业务就绪的数据。3统计信息生成calculateKeepInfo(records: RecordVO[])成就系统、首页概览卡片均依赖此方法calculateKeepInfo(records:RecordVO[]):KeepInfo{constinfonewKeepInfo(0,0,0,0);if(!records||records.length0)returninfo;info.taskrecords.length;records.forEach((rv:RecordVO){// 成功判定计划量 0 且 完成量 计划量if(rv.amount!0rv.successAmountrv.amount){info.successTask1;}info.expendrv.calorie;info.dayrv.createTime;// 使用最后一条记录的日期});info.isSuccess(info.taskinfo.successTaskinfo.task!0);Logger.debug(KeepService,${info.task}/${info.successTask}, 任务时间${info.day});returninfo;}业务规则显性化成功条件不再是“魔法数字”而是清晰的布尔表达式日志输出便于调试当日任务完成情况返回KeepInfo对象包含task,successTask,expend,isSuccess,day五个维度满足多场景需求。 注意info.day取最后一条记录的时间适用于“按日统计”场景。若需支持周/月视图可扩展为传入时间范围。四、服务层与各模块的协作关系下图展示了KeepService在整体架构中的位置------------------ ------------------ | HomePage | | HistoryPage | | PersonContent |-----| Achievement... | ------------------ ------------------ ↑ 调用 | -------------- | KeepService | ←─── 业务规则、数据聚合 -------------- ↑ 调用 | ------------------ ------------------ | RecordModel | | RecordItemModel | | (RDB 操作封装) | | (内存配置列表) | ------------------ ------------------ ↑ | -------------- | Small_V_Health.db | | (SQLite 文件) | --------------协作流程示例用户点击“完成跳绳30分钟”HomePageContent调用KeepService.insert(0, 30)KeepService创建RecordPO设置createTime为今日0点调用RecordModel.insert()后者通过DBUtil写入数据库返回主键 IDHomePageContent刷新列表并触发成就检查成就页调用KeepService.selectRecordByDate(today)获取当日记录KeepService查询 RDB 查询RecordItemModel 计算卡路里 → 返回RecordVO[]成就页根据RecordVO渲染详情。✅ 整个过程无任何跨层调用每一层只与相邻层交互。五、工程实践与最佳建议1.错误处理策略服务层抛出Error由 UI 层捕获并展示友好提示如AlertDialog关键操作如删除应要求 UI 层二次确认而非在服务层处理。2.性能优化RecordItemModel使用内存数组keeps避免频繁读取 Preferences大数据量查询如全年记录应分页加载KeepService可扩展queryByDateRange方法。3.可测试性虽然当前未展示测试代码但KeepService天然支持单元测试// mock RecordModel 和 RecordItemModelconstmockRecordModel{queryByDate:jest.fn()};constmockItemModel{getById:jest.fn()};// 测试 calculateKeepInfo 逻辑constservicenewKeepService(mockRecordModel,mockItemModel);constresultservice.calculateKeepInfo([...]);expect(result.successTask).toBe(2);4.未来扩展方向引入缓存对selectRecordByDate结果做短期缓存减少数据库查询支持撤销在insert后返回操作 ID配合deleteById实现“撤回”事件通知通过Emitter通知 UI 层数据变更替代手动刷新。六、总结服务层——架构成熟的标志KeepService的引入标志着「小V健身助手」从“功能堆砌”走向“架构驱动”。它不仅是代码组织方式的升级更是开发思维的转变从前 “怎么把数据存进去、读出来”现在 “用户的运动行为意味着什么如何用数据讲述他的坚持故事”通过服务层我们将零散的操作升华为连贯的业务叙事用户完成一次跳绳 → 系统记录计划与实际 → 计算卡路里消耗 → 更新当日成就状态 → 在历史中留下足迹。这正是优秀应用体验的底层支撑。下一站我们将基于KeepService提供的丰富数据构建可视化图表让用户一眼看清自己的进步轨迹——敬请期待《小V健身助手开发手记七用曲线讲述你的坚持》。附录关键类说明类名职责所在目录KeepService业务逻辑协调、数据聚合、规则计算/service/KeepService.tsRecordModel封装 RDB 表recode的 CRUD 操作/model/RecordModel.tsRecordItemModel提供运动项元数据名称、图标、单位、卡路里/model/RecordItemModel.tsRecordPO数据库实体映射对象Persistence Object/commond/tables/RecordPO.tsRecordVO业务视图对象View Object含计算字段/viewmodel/RecordVO.tsKeepInfo统计摘要信息任务数、完成数、卡路里等/viewmodel/KeepInfo.ts代码部分importRecordItemModel from../model/RecordItemModelimportRecordModel from../model/RecordModelimportRecordPO from../commond/tables/RecordPOimportDateUtil from../util/DateUtilimportLogger from../util/LoggerimportKeepInfo from../viewmodel/KeepInfoimportRecordVO from../viewmodel/RecordVOclass KeepService{// 新增运动记录 insert(keepId: number, amount: number): Promisenumber{// 获取选中日期或当前日期的开始时间戳 const selecterDateAppStorage.Get(selecterDate)as number;const createTimeselecterDate!undefined ? selecterDate:DateUtil.beginTimeOfDay(new Date());// 创建RecordPO对象并设置属性 const recordPOnew RecordPO();recordPO.keepIdkeepId;recordPO.amountamount;recordPO.createTimecreateTime;recordPO.successAmount0;// 初始完成量为0returnRecordModel.insert(recordPO);}// 根据ID删除运动记录 deleteById(id: number): Promisenumber{returnRecordModel.delete(id);}// 更新运动记录 update(record: RecordVO): Promisenumber{// 检查记录ID是否存在if(record.idundefined){throw new Error(记录ID不能为空);}// 创建RecordPO对象并设置属性 const recordPOnew RecordPO();recordPO.idrecord.id;recordPO.keepIdrecord.keepId;recordPO.amountrecord.amount;recordPO.successAmountrecord.successAmount;recordPO.createTimerecord.createTime;returnRecordModel.update(recordPO, record.id);}// 查询所有运动记录 async selectAllRecord(): PromiseRecordVO[]{// 查询所有RecordPO记录 const recordPOsawait RecordModel.queryAll();// 将PO转换为VO并补充相关信息returnrecordPOs.map((rp: RecordPO){// 检查必要字段是否存在if(rp.idundefined||rp.createTimeundefined){throw new Error(记录数据不完整缺少必要字段);}// 通过运动项ID查询RecordItem对象 const recordItemRecordItemModel.getById(rp.keepId);// 计算卡路里消耗运动时长 × 单位卡路里 const calorierp.amount * recordItem.calorie;// 创建RecordVO对象使用完整的构造函数参数 const recordVOnew RecordVO(rp.id, rp.keepId, calorie, recordItem, rp.amount, rp.successAmount, rp.createTime);returnrecordVO;});}// 根据日期查询运动记录 async selectRecordByDate(date: number): PromiseRecordVO[]{// 查询指定日期的RecordPO记录 const recordPOsawait RecordModel.queryByDate(date);// 将PO转换为VO并补充相关信息returnrecordPOs.map((rp: RecordPO){// 检查必要字段是否存在if(rp.idundefined||rp.createTimeundefined){throw new Error(记录数据不完整缺少必要字段);}// 通过运动项ID查询RecordItem对象 const recordItemRecordItemModel.getById(rp.keepId);// 计算卡路里消耗运动时长 × 单位卡路里 const calorierp.amount * recordItem.calorie;// 创建RecordVO对象使用完整的构造函数参数 const recordVOnew RecordVO(rp.id, rp.keepId, calorie, recordItem, rp.amount, rp.successAmount, rp.createTime);returnrecordVO;});}// 将RecordVO数组转换为KeepInfo统计信息 calculateKeepInfo(records: RecordVO[]): KeepInfo{// 使用默认参数创建KeepInfo对象 const infonew KeepInfo(0,0,0,0);// 检查记录是否存在if(!records||records.length0){returninfo;}// 设置总任务数 info.taskrecords.length;// 统计各项指标 records.forEach((rv: RecordVO){// 判断任务是否完成运动量不为0且完成量等于计划量if(rv.amount!0rv.successAmountrv.amount){info.successTask1;}// 累计卡路里消耗 info.expendrv.calorie;// 设置日期使用最后一条记录的日期 info.dayrv.createTime;});// 判断是否所有任务都完成if(info.taskinfo.successTaskinfo.task!0){info.isSuccesstrue;}// 输出调试信息总任务数/已完成任务数任务时间 Logger.debug(KeepService,${info.task}/${info.successTask},任务时间${info.day},总任务/已完成任务);returninfo;}}