最终架构
The art of programming is the art of organizing complexity.
- Edsger Dijkstra
最终架构概览图

主要的类
输入解析与控制系统
GameProcess 类
管理指令信息列表的迭代, 读取以及调用正确的 Command 执行命令.
Command 类
抽象类 , 下有各个指令的具体类实现, 分别负责在 GameProcess 里, 调用 Gulid 相关方法, 执行命令操作.
(具体类数量过多, 概览图中未给出, 具体结构如下)

Lexer & Parser 类
负责解析 lr(load relations) 命令的输入.
下辖 Boukensya 类, 负责存储解析的关系树.
创建与管理系统
Gulid 类
所谓冒险家公会, 负责记录并管理流程中添加的所有 Adventurer, 并且接受 Command 类的指令, 通过相应方法, 判断异常, 适时调用 Adventurer 类的相应方法, 执行具体操作.
Shop 类
负责响应 bi 指令, 操纵冒险者 money 改变, 并且通过内部的 ItemFactory 实例, 制造物品添加给对应的冒险者.
工厂类
此处是应用了设计模式中的 工厂模式 , 有三个对应的工厂, 负责 Equipment, Spell, Bottle 类实例的制造, 并且集中到 ItemFactory 类, 成为暴露给外界的接口, 负责所有物品的创建.
最一开始, 逻辑尚简单的时候, 物品的创建全部由 Gulid 类对应方法内完成, 但是随着物品种类与效果之丰富, 这种方法会导致 Gulid 类过大, 细节过多, 因此我跟随课程组的建议, 使用工厂类封装了物品实例创建逻辑的细节, 成功有效管理了复杂性.
物品与效果系统
接口
使用接口, 一方面可以抽象出类的 共通的逻辑 , 让我们考虑实现功能必须的方法, 另一方面, 也可以使得不同实例存储在同一个接口变量名下, 调用接口的方法, 从而不必类类特判, 而是复用同一段代码(DRY).
本次项目, 我共为物品设计了两个接口, 分别对应物品的两个特性: Usable(可使用性), Carriable(可携带性)
-
Usable接口:由
Spell和Bottle类实现, 因为这两者是响应use命令的, 可被冒险者使用的物品, 可以抽象出他们的使用条件与效果, 提供使用接口, 并且通过一个方法找到对应的物品, 用同一段逻辑使用这两种物品. -
Carriable接口:由
Bottle和Equipment类实现, 因为在某次迭代后, 这两者都可以存放于背包中携带, 即响应ti,ri, 需要一个同一类型名来存储于同一容器, 虽然在后续迭代中, 携带的Equipment被我单独存储了, 但是这样做也可以复用携带与移除物品的代码, 使得两个物品的携带, 在外界调用同一接口, 而因此使得外界不必关注携带物品的种类, 无需判断后调用Equipment或者Bottle的携带方法, 从而使得Adventures类的细节没有暴露给Gulid类.
物品类
在物品的设计中, 我考虑到了 组合优于继承, 通过组合的方法实现了三种物品的功能, 尤其是 Bottle 和 Spell, 两者不必创建任何子类, 复用了同样的 Effect 子类, 有效防止了类爆炸, 并且留出了更大的扩展空间, 如果真的有增加效果的需要, 不必修改 Bottle 和 Spell 的代码, 而是增加新的 Effect 类, 符合 对修改封闭, 对扩展开放 的 开闭原则 .
Bottle 类 & Spell 类
负责存储 id, power, manaCost 等数值特征字段, 以及一个 Effect 实体, 负责记录药水效果, 并且提供对冒险者状态产生影响的方法.
Equipment 类
抽象类 , 下有:
Armour类Weapon抽象类Sword类Magicbook类
以上的武器类, 分别用于区分Armour 和 Weapon 不同的效果, 以及 Weapon 不同的攻击效果实现.
角色系统
Bag 类
继承自 LinkedHashMap 的类, Override removeEldestEntry 方法, 从而避免重复制造轮子, 把管理背包容量的逻辑托管给了 Java 的官方实现.
Adventurer 类
核心类, 通过组合实现功能.
- 有
id,atk,def,mana,hitPoint,money字段, 负责识别冒险者, 记录冒险者状态 - 使用 观察者模式, 有
boss,subordinate字段, 记录上下级关系, 并且有求助和援助的相应方法接口. - has-a
Bag, has-manyBottle,Equipment,Spell. 通过不同的方法管理物品的获取, 装备, 使用等, 接口暴露给Gulid, 而避免了暴露内部细节. - 可以根据当前的
atk创建伤害性的Effect实例, 通过此实例对被攻击者造成血量改变, 从而托管了伤害功能, 实现了代码复用.
迭代思路
In most projects, the first system built is barely usable…Hence plan to throw one away; you will, anyhow.
— Frederick P. Brooks, Jr., The Mythical Man-Month
第一次迭代: 原型
创立 Adventurer, Bottle, Equipment 类, 与管理冒险者的 Gulid 类, 成为接下来迭代的原型, 为接下来的框架打好基础.
第二次迭代: 接口与继承
增加了 Bottle 的种类, 增加了 Spell 类以及下属的子类, 并且区分了物品的性质: 可携带的 Bottle 和 Equipment 和 可使用的 Bottle 和 Spell. 因此我创立了对应的两个接口.
由于此时系统尚不复杂, 我直接对 Bottle 和 Spell 通过继承实现了多态, 但是造成了许多重复小类: 类数量激增, 而其中内容极少, 造成了项目结构无端复杂, 为我日后的重构埋下了伏笔.
第三次迭代: 装备与战斗
现在, Equipment 也区分了很多种类, 不同类的创建细节不同, 全部放在 Gulid 中导致代码失去重点, 复杂性过高, 因此我在课程的教导下, 通过工厂方法创建了 ItemFactory 类.
同时, 随着物品种类增加, 物品类的类数量过多, 但很多是没几行代码的类, 实际的复用性堪忧, 且项目结构过于复杂. 我决心把 Bottle 与 Spell 的效果大一统, 因此我创建了 Effect 类, 把 Bottle 和 Spell 的不同效果通过与不同 Effect 实例组合实现. 这种大一统带来了意外惊喜, 我 fight 指令的伤害效果, 也可以藉由 Effect 类实例实现, 从而使得代码逻辑更加一致, 复用性也得到了提升.
第四次迭代: 雇佣关系
此次迭代是通过实践观察者模式实现, 得益于我上次迭代的重构, 此次我只需要为 Adventurer 类增加上下级的记录, 设计几个递归寻找上下级同盟的辅助方法, 即可直接在 use 前根据物品的 Effect 实例内容, 判断增减益效果, 决定能否使用; 在 fight 相关方法直接增加一个上下级判断, 以及攻击后求援方法的调用, 仅仅几处修改与扩展.
第五次迭代: 雇佣关系批量导入
本次主要是完善 Lexer, 实现 Parser, 实践递归下降法, 至于雇佣可以直接复用上次迭代的方法, 工作量不大.
使用 JUnit 的心得体会
Program testing can be used to show the presence of bugs, but never to show their absence!
- Edsger Dijkstra
代码的稳健性与可靠性要依靠测试背书, 尤其是现实工作中并不存在一个知晓项目一切细节的助教为我们编写评测样例.
此次 OOPre 课程中, 使用 JUnit 进行测试, 为我提供了丰富的测试编写经历与宝贵经验.
我利用课程安排, 尝试了TDD, 效果甚好, 我写完方法签名后, 立即编写测试, 这带来的好处是巨大的:
- 通过编写测试样例, 可以帮助我阅读要求, 明确设计目的, 防止开发大方向跑偏.
- 通过编写测试, 我能感受到自己的方法是不是易于测试的, 并在此期间不断调整方法的签名, 更改参数与返回值.
这步实际就是规划暴露的接口, 当我实现一个易于测试的方法时, 这个方法也往往封闭内部具体细节, 但又恰当的暴露或返回合适的信息给外界, 从而可以更好的封装并且被外界调用. - 通过编写我自己认为覆盖完整的样例, 我重构之后只需要全部跑一遍通过, 我便基本不必关心重构的部分和其余部分的配合关系, 可以相信我的代码功能还是正常的.
- 通过从底层的方法测试到顶层, 可以在开发中更好地管理复杂度, 循序渐进, 一步步打好地基, 使得我们每次只要专注开发的层级的抽象, 而不必关心底层已经测试通过的功能, 节约我们的精力.
但是还需要额外注意, 测试可以帮助我们找到 bug, 但是尽管通过我们的测试, 也不能确保其完美无缺, 只能臻于完善. 这也是我认为测试难点之所在, 如何写出覆盖面更广, 更具有代表性的, 真正有效的好测试, 是我依旧需要钻研的课题.
学习 OOPre 的心得体会
OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things.
- Alan Kay
此次 OOPre 课程的学习, 让我窥探了面向对象编程的强大抽象能力与复杂度管理能力.
我在此次课程中, 感受到了面向对象的三大特征: 继承, 封装, 多态 , 以及这些特性如何为大型项目开发而服务的.
我面向过程编程时, 关注于把任务分解, 一步步想着要计算机做什么事, 教给计算机如何执行.
这过程中, 任务的全过程都要在脑中考虑, 全部细节都暴露在眼前, 小的算法题尚可, 但是巨大的项目就难以开发维护了, 人的精力是有限的, 关注的东西是有极限的.
因此, 面向对象的优势就体现了.
世界上的东西是有分类的, 我们天生就会为事物分类, 这其实就是一种提取共性, 创造抽象的过程. 面向对象为这种过程提供了非常便捷的语法支持.
通过面向对象, 我们可以站在项目的角度思考, 思考各个功能涉及的组件, 分门别类, 逐个实现, 并且在此过程中也可以借助继承和组合来复用代码. 从而, 等到我们实现了一个个类, 完成了一部分的具体功能, 我们可以不再关注内部, 而是调用暴露的接口, 使得一个个类成为 “黑箱” . 这使得我们每时每刻要考虑的内容变少, 使得我们项目组织更有结构, 而不是把不同功能的代码杂糅在一起.
所以, 面向对象的真正意义得以显现: 不是表面的三大特征, 这只是手段, 而真正的目的是 组织数据存在形式 , 实则是一种 信息交流 , 类与类之间保存着各自必须的信息, 通过一个个接口相互交流合作, 从而完成功能. 我们依靠面向对象, 使得信息的组织与封闭管理实现, 使得我们关注最少信息, 而不必同时手动掌控一切 这也是和面向过程编程最大的不同, 后者将信息集中管理, 控制一个个函数读取信息, 产生新的信息, 继续由我们在程序执行过程中关注并管理.
The determined Real Programmer can write FORTRAN programs in any language.
- Post, Ed (July 1983). Real Programmers Don’t Use Pascal
最后, 面向过程到面向对象的思路转变也绝不是一个简单的 C 到 Java 开发语言的改变. 要真正学会面向对象, 我还需要结合语言的特性, 学习更多的设计模式, 提升自己组织程序结构与管理复杂度的能力, 而不是用着面向对象的语言继续面向过程. 在此意义上, OOPre 为我进一步学习指明了方向.