架构设计及迭代
1. 最终设计架构
-
输入处理:在程序一开始就读入所有指令,存储在队列中。然后对输入进行处理,对于除了type=14的指令,每次出队一条指令,根据type获得需要的参数,然后调用HandleInstr的相应方法;对于type=14,需要出队m+1条指令,然后调用HandleInstr的相应方法。
-
业务处理:核心代码编写在一个final类HandleInstr中,用一个HashMap<Integer, Adventurer>来管理执行过程中的所有冒险者,根据需要处理的23种指令编写了23个
public static void
属性的方法,每个方法对应完成一种操作的数据处理和输出(但是输出和数据处理放在一起似乎不利用用junit进行单元测试)。 -
核心逻辑:作业的核心逻辑围绕冒险者展开,对于冒险者需要管理的信息有:拥有的物品(在背包中的和不在背包中的)、与自己有关战斗日志、雇佣关系、money。因此,我使用了
BottleOperator、EquipmentOperator、FoodOperator、Bag
这四个类来管理拥有的物品,冒险者买卖装备的行为我设计了一个单例模式的Shop
类,存储购买信息,产生物品(感觉也像工厂);对于战斗日志建立一个BattleLog
类存储战斗日志的相关信息;对于雇佣关系,我采用了观察者模式(Observer Pattern),但是Observer和Observed接口都由Adventurer实现了,感觉不像真正的观察者模式。
最后处理数据和输出部分的代码架构如下图:
2. 迭代过程
第一次迭代
第一次的需求很简单,但是考虑到后面的迭代和开发流程,我决定将输入输出分开,因此设计了HandleInstr类来存储所有冒险者和进行执行指令,对每个冒险者采用HashMap来管理拥有的Bottle和Equipment。
第二次迭代
第二次新增了食物这一冒险者可以拥有的物品,并且对于每个物品可以选择放到背包,冒险者可以使用背包物品。这一迭代中,经过几次删删改改,我最终决定让冒险者拥有Bag类来管理放在背包中的物品,但是之前冒险者删除物品的操作在背包的也要同时进行,在后续的迭代中也多次出现冒险者对物品操作与背包高度关联,因代码在某些位置没有同时进行修改或不需要同时修改时不小心改变了背包而发生问题。
为了让HandleInstr类的功能更简单,我设计了ReadInstr类来讲输入的指令(一个字符串)根据type
解析出HandleInstr中需要的参数并且调用对应方法,HandleInstr中只需要根据得到的参数执行指令即可。
第三次迭代
第三次新增了战斗日志的功能,对于Adventurer类没有新增的功能,只需要存储跟自己有关的战斗日志。我设计了两个新的类,BattleLog类用来存储一条战斗日志的相关信息(时间、参与的冒险者者、类型等),然后重写了它的toString方法以简化本次新增功能所要求的输出逻辑,另外一个类是BattleLogManage,是一个final
修饰的类,用于根据指令产生战斗日志、管理所有的战斗日志。
在这次迭代中,由于进入战斗模式后还会根据参数进行输入,为了让输入跟处理分开,我采用队列在程序开始前存储所有的输入,这样只需要根据进入战斗模式这条指令的参数选择需要出队的指令数即可。
第四次迭代
在第四次迭代为Adventurer、Bottle、Equipment、Food赋予价值体这一概念,同时为Bottle、Equipment增加了子类,最复杂的是在计算一个冒险者拥有的价值时引入了雇佣这一概念。虽然课上介绍对于Commodity适合使用接口,但我不能理解接口要如何简化代码,似乎还是要写price这些属性,于是就让Adventurer、Bottle、Equipment、Food都继承了Commodity( × \times ×)。对于雇佣关系,根据价值计算方法,可以抽象成一个有向无环图,记 p ( i ) p(i) p(i)为冒险者 i i i拥有价值体的价值总和, f ( i ) f(i) f(i)为冒险者 i i i除去雇佣关系带来的价值(即Bottle、Food、Equipment带来的价值之和),那么 p ( i ) = f ( i ) + ∑ j ∈ i . e m p l o y e e s p ( j ) p(i) = f(i) + \sum_{j\in i.employees} p(j) p(i)=f(i)+∑j∈i.employeesp(j),显然这是一个 dfs \text{dfs} dfs问题,加上记忆化时间复杂度可以做到 O ( n ) O(n) O(n),其中 n n n为冒险者个数。
在第四次迭代中,Adventurer类的属性已经非常多了,需要具体处理的功能也很混乱,因此我设计了BottleOperator、FoodOperator、EquipmentOperator来管理Bottle、Food、Equipment,但这也导致了这些类和Bag类管理了部分相同的对象,设计删除物品逻辑很复杂,需要同时考虑多个类。
第五次迭代
第五次迭代似乎时考察上课介绍的设计模式,可能会用到的有工厂模式、观察者模式、单例模式。对于冒险者由于雇佣关系带来的援助行为,我使用观察者模式处理,但是可能时因为对观察者模式理解很浅也没有看过真正在项目的应用,因此我在Adventurer类同时实现了Observer和Observed接口,感觉怪怪的。然后对于冒险者与商店之间进行的买卖行为,显然商店要用单例模式,我感觉商店的卖出操作可能也可以用一个工厂模式,但是我没太搞懂工厂模式中接口怎么用就没写。
在23号指令出售背包的所有物品中,因为背包和冒险者对物品进行相关操作方法耦合程度太高,我出现了bug,大概就是一边遍历一个容器一遍用容器本身的remove方法删除,之后的bug修复环节特别麻烦。
junit使用体会
- 使用junit进行单元测试可以方便地检查底层类的正确性。在迭代中bottle、equipment类的一些属性需要通过计算来影响冒险者的属性,通过命令输入来检测干扰因素多,而且需要通过调试逐层进入到Adventurer类才能看到相应变化,使用junit进行测试可以排除这些问题。
- 迭代作业的输入处理比较麻烦,使用junit进行一些简单的测试可以不需要自己构造输入,在写代码时也可以先完成业务处理功能,每完成一部分处理功能就用junit进行测试,最后再完成输入处理。
- 要尽量将输出打印功能放在外层,不要出现在junit测试的代码中,或者将输出内容同时作为方法的返回值,这样进行测试可以使用相应的assert方法,比较方便。
- 对于某些情况复杂的情况感觉使用junit不太方便,而且单例模式、final类的静态属性出现在junit的时候会造成麻烦。
课程体会
学习面向对象让我对于处理复杂逻辑,具有多个对象的系统应该如何管理有了一定认识。可以通过继承、多态来减少代码量,使得不同对象间具有逻辑关系;封装的思想可以只将代码需要处理的任务最表层暴露出来进行调用,省去了内部复杂的关系。面向对象通过抽象来化简需求,让复杂的问题得以分层次解决。
对课程介绍的接口和设计模式没有真正理解,每次用起设计模式和接口(感觉设计模式就是面向接口编程)都感觉很奇怪。
课程建议
在最初git的相关事项希望可以给出更详细的使用说明或者视频教程(包括如何配置ssh,上传代码、管理版本等)。
尽量在指导书中将本次迭代的需求和一些特殊情况解释清楚,比如在新增战斗日志功能的homework4中对于自己攻击自己的情况和最后一次迭代homework7中没有在指导书说明使用过的瓶子卖给商店怎么记录capacity。