OOPre 课程作业总结:从架构迭代到 OOP 思维的蜕变

王新宇-24231208 2025-11-17 16:15:11

作为面向对象先导课程的结课作业,本次作业围绕 “冒险者系统” 展开,涵盖指令解析、雇佣关系管理、物品交互、战斗系统等核心功能。在迭代开发过程中,不仅完成了功能实现,更深化了对面向对象思想的理解。以下从架构设计、JUnit 测试心得、OOP 思维过渡体会及课程建议四个维度展开总结。

一、作业最终架构设计与迭代调整

本次作业的核心是构建一个可扩展、低耦合的冒险者管理系统,最终架构按 “职责划分” 分为核心实体层工具支撑层交互逻辑层三层,各模块职责单一且依赖清晰。

1. 最终架构设计(分层模块)

(1)核心实体层:封装业务核心对象

该层包含所有业务相关的实体类,通过类的继承与接口实现统一行为、区分差异,核心类关系如下:

  • Adventurer(冒险者):系统核心对象,封装属性(血量、攻击力、金钱等)与行为(战斗、使用物品、雇佣关系管理),实现AdventureObject接口;
  • 物品类:按功能分为Bottle(药水)、Equipment(装备)、Spell(法术)三大抽象类,均实现Usable(可使用)或Carryable(可携带)接口,子类如HpBottleSwordAttackSpell各自实现具体的use()逻辑(多态应用);
  • 接口定义AdventureObject(统一对象 ID / 类型描述)、Usable(统一物品使用行为)、Carryable(统一物品携带属性),确保不同实体的行为一致性。

(2)工具支撑层:解耦通用功能

该层包含不依赖业务逻辑的通用工具类,降低核心模块的耦合度:

  • Factory(工厂类):统一创建物品对象(如createBottle()createEquipment()),避免在AdventurerMain中重复 new 对象,符合 “单一职责原则”;
  • Lexer(词法分析器):专门处理lr指令的参数解析,将字符串拆分为标识符、括号、逗号等 Token,为递归下降解析提供支撑;
  • Main(入口类):负责指令读取与分发,调用各模块逻辑处理指令,不包含具体业务逻辑,仅承担 “调度者” 角色。

(3)交互逻辑层:处理指令与规则约束

该层通过Main的指令分发与Adventurer的方法实现,封装业务规则:

  • 指令处理Main通过switch-case分发aa(创建冒险者)、ar(建立雇佣)、lr(批量导入雇佣关系)等指令,调用对应模块逻辑;
  • 约束检查Adventurer内置雇佣关系约束(isBoss()判断上级)、物品使用约束(checkUseConstraints())、战斗约束(checkFightConstraints()),确保业务规则不扩散到其他类。

2. 迭代中的架构调整与思考

本次作业并非一步到位,而是经历了 3 次关键架构调整,核心思考围绕 “降低耦合、提升可维护性” 展开:

(1)第一次调整:从 “硬编码创建” 到 “工厂模式”

  • 初始问题:最初创建药水、装备时,直接在AdventureraddBottle()addEquipment()中用new HpBottle()new Sword()硬编码,若新增物品类型(如ManaBottle),需修改Adventurer类,违反 “开闭原则”;
  • 调整方案:引入Factory类,统一负责物品创建,Adventurer只需调用Factory.createBottle(),无需关心具体实现;
  • 思考:工厂模式让 “对象创建” 与 “业务逻辑” 解耦,后续新增物品类型时,仅需修改Factory,符合 OOP“对扩展开放、对修改关闭” 的原则。

(2)第二次调整:从 “扁平雇佣关系” 到 “树形结构 + 递归解析”

  • 初始问题:最初仅支持ar(添加单个雇佣)、rr(删除单个雇佣)指令,雇佣关系存储为 “直接上级 - 直接下级” 的扁平结构,无法处理lr指令的批量树形关系(如8kkY(vekV,x));
  • 调整方案
    1. Adventurer中新增getAllSubordinates()方法,通过广度优先遍历获取所有间接下级,支撑树形关系管理;
    2. 新增Lexer类与parseAdventurer()递归解析方法,将lr指令的字符串参数解析为树形雇佣关系,动态调用addEmployee()建立关联;
  • 思考:递归下降解析是处理 “嵌套结构”(如树形关系)的高效方式,通过 “分而治之” 将复杂解析拆分为 “解析单个冒险者”“解析冒险者序列” 等子问题,代码逻辑更清晰。

(3)第三次调整:从 “指令内联约束检查” 到 “统一约束方法”

  • 初始问题:最初在usefight指令的处理逻辑中,直接内联雇佣关系检查(如if (target == adventurer.getEmployer())),导致相同的约束逻辑在多个地方重复,且难以维护;
  • 调整方案:在Adventurer中封装checkUseConstraints()checkFightConstraints()方法,集中处理约束检查,MainuseItem()仅需调用该方法即可;
  • 思考:“抽取共性逻辑” 是 OOP 的核心思想之一,通过统一方法封装约束规则,不仅减少重复代码,更让后续规则修改(如新增 “不能攻击盟友” 约束)只需改一处。

二、JUnit 测试心得体会

在作业开发中,通过 JUnit4 编写测试用例(覆盖AdventurerBottleFactory等核心类),深刻体会到 “测试驱动开发” 对代码质量的保障作用,核心心得如下:

1. 测试的核心价值:提前暴露 “隐形 bug”

本次作业中,多个隐藏 bug 是通过 JUnit 测试发现的,最典型的是雇佣关系判断逻辑错误

  • 最初isBoss()方法错误地判断 “目标是否将当前冒险者视为上级”(target.isBoss(this)),而非 “当前冒险者是否将目标视为上级”(this.isBoss(target));
  • 编写AdventurerTest.testIsBoss()时,构造 “a1→a2→a3” 的三级雇佣关系,测试 “a3 是否视 a1 为上级”,发现断言失败,才定位到逻辑颠倒的问题。

这让我意识到:手动测试难以覆盖所有分支(如间接上级、边界情况),而 JUnit 通过 “明确输入→预期输出” 的断言,能精准暴露逻辑漏洞,尤其适合复杂的业务规则(如雇佣关系、战斗伤害计算)。

2. 测试用例设计:“覆盖分支” 比 “覆盖行数” 更重要

最初编写测试时,仅关注 “行覆盖率”,但发现即使行覆盖率达到 80%,仍有大量分支未覆盖(如背包满时的物品挤出逻辑、死亡时的雇佣关系清理)。后续调整测试策略,重点关注 “分支覆盖”:

  • 边界情况测试:如测试takeBottle()时,构造 “背包已存 10 个药水” 的场景,验证 “新药水加入时,最早的药水被挤出”;
  • 异常场景测试:如测试useItem()时,构造 “使用者死亡”“目标死亡”“物品不在背包” 等场景,验证指令是否按预期失败;
  • 递归逻辑测试:如测试lr指令解析时,构造嵌套结构(如King(Knight(Archer),Rogue)),验证雇佣关系是否正确建立。

最终分支覆盖率从 27.4% 提升至 65%,代码的健壮性显著提升 —— 后续集成测试时,因分支未覆盖导致的 bug 减少了 70%。

3. 测试的 “副作用”:倒逼代码设计更合理

编写测试的过程,也是对代码设计的 “反向校验”。例如:

  • 最初AdventurercleanupEmploymentOnDeath()是私有方法,无法直接测试 “死亡时是否解除雇佣关系”;
  • 为了测试该逻辑,不得不重构代码:新增getEmployer()getEmployees()方法,暴露必要的属性(非直接暴露字段,而是通过方法封装);
  • 最终结果:代码的 “可测试性” 提升,同时也符合 “封装原则”(对外暴露方法而非字段)。

这让我明白:好的代码设计必然是 “可测试的”,而编写测试的过程会倒逼我们优化类的职责与接口设计

三、学习 OOPre 的心得体会:从 “面向过程” 到 “面向对象” 的思维跃迁

作为从 “面向过程编程”(如 C 语言)过渡到 “面向对象编程”(Java)的核心课程,OOPre 不仅教会了语法,更重塑了我的编程思维,核心体会集中在三个方面:

1. 从 “关注步骤” 到 “关注对象”:拆分问题的视角转变

面向过程编程时,解决 “冒险者使用药水” 的问题,会按 “步骤” 思考:

  1. 检查冒险者是否存活;
  2. 检查药水是否在背包;
  3. 计算使用药水后的血量;
  4. 更新冒险者血量并删除药水。

而面向对象编程时,会按 “对象职责” 思考:

  • Adventurer(冒险者):负责管理自身状态(血量、背包),提供useItem()方法封装 “使用物品” 的逻辑;
  • HpBottle(药水):负责实现 “使用时恢复血量” 的具体逻辑(use()方法);
  • Main(入口):仅负责分发指令,不关心 “如何使用药水”。

这种转变的核心是 “职责封装”—— 每个对象只做自己擅长的事,代码结构更清晰,后续修改 “药水效果” 时,只需改HpBottle,无需改动AdventurerMain

2. 继承与多态:告别 “if-else 地狱”,实现代码复用

作业初期,处理 “不同物品的使用逻辑” 时,曾用面向过程的思维写过这样的代码:

java

// 反面例子:面向过程的if-else判断
if (itemType.equals("HpBottle")) {
    target.setHitPoint(target.getHitPoint() + effect);
} else if (itemType.equals("AtkBottle")) {
    target.setBaseAtk(target.getBaseAtk() + effect);
} else if (itemType.equals("AttackSpell")) {
    target.setHitPoint(target.getHitPoint() - power);
}

这种代码的问题是:新增物品类型时,需不断加if-else,代码臃肿且易出错。

学习多态后,重构为:

  • 定义Usable接口,声明use(Adventurer user, Adventurer target)方法;
  • 每个物品类(HpBottleAttackSpell)实现use()方法,封装自身逻辑;
  • AdventureruseItem()只需调用usable.use(this, target),无需判断物品类型。

这让我体会到:多态是 OOP 的 “灵魂” 之一—— 它将 “做什么” 与 “怎么做” 分离,既实现了代码复用,又让扩展变得简单。

3. 设计原则:代码的 “长期主义”

课程中学习的 “单一职责原则”“开闭原则”“依赖倒置原则”,最初觉得是 “理论空话”,但在作业迭代中逐渐理解其价值:

  • 若不遵循 “单一职责原则”,将Lexer的解析逻辑写在Main中,后续修改lr指令格式时,需改动Main的大量代码;
  • 若不遵循 “开闭原则”,新增DefBottle时,需修改Factory外的多个类;

这些原则的本质是 “为长期维护服务”—— 好的 OOP 代码不仅能完成当前功能,更能在需求变化时(如新增指令、新增物品),以最小的修改成本适配,这也是面向对象优于面向过程的核心优势。

四、对 OOPre 课程的建议

  1. 建议增加 “小型课程设计” 环节:当前作业以 “指令驱动” 为主,建议中期增加一次 “组队课程设计”(如开发一个简易 RPG 游戏),让学生在更贴近实际的场景中应用 OOP 思想(如角色、怪物、道具的交互),深化对 “类协作”“设计模式” 的理解;
  2. 建议补充 “调试与测试” 实践指导:课程中对 JUnit 的讲解偏基础,建议增加 “如何定位 OOP 代码中的 bug”“如何设计高覆盖度测试用例” 的案例教学(如结合作业中的 “雇佣关系逻辑错误”“递归解析异常” 等场景),帮助学生提升工程能力。
...全文
35 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

270

社区成员

发帖
与我相关
我的任务
社区描述
2026年北航面向对象设计与构造
java 高校
社区管理员
  • 孙琦航
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧