270
社区成员
发帖
与我相关
我的任务
分享作为面向对象先导课程的结课作业,本次作业围绕 “冒险者系统” 展开,涵盖指令解析、雇佣关系管理、物品交互、战斗系统等核心功能。在迭代开发过程中,不仅完成了功能实现,更深化了对面向对象思想的理解。以下从架构设计、JUnit 测试心得、OOP 思维过渡体会及课程建议四个维度展开总结。
本次作业的核心是构建一个可扩展、低耦合的冒险者管理系统,最终架构按 “职责划分” 分为核心实体层、工具支撑层、交互逻辑层三层,各模块职责单一且依赖清晰。
(1)核心实体层:封装业务核心对象
该层包含所有业务相关的实体类,通过类的继承与接口实现统一行为、区分差异,核心类关系如下:
AdventureObject接口;Bottle(药水)、Equipment(装备)、Spell(法术)三大抽象类,均实现Usable(可使用)或Carryable(可携带)接口,子类如HpBottle、Sword、AttackSpell各自实现具体的use()逻辑(多态应用);AdventureObject(统一对象 ID / 类型描述)、Usable(统一物品使用行为)、Carryable(统一物品携带属性),确保不同实体的行为一致性。(2)工具支撑层:解耦通用功能
该层包含不依赖业务逻辑的通用工具类,降低核心模块的耦合度:
createBottle()、createEquipment()),避免在Adventurer或Main中重复 new 对象,符合 “单一职责原则”;lr指令的参数解析,将字符串拆分为标识符、括号、逗号等 Token,为递归下降解析提供支撑;(3)交互逻辑层:处理指令与规则约束
该层通过Main的指令分发与Adventurer的方法实现,封装业务规则:
Main通过switch-case分发aa(创建冒险者)、ar(建立雇佣)、lr(批量导入雇佣关系)等指令,调用对应模块逻辑;Adventurer内置雇佣关系约束(isBoss()判断上级)、物品使用约束(checkUseConstraints())、战斗约束(checkFightConstraints()),确保业务规则不扩散到其他类。本次作业并非一步到位,而是经历了 3 次关键架构调整,核心思考围绕 “降低耦合、提升可维护性” 展开:
(1)第一次调整:从 “硬编码创建” 到 “工厂模式”
Adventurer的addBottle()、addEquipment()中用new HpBottle()、new Sword()硬编码,若新增物品类型(如ManaBottle),需修改Adventurer类,违反 “开闭原则”;Factory类,统一负责物品创建,Adventurer只需调用Factory.createBottle(),无需关心具体实现;Factory,符合 OOP“对扩展开放、对修改关闭” 的原则。(2)第二次调整:从 “扁平雇佣关系” 到 “树形结构 + 递归解析”
ar(添加单个雇佣)、rr(删除单个雇佣)指令,雇佣关系存储为 “直接上级 - 直接下级” 的扁平结构,无法处理lr指令的批量树形关系(如8kkY(vekV,x));Adventurer中新增getAllSubordinates()方法,通过广度优先遍历获取所有间接下级,支撑树形关系管理;Lexer类与parseAdventurer()递归解析方法,将lr指令的字符串参数解析为树形雇佣关系,动态调用addEmployee()建立关联;(3)第三次调整:从 “指令内联约束检查” 到 “统一约束方法”
use、fight指令的处理逻辑中,直接内联雇佣关系检查(如if (target == adventurer.getEmployer())),导致相同的约束逻辑在多个地方重复,且难以维护;Adventurer中封装checkUseConstraints()、checkFightConstraints()方法,集中处理约束检查,Main或useItem()仅需调用该方法即可;在作业开发中,通过 JUnit4 编写测试用例(覆盖Adventurer、Bottle、Factory等核心类),深刻体会到 “测试驱动开发” 对代码质量的保障作用,核心心得如下:
本次作业中,多个隐藏 bug 是通过 JUnit 测试发现的,最典型的是雇佣关系判断逻辑错误:
isBoss()方法错误地判断 “目标是否将当前冒险者视为上级”(target.isBoss(this)),而非 “当前冒险者是否将目标视为上级”(this.isBoss(target));AdventurerTest.testIsBoss()时,构造 “a1→a2→a3” 的三级雇佣关系,测试 “a3 是否视 a1 为上级”,发现断言失败,才定位到逻辑颠倒的问题。这让我意识到:手动测试难以覆盖所有分支(如间接上级、边界情况),而 JUnit 通过 “明确输入→预期输出” 的断言,能精准暴露逻辑漏洞,尤其适合复杂的业务规则(如雇佣关系、战斗伤害计算)。
最初编写测试时,仅关注 “行覆盖率”,但发现即使行覆盖率达到 80%,仍有大量分支未覆盖(如背包满时的物品挤出逻辑、死亡时的雇佣关系清理)。后续调整测试策略,重点关注 “分支覆盖”:
takeBottle()时,构造 “背包已存 10 个药水” 的场景,验证 “新药水加入时,最早的药水被挤出”;useItem()时,构造 “使用者死亡”“目标死亡”“物品不在背包” 等场景,验证指令是否按预期失败;lr指令解析时,构造嵌套结构(如King(Knight(Archer),Rogue)),验证雇佣关系是否正确建立。最终分支覆盖率从 27.4% 提升至 65%,代码的健壮性显著提升 —— 后续集成测试时,因分支未覆盖导致的 bug 减少了 70%。
编写测试的过程,也是对代码设计的 “反向校验”。例如:
Adventurer的cleanupEmploymentOnDeath()是私有方法,无法直接测试 “死亡时是否解除雇佣关系”;getEmployer()、getEmployees()方法,暴露必要的属性(非直接暴露字段,而是通过方法封装);这让我明白:好的代码设计必然是 “可测试的”,而编写测试的过程会倒逼我们优化类的职责与接口设计。
作为从 “面向过程编程”(如 C 语言)过渡到 “面向对象编程”(Java)的核心课程,OOPre 不仅教会了语法,更重塑了我的编程思维,核心体会集中在三个方面:
面向过程编程时,解决 “冒险者使用药水” 的问题,会按 “步骤” 思考:
而面向对象编程时,会按 “对象职责” 思考:
Adventurer(冒险者):负责管理自身状态(血量、背包),提供useItem()方法封装 “使用物品” 的逻辑;HpBottle(药水):负责实现 “使用时恢复血量” 的具体逻辑(use()方法);Main(入口):仅负责分发指令,不关心 “如何使用药水”。这种转变的核心是 “职责封装”—— 每个对象只做自己擅长的事,代码结构更清晰,后续修改 “药水效果” 时,只需改HpBottle,无需改动Adventurer或Main。
作业初期,处理 “不同物品的使用逻辑” 时,曾用面向过程的思维写过这样的代码:
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)方法;HpBottle、AttackSpell)实现use()方法,封装自身逻辑;Adventurer的useItem()只需调用usable.use(this, target),无需判断物品类型。这让我体会到:多态是 OOP 的 “灵魂” 之一—— 它将 “做什么” 与 “怎么做” 分离,既实现了代码复用,又让扩展变得简单。
课程中学习的 “单一职责原则”“开闭原则”“依赖倒置原则”,最初觉得是 “理论空话”,但在作业迭代中逐渐理解其价值:
Lexer的解析逻辑写在Main中,后续修改lr指令格式时,需改动Main的大量代码;DefBottle时,需修改Factory外的多个类;这些原则的本质是 “为长期维护服务”—— 好的 OOP 代码不仅能完成当前功能,更能在需求变化时(如新增指令、新增物品),以最小的修改成本适配,这也是面向对象优于面向过程的核心优势。