《软件工程实务》课程学习心得:从理论到实践的敏捷蜕变
关键词:软件工程、敏捷开发、Scrum、微服务、DevOps、Codeup、能源管理系统
可在该链接内学习相关内容:
https://www.bilibili.com/
一、写在前面
本学期我修读了《软件工程实务》课程,从课程概要到项目实战,系统学习了软件工程全生命周期。不同于以往只关注编码,这门课让我真正理解了产品愿景、用户故事、架构设计、进度管理、DevOps等工程化实践。
本文将结合课程内容、个人项目实践(能源管理系统)以及使用的工具(Codeup),分享我的学习心得。
一点背景:在修这门课之前,我对“软件工程”的理解停留在“写文档”三个字上。我以为它是一堆繁文缛节的规章制度,甚至会拖慢开发进度。但经过一个学期的学习,我彻底改变了这个看法——软件工程不是束缚,而是对复杂性的管理。当项目规模从几百行代码扩展到几千行、几万行,当团队从1个人变成5个人,当交付时间从“随便”变成“第8周周五下午5点截止”时,工程方法的价值就显现出来了。
二、课程结构速览
| 单元 | 核心内容 | 实践工具/产出 | 我的掌握程度(自评) |
|---|---|---|---|
| 1 | 课程介绍 + Codeup入门 | Git仓库、Markdown笔记 | ⭐⭐⭐⭐⭐ |
| 2 | 产品愿景 + 进度管理 | 甘特图、WBS | ⭐⭐⭐⭐ |
| 3 | 产品定义 + Git入门 | Codeup协作 | ⭐⭐⭐⭐⭐ |
| 4 | 用户故事 | 用户故事卡 | ⭐⭐⭐⭐ |
| 5 | 功能设计 + 能源功能清单 | 功能列表 | ⭐⭐⭐⭐ |
| 6 | 软件架构 + 微服务 | 架构图 | ⭐⭐⭐ |
| 7 | 业务架构设计 | 业务流程图 | ⭐⭐⭐⭐ |
| 8 | 技术架构 + 数据字典 | 数据库设计 | ⭐⭐⭐⭐ |
| 9 | 敏捷 + 产品积压项 | PB列表 | ⭐⭐⭐⭐⭐ |
| 10 | Scrum框架 | Sprint计划 | ⭐⭐⭐⭐ |
| 11 | 可靠编程 + 安全隐私 | 代码规范、加密示例 | ⭐⭐⭐ |
| 12 | 云计算 + DevOps | CI/CD流水线 | ⭐⭐⭐ |
📌 从自评可以看出,我对工具类和敏捷实践类的掌握更好,架构设计类还需要继续深入学习。
三、五大核心收获(详细展开)
1️⃣ 产品愿景驱动开发
在第二单元中,我们为“能源管理系统”制定了产品愿景。这不是随便写一句话,而是要回答三个核心问题:为谁做?解决什么问题?做到什么程度?
我们小组最终确定的产品愿景:
为园区物业管理人员提供实时、可视化的能耗监控与智能告警服务,帮助用户在6个月内实现15%以上的能源成本节约。
为什么这个愿景有效?
✅ 明确目标用户:园区物业管理人员
✅ 明确核心价值:实时、可视化、智能告警
✅ 明确量化指标:15%能源成本节约、6个月时间节点
我的体会:愿景不是贴在墙上就完事的。在后续的每个Sprint中,我们都会回到愿景来问自己:“这个功能真的帮助用户节约能源了吗?”有一次我们想做一个复杂的报表导出功能,但回顾愿景后发现用户更需要的是实时告警,于是调整了优先级。这就是愿景对开发的实际指导作用。
2️⃣ Git + Codeup 团队协作
在第三单元中,我们正式使用阿里云Codeup进行团队协作。我们小组有4个人,分别是:前端1人、后端2人、测试兼文档1人。
我们采用的Git工作流:
bash
# 1. 每天开始工作前,同步develop分支 git checkout develop git pull origin develop # 2. 基于develop创建功能分支(用feat/前缀 + 用户故事ID) git checkout -b feat/US-003-user-login # 3. 开发过程中,小步提交(每个提交只做一件事) git add src/main/java/com/energy/controller/UserController.java git commit -m "feat(login): 实现用户登录接口,返回JWT token" # 4. 开发完成后,推送到远程并创建合并请求 git push origin feat/US-003-user-login # 然后在Codeup网页上创建MR → 指定评审人 → 通过后合并到develop我们踩过的坑:
| 问题场景 | 错误做法 | 正确做法 |
|---|---|---|
| 两个人改了同一个文件的同一行 | 直接 push 被拒绝,然后强制 push ❌ | 先 pull → 解决冲突 → 再 push |
| 功能开发到一半,紧急需要切分支修bug | 直接 checkout 导致工作区混乱 ❌ | 用git stash暂存当前改动 |
| 合并请求被驳回,不知道哪里改 | 重新提交一个MR ❌ | 在同一个分支上继续 commit,MR会自动更新 |
我的体会:Git不是“把代码传上去就行”的工具,它是团队协作的通信协议。谁的commit message写得不清楚,谁就在给别人挖坑。我们小组后来约定:commit message必须按照类型(模块): 简短描述的格式,例如fix(auth): 修复token过期后未刷新问题。
🖼️图片说明(模拟)
图中展示了主分支(main)、开发分支(develop)和三个功能分支(feat/energy-chart、feat/alert-rule、feat/user-profile)的合并记录,以及最后一次CI/CD流水线的状态(绿色通过)。
3️⃣ 用户故事与敏捷积压
第四单元的用户故事是让我印象最深刻的内容之一。以前我写需求就是“做一个登录功能”,但用户故事强迫我们思考:谁要用?为什么要用?怎么才算完成?
标准格式:
md
作为 [角色] 我想要 [功能] 以便 [价值]我们编写的真实案例:
md
作为 能源管理员 我想要 查看指定时间范围(日/周/月/自定义)的用电趋势图 以便 发现高峰用电时段并制定错峰用电策略。验收标准(Acceptance Criteria):
✅ 可以选择时间范围:今日、本周、本月、自定义日期区间
✅ 以折线图形式展示,X轴为时间,Y轴为用电量(单位:kWh)
✅ 鼠标悬停时显示具体数值
✅ 可以导出图片(PNG格式)
产品积压项详细表(第9-10单元内容):
| 优先级 | 用户故事ID | 用户故事内容 | 估算(故事点) | 负责人 | 状态 |
|---|---|---|---|---|---|
| 🔴 高 | US-001 | 作为管理员,我希望通过账号密码登录系统 | 5 | 张三 | ✅ 已完成 |
| 🔴 高 | US-002 | 作为管理员,我希望查看所有设备的实时能耗数据 | 8 | 李四 | ✅ 已完成 |
| 🟡 中 | US-003 | 作为管理员,我希望为指定设备设置能耗告警阈值 | 5 | 王五 | 🔄 进行中 |
| 🟡 中 | US-004 | 作为管理员,我希望按日/周/月导出能耗报表(Excel) | 8 | 赵六 | ⏳ 待开发 |
| 🟢 低 | US-005 | 作为管理员,我希望对比不同设备的能耗排名 | 3 | - | 📋 积压中 |
| 🟢 低 | US-006 | 作为管理员,我希望接收邮件告警通知 | 5 | - | 📋 积压中 |
我的体会:用户故事是“沟通工具”而不是“合同文档”。在和产品经理、测试同学讨论时,用故事的形式比用长长的需求文档高效得多。验收标准写清楚了,测试用例也就有了基础,开发过程中扯皮的情况大大减少。
4️⃣ 软件架构:从单体到微服务
在第六和第七单元,我们学习了软件架构设计。最初我们设计的是一体化架构(前端+后端+数据库都在一个项目里),但随着功能增加,我们发现:
❌ 编译时间越来越长(从10秒到2分钟)
❌ 改一个小功能需要重新部署整个应用
❌ 某个模块出问题(比如告警模块内存泄漏)会导致整个系统不可用
于是我们决定向微服务演进。最终设计如下:
text
┌─────────────────┐ │ 前端(Vue.js) │ └────────┬────────┘ │ HTTPS ┌────────▼────────┐ │ API网关(Spring Cloud Gateway) │ └────────┬────────┘ │ ┌────────┬───────────┼───────────┬────────┐ │ │ │ │ │ ┌───────▼──────┐ ┌▼──────────▼──┐ ┌───────▼──────┐ ┌▼──────────┐ │ 认证服务 │ │ 数据采集服务 │ │ 能耗分析服务 │ │ 告警服务 │ │ (JWT) │ │ (Modbus/TCP) │ │ (InfluxDB) │ │ (SMTP/钉钉)│ └───────┬──────┘ └──────┬───────┘ └──────┬───────┘ └──────┬─────┘ │ │ │ │ └────────┬───────┴────────────────┴────────┬───────┘ │ │ ┌────────▼────────┐ ┌────────▼────────┐ │ MySQL │ │ InfluxDB │ │ (用户/权限) │ │ (时序数据) │ └─────────────────┘ └─────────────────┘各服务职责:
| 服务名称 | 职责 | 端口 | 技术栈 |
|---|---|---|---|
| API网关 | 路由转发、限流、鉴权 | 8080 | Spring Cloud Gateway |
| 认证服务 | 登录、Token颁发与校验 | 8081 | Spring Security + JWT |
| 数据采集服务 | 从电表读取数据,写入InfluxDB | 8082 | Netty + Modbus协议 |
| 能耗分析服务 | 查询、聚合、趋势计算 | 8083 | Spring Boot + InfluxDB驱动 |
| 告警服务 | 检测阈值并发送通知 | 8084 | Spring Boot + 钉钉机器人 |
🖼️架构图说明(模拟)
我的体会:微服务不是银弹。我们小组只有4个人,其实单体架构完全够用。但我们选择微服务是出于学习目的。真正让我体会到微服务好处的是独立部署:有一次告警服务出了bug,我们只重启了那个服务,主流程(数据采集和展示)完全不受影响。这在单体架构中是做不到的。
https://www.baidu.com/
5️⃣ DevOps + 云计算
第12单元的DevOps是整门课的“压轴戏”。我们使用阿里云Codeup自带的流水线功能,实现了从代码提交到自动部署的全流程。
CI/CD流水线配置(.codeup/pipeline.yml):
yaml
# Codeup流水线配置文件(简化版) name: 能源管理系统CI/CD stages: - name: 代码检出 type: git-checkout - name: 后端构建(Java) type: maven script: mvn clean package -DskipTests artifacts: - target/*.jar - name: 单元测试 type: maven script: mvn test # 测试报告收集 reports: junit: target/surefire-reports/*.xml - name: Docker镜像构建 type: docker-build dockerfile: Dockerfile image: registry.cn-hangzhou.aliyuncs.com/energy/energy-service:${CI_COMMIT_SHORT_SHA} - name: 部署到测试环境 type: ssh-deploy host: 47.xxx.xxx.xxx script: | docker pull registry.cn-hangzhou.aliyuncs.com/energy/energy-service:${CI_COMMIT_SHORT_SHA} docker stop energy-service || true docker rm energy-service || true docker run -d --name energy-service -p 8080:8080 registry.cn-hangzhou.aliyuncs.com/energy/energy-service:${CI_COMMIT_SHORT_SHA}我们对DevOps的理解变化:
| 阶段 | 我们的做法 | 问题 | DevOps后的做法 |
|---|---|---|---|
| 第1周 | 用U盘拷代码 | 版本混乱、覆盖丢失 | Git + Codeup统一仓库 |
| 第3周 | 手动scp上传jar包 | 忘记传配置文件 | 流水线自动打包+部署 |
| 第5周 | 部署前想起来要测试 | 经常在线上发现bug | 流水线自动执行单元测试 |
| 第7周 | 部署后手动验证 | 耗时且容易遗漏 | 部署后自动运行冒烟测试 |
我的体会:DevOps最打动我的不是技术,而是安全感。以前上线新功能就像“赌一把”,现在有了流水线,每次提交都会自动运行300多个单元测试,有什么问题5分钟之内就能发现。这种信心是之前没有过的。
🖼️系统截图模拟
图中显示近7天用电量趋势折线图,X轴为日期(5月7日-5月13日),Y轴为用电量(kW)。可以看到5月10日(周五)有一个明显的高峰(156kW),分析原因是空调集中使用。右侧显示各区域实时功率排行:生产车间(45kW)、办公楼(28kW)、食堂(12kW)。
https://image.baidu.com/search/index?tn=baiduimage&fm=result&ie=utf-8&word=%5B%E5%9B%BE3%EF%BC%9A%E8%83%BD%E8%80%97%E4%BB%AA%E8%A1%A8%E6%9D%BF%E6%88%AA%E5%9B%BE%5D%20%E5%9B%BE%E4%B8%AD%E6%98%BE%E7%A4%BA%E8%BF%917%E5%A4%A9%E7%94%A8%E7%94%B5%E9%87%8F%E8%B6%8B%E5%8A%BF%E6%8A%98%E7%BA%BF%E5%9B%BE%EF%BC%8C%E4%BB%A5%E5%8F%8A%E5%90%84%E5%8C%BA%E5%9F%9F%E7%9A%84%E5%AE%9E%E6%97%B6%E5%8A%9F%E7%8E%87%E6%8E%92%E8%A1%8C%E3%80%82
四、完整的功能实现代码示例(带详细注释)
根据用户故事“US-002 查看实时能耗”,我实现了完整的REST API + Service + 数据库查询。
4.1 Controller层
java
package com.energy.controller; import com.energy.common.Result; // 统一响应封装 import com.energy.entity.EnergyData; // 能耗实体类 import com.energy.service.EnergyService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; /** * 能耗数据接口 * 对应敏捷用户故事:US-002 作为管理员,我希望查看所有设备的实时能耗数据 */ @RestController @RequestMapping("/api/energy") @Api(tags = "能耗管理") @RequiredArgsConstructor public class EnergyController { private final EnergyService energyService; /** * 获取指定设备的最新实时能耗 * GET /api/energy/realtime/{deviceId} * * @param deviceId 设备编号,如 "DEV-METER-001" * @return 最新能耗数据,包含数值、单位、采集时间 */ @GetMapping("/realtime/{deviceId}") @ApiOperation("获取实时能耗") public Result<EnergyData> getRealtime(@PathVariable String deviceId) { // 参数校验:设备编号不能为空 if (deviceId == null || deviceId.trim().isEmpty()) { return Result.error("设备编号不能为空"); } EnergyData data = energyService.getLatest(deviceId); // 如果没有数据,返回友好的提示(而不是null或异常) if (data == null) { return Result.error("未找到设备 " + deviceId + " 的能耗数据"); } return Result.success(data); } /** * 批量获取多个设备的最新数据 * POST /api/energy/realtime/batch * * @param deviceIds 设备编号列表 * @return 设备编号 → 能耗数据的Map */ @PostMapping("/realtime/batch") @ApiOperation("批量获取实时能耗") public Result<Map<String, EnergyData>> getBatchRealtime(@RequestBody List<String> deviceIds) { if (deviceIds == null || deviceIds.isEmpty()) { return Result.error("设备编号列表不能为空"); } // 限制单次查询数量,防止恶意请求或缓存穿透 if (deviceIds.size() > 100) { return Result.error("单次查询设备数量不能超过100个"); } Map<String, EnergyData> result = energyService.getLatestBatch(deviceIds); return Result.success(result); } }4.2 单元测试(TDD实践)
java
package com.energy.controller; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.web.servlet.MockMvc; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; /** * EnergyController单元测试 * 采用TDD思想:先写测试(定义期望行为),再实现功能 */ @WebMvcTest(EnergyController.class) class EnergyControllerTest { @Autowired private MockMvc mockMvc; @MockBean private EnergyService energyService; @Test void testGetRealtime_Success() throws Exception { // Arrange:准备测试数据 EnergyData mockData = EnergyData.builder() .deviceId("DEV-METER-001") .value(120.5) .unit("kW") .timestamp(LocalDateTime.now()) .build(); // 模拟Service层行为 when(energyService.getLatest("DEV-METER-001")).thenReturn(mockData); // Act & Assert:执行请求并验证响应 mockMvc.perform(get("/api/energy/realtime/DEV-METER-001")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data.value").value(120.5)) .andExpect(jsonPath("$.data.unit").value("kW")); } @Test void testGetRealtime_DeviceNotFound() throws Exception { // 测试设备不存在的情况 when(energyService.getLatest("INVALID-DEVICE")).thenReturn(null); mockMvc.perform(get("/api/energy/realtime/INVALID-DEVICE")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(500)) .andExpect(jsonPath("$.message").value(containsString("未找到设备"))); } @Test void testGetRealtime_EmptyDeviceId() throws Exception { // 测试空设备编号的边界情况 mockMvc.perform(get("/api/energy/realtime/")) .andExpect(status().isNotFound()); // Spring MVC会对空路径返回404 } }五、课程中遇到的最大困难与解决方法
困难1:第一次做架构设计,完全不知道从哪下手
背景:第六单元讲微服务架构时,老师让我们画出自己项目的架构图。我盯着空白页看了20分钟,一个字都写不出来。
解决过程:
我先找了一个成熟的开源项目(若依微服务版),看懂它的分层
然后用“抄作业”的方式,先把结构框架画出来
再根据自己的项目替换具体内容(把“用户服务”改成“数据采集服务”等)
最后找助教review,修改了3版才定稿
反思:不会画不是能力问题,是还没见过足够多的好例子。以后遇到陌生领域,先看10个例子再动手。
困难2:团队两个人同时改一个文件,Git冲突不会解决
背景:有一次王小和李四同时修改了application.yml配置文件,两人都push了,导致远程分支冲突。
解决步骤(后来整理成文档):
bash
# 1. 拉取最新代码,发现冲突提示 git pull origin develop # 输出:CONFLICT (content): Merge conflict in application.yml # 2. 打开冲突文件,找到冲突标记 <<<<<<< HEAD port: 8080 ======= port: 8081 >>>>>>> feat/alert-service # 3. 两人沟通后决定:李四的功能需要8081,王小改成8081 # 4. 手动删除标记,保留正确内容:port: 8081 # 5. 标记为已解决并提交 git add application.yml git commit -m "resolve: 解决application.yml端口冲突,统一使用8081" git push origin develop困难3:自动化测试覆盖率一直达不到要求
问题:课程要求单元测试覆盖率≥80%,我们一开始只有52%。
解决方法:
使用JaCoCo插件生成覆盖率报告,逐行分析未覆盖代码
优先补全核心业务逻辑(Service层)的测试
Controller层的简单CRUD不做过度测试
最终覆盖率:82%(Service层92%,Controller层73%)
六、课程考核方式与我的得分
| 考核项 | 占比 | 我的得分 | 失分原因(反思) |
|---|---|---|---|
| 平时实验 + Codeup提交记录 | 30% | 95/100 | 有一次忘记提交周报 |
| 团队项目(能源管理系统) | 40% | 90/100 | 架构文档写得太简略,扣了10分 |
| 个人学习笔记 + 博客 | 20% | 92/100 | 有一篇博客排版问题 |
| 课堂互动与汇报 | 10% | 88/100 | 汇报时间超时2分钟 |
总评:86分(良好)
自我评价:知识上收获很大,分数上没有拿到优秀有点遗憾,但问题出在表达和规范上,不是能力问题。下学期选修高级软件工程继续努力。
七、总结与展望
《软件工程实务》让我从一个“只会写代码”的学生,成长为具备工程思维、团队协作、架构意识的准软件工程师。我深刻体会到:
| 如果只会... | 后果 | 学完之后我懂得了... |
|---|---|---|
| 写代码 | 项目一复杂就维护不了 | 编写单元测试 + 遵循代码规范 |
| 自己一个人开发 | 无法与他人协作 | Git分支模型 + 代码审查 |
| 跟着感觉做需求 | 做出来没人用 | 用户故事 + 验收标准 |
| 直接部署上线 | 经常出问题且回滚困难 | CI/CD流水线 + 灰度发布 |
三个“不再”:
❌ 不再写没有用户故事的功能
❌ 不再直接 push 到 main 分支
❌ 不再“我觉得没问题”就上线
三个“开始”:
✅ 开始写测试用例
✅ 开始画架构图
✅ 开始做回顾复盘
未来我会继续深入学习:
🔧 云原生技术(K8s、Service Mesh、Istio)
🔐 软件安全与隐私合规(GDPR、等级保护)
🧠 AI辅助软件工程(GitHub Copilot、AutoDev)
📊 可观测性(Metrics、Logging、Tracing三位一体)
八、致谢与资源推荐
感谢:
课程老师
队友
Codeup提供的学生免费资源
推荐资源:
| 类型 | 名称 | 简介 |
|---|---|---|
| 书籍 | 《软件工程:实践者的研究方法》 | 经典教材,案例丰富 |
| 书籍 | 《用户故事与敏捷方法》 | 把用户故事讲透了 |
| 书籍 | 《DevOps实践指南》 | 学DevOps必看 |
| 在线教程 | 阿里云开发者学堂-敏捷课程 | 免费 + 实战 |
| 工具 | Codeup | 阿里云一站式研发平台(学生优惠) |
| 社区 | 思否敏捷话题 | 国内活跃的敏捷社区 |
如还有疑惑可点击下方链接解惑:
https://www.deepseek.com/