BUAA OOpre After Story
写在最前
OOpre本身的体验是很好的,写代码的过程也很有趣,在这个学期的课程中体验可以算是靠前的,不过仍然有一些我认为可以改进的地方
总体来说,OOpre是不错的java入门课程和相对复杂程序设计的入门课程
架构设计
抛开非迭代的两次作业不谈,作业的宗旨是开发一个模拟性的程序,根据课程顺序主要包括以下知识点
- Git和现代IDE的使用(不知道为什么很多同学都习惯使用devcpp这种东西)
- java基础:变量,运算,循环,函数和基础IO
- JUnit单元测试
- 容器
- 正则表达式
- 面向对象:继承
- 接口和多态
- 设计模式
我的代码架构为:
./src
├── Main.java
├── adventurer
│ ├── Adventurer.java
│ └── Backpack.java
├── bottle
│ ├── Bottle.java
│ ├── RecoverBottle.java
│ ├── RegularBottle.java
│ └── ReinforcedBottle.java
├── commodity
│ ├── Commodity.java
│ └── Store.java
├── equipment
│ ├── CritEquipment.java
│ ├── EpicEquipment.java
│ ├── Equipment.java
│ └── RegularEquipment.java
├── food
│ └── Food.java
└── log
├── AoeLog.java
├── AttackLog.java
├── BattleLog.java
├── LogItem.java
└── PotionLog.java
7 directories, 19 files
代码架构图:
显然,这个架构是合理的但是是不够完美的。
完美的架构
更合理的继承关系
Bottle、Equipment和Food三个类中有大量类似的方法和属性,例如
removeBottle(int id);
removeEquipment(int id);
removeFood(int id);
这样的方法大可以通过让这三个类同时继承一个抽象类Item,并提供方法
removeItem(int id);
来统一到同一个接口,这样的话,例如Main中处理指令的方法:
public static void carryCommand(Adventurer adv, int carryId, int commandType) {
switch (commandType) {
case 9:
adv.tryCarryEquipment(carryId);
break;
case 10:
adv.tryCarryBottle(carryId);
break;
case 11:
adv.tryCarryFood(carryId);
break;
default:
break;
}
}
完全可以通过调用同一个方法来解决
public static void carryCommand(Adventurer adv, int carryId) {
adv.tryCarryItem(carryId);
}
一开始没有这样做是出于防止同一个id被赋予不同的物品的考虑,由于在Adventurer中三种物品被(id, Item)的键值对保存于HashMap,id产生冲突会很麻烦,不过似乎id并不会冲突
事实上,通过在抽象类中添加一些方法,我们可以很容易的将所有的Item以(hash, Item)的键值对保存于同一个hashmap,同时做到对id,name的快速查找,例如提供一个方法
getHash() {
return name + String.valueOf(id);
}
在Main外部管理输入输出和指令解析
应该不少同学都是这样做的,我在Main里完成输入输出管理和解析纯粹是一开始顺手写了后面懒得重构
替代switch,更好看的指令调用
到了开发中期,我们会面临指令解析的类中一定有一个函数会带有很长的switch语句,导致checkstyle很难控制在100行以内,事实上,有一个简明且可读性佳的方法可以解决这个问题——闭包(lambda表达式)
对于除了1以外的每一个指令,我们可以定义一个函数
command_i(Adventurer adv, String[] command);
来处理这个指令,而通常,我们会采取对指令编号switch的方法来调用每个指令的执行方法,例如
switch(commandType) {
case 2: case 4: case 6:
...
break;
...
}
这样的结构事实上可以通过一个形如
interface Command {
void executeCommand(Adventurer adv, String[] command);
}
private static HashMap<Integer, Command> commandHashMap = new HashMap<>();
的HashMap来替代,在将处理指令的函数通过lambda表达式存入HashMap后,上文的switch语句可以被以下语句简单替代:
commandHashMap.get(commandType).executeCommand(adv, command);
具体细节有兴趣的同学可以自行学习java的lambda表达式
一些经验
在我的架构中,有一些部分我认为是值得讨论一下的,在此做一点简单的介绍
抽象类
在我的架构中,战斗记录的处理依赖于对LogItem子类的解析,LogItem本身是一个抽象类(只能被继承,不能被实例化的类),在LogItem中定义了存储log表达式,存储相关的解析正则表达式和获取log的方法,解析的过程完全置放在LogItem类的相关方法中,存储log的逻辑置放在静态工具类BattleLog中。
事实上,还有更清晰的方法:定义一个接口Logable,其中定义Log必须要实现的方法(存储表达式,存储正则,解析表达式,返回log),LogItem类继承Logable,再作为抽象基类
删除
当删除Adventurer的东西时,需要同时删除本身和backpack中的引用
如果这个过程全部都手动操作容器,很容易造成遗漏,这一点在最后一次作业中最容易发生问题
应该在Adventurer中实现remove方法,同时调用backpack中的remove方法,在任何删除的情况下都直接调用方法,而不要手动操作容器
JUnit Test
- 课程组本意是好的,本身单元测试也是重要的
- 但是由于缺乏自动生成的方法,加上写单元测试也是很花时间的过程,实际上同学们肯定都是随便写完成任务要求的
- 由于在标准流进行io,在OOpre课程中单元测试发现问题的概率远不如搭建自动评测机来的容易
- 希望不要要求Main类中的单元测试覆盖率,意义很小很敷衍
希望OOpre未来能找到更好的使用单元测试的方法,而不是单纯要求数据达标
心得
其实在做OOpre之前自己就组织过一些上千行的项目,所以整个开发还算是很顺利,而且很享受
用Cpp做大型项目非常有助于强调架构设计的重要性和锻炼同学的心态
如果要我说的话,Java作为编程语言远远算不上是好用的,作为面向对象的入门语言也不一定是最好的,仅仅能算作是最常用的语言,在面向对象上我认为C#是更适合学习的语言,目前对OOpre课程中Java语法产生的意见有:
- jdk版本不够高不支持自动类型推断(很不方便很不现代)
- java不支持默认参数(我不理解)
- java没有set,get,没有像swift和C#一样方便的结构体
- 用.equals()判断字符串相等太过反人类
OOpre课程的迭代顺序整体来说是不错的,不过或许有一部分应该改进:
- 第六次开发学习继承和接口,然而这个时候大部分代码都定型了,最后几乎一定会导致对以前代码的重构,这一次作业的设计我认为是不合理的
- 我认为合理的顺序是
- git的使用和java语法入门
- 对象基础,属性和方法
- 继承
- 容器
- 接口和多态
建议
- 稍微调整迭代顺序,把继承放到更早的位置,有助于同学进行架构设计
- 升级jdk版本,希望下个学年的同学们能用上自动类型推断
- 能不能不用csdn…………………………………..