📢完整的题目要求和源代码请按下面指示前往我的Github仓库~
BUAA-2023-OOpre
OOpre课程的简单建议
经过这学期的学习,我收获颇丰,这门课程的开设于笔者而言很有意义。
笔者的建议是:
**加大课程训练力度,可以考虑 包括但不限于 加强深入迭代、增加hack互测与博客作业形式、增加研讨课和实验课、引入上机比赛等等,达到全面模拟并提前熟悉OO正课节奏的效果**
伟大的领袖毛泽东曾说:“不打无准备之仗,方能立于不败之地。”
《礼记·中庸》中记载:“凡事预则立,不预则废。”
生活中,人们也常说:“机会都是留给有准备的人的。”
由此可见,任何事情成功的前提便是做充分的准备,对于这门重要的OO课程也是如此。笔者衷心希望课程组能考虑这条建议。
下面是笔者的学习心得和过程总结,还请不吝赐教。
代码架构分析
最后一次作业代码架构
笔者用从IntelliJ IDEA导出的一张类图以供参考。

总体而言,这份代码基本能够一次性通过历次强测 ,也符合checkstyle检查规范。但仍然存在诸多不足。这里浅显地分析两点。
- 第一是类的设计问题。Adventure类的Adventure这个单词似乎没有很好地表达“冒险者”的意思。上一次作业原本想改,但是不想再大动干戈,避免出现其他奇怪的错误。还有PrintInfo这个类,原本是用来处理输出信息的,但是后来的迭代没再用到,也算是一个败笔。
- 第二个是封装性问题。对于很多的新增特性,比如背包,笔者在Adventure类中新增了三个属性来处理,但是应该封装成一个Package类更加妥当。战斗日志也可以单独封装,但笔者在Main类中直接对其进行处理。可以看到这样导致每个类分工不明确。
Main类重构历程
笔者对代码的重构主要集中在Main类上。
第四次作业前
第二次作业时,笔者的Main类结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| public class Main { public static void main(String[] args) { int n; int capacity; int star; int advId; int botId; int equipId; String name; int type; HashMap<Integer, Adventure> adventures = new HashMap<>(); Scanner scanner = new Scanner(System.in); n = scanner.nextInt(); for (int i = 0; i < n; i++) { type = scanner.nextInt(); switch (type) { case 1: advId = scanner.nextInt(); name = scanner.next(); adventures.put(advId, new Adventure(advId, name)); break; case 2: advId = scanner.nextInt(); botId = scanner.nextInt(); name = scanner.next(); capacity = scanner.nextInt(); adventures.get(advId).addBottle(new Bottle(botId, name, capacity)); break; case 3: advId = scanner.nextInt(); botId = scanner.nextInt(); adventures.get(advId).removeBottle(botId).printIntFirst(); break; case 4: advId = scanner.nextInt(); equipId = scanner.nextInt(); name = scanner.next(); star = scanner.nextInt(); adventures.get(advId).addEquipment(new Equipment(equipId, name, star)); break; case 5: advId = scanner.nextInt(); equipId = scanner.nextInt(); adventures.get(advId).removeEquipment(equipId).printIntFirst(); break; case 6: advId = scanner.nextInt(); equipId = scanner.nextInt(); adventures.get(advId).increaseStar(equipId).printStringFirst(); break; default: break; } } } }
|
有两个很突出的问题。方法行数很容易超过限制;没有统一的输入解析,不便进行Junit测试。因此从第三次作业开始,笔者尝试通过重构来解决这些问题。下面是第三次作业的结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| public static ArrayList<String> getOrders(String line) { String[] strings = line.trim().split(" "); return new ArrayList<>(Arrays.asList(strings)); }
public static void makeChoice(HashMap<Integer, Adventure> advs, ArrayList<String> orders) { int type = Integer.parseInt(orders.remove(0)); switch (type) { case 1: addAdventure(advs, orders); break; case 2: addBottle(advs, orders); break; case 3: removeBottle(advs, orders); break; case 4: addEquipment(advs, orders); break; case 5: removeEquipment(advs, orders); break; case 6: increaseStar(advs, orders); break; case 7: addFood(advs, orders); break; case 8: removeFood(advs, orders); break; case 9: takeEquipment(advs, orders); break; case 10: takeBottle(advs, orders); break; case 11: takeFood(advs, orders); break; case 12: useBottle(advs, orders); break; case 13: eatFood(advs, orders); break; default: break; } }
public static void main(String[] args) { int n; HashMap<Integer, Adventure> advs = new HashMap<>(); Scanner scanner = new Scanner(System.in); n = Integer.parseInt(scanner.nextLine().trim()); for (int i = 0; i < n; i++) { String nextLine = scanner.nextLine(); makeChoice(advs, getOrders(nextLine)); } } }
|
这里对输入进行了集中处理,也把每个操作封装成一个函数来调用。算是比较妥善地解决了上述的两个问题 。
但是随着迭代的进行很快又出现了第三个问题:
switch-case语句中即使每个case只有三行,也会显得太过臃肿。方法限制为60行,最多只能有20个case语句。
这时笔者进行了最大的一次改动。
第四次作业后
第四次作业Main类结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| public static void addAction() { actionsMap.put(1, Main::addAdventure); actionsMap.put(2, Main::addBottle); actionsMap.put(3, Main::removeBottle); actionsMap.put(4, Main::addEquipment); actionsMap.put(5, Main::removeEquipment); actionsMap.put(6, Main::increaseStar); actionsMap.put(7, Main::addFood); actionsMap.put(8, Main::removeFood); actionsMap.put(9, Main::takeEquipment); actionsMap.put(10, Main::takeBottle); actionsMap.put(11, Main::takeFood); actionsMap.put(12, Main::useBottle); actionsMap.put(13, Main::eatFood); actionsMap.put(14, Main::enterFightMode); actionsMap.put(15, Main::searchByDate); actionsMap.put(16, Main::searchByAttacker); actionsMap.put(17, Main::searchByAttacked); } public static ArrayList<String> getOrders(String line) { String[] strings = line.trim().split(" "); return new ArrayList<>(Arrays.asList(strings)); }
public static void makeChoice(ArrayList<String> orders) { int type = Integer.parseInt(orders.remove(0)); actionsMap.get(type).accept(orders); }
public static void main(String[] args) { addAction(); int n; n = Integer.parseInt(scanner.nextLine().trim()); for (int i = 0; i < n; i++) { String nextLine = scanner.nextLine(); makeChoice(getOrders(nextLine)); } }
|
下面是笔者具体的修改过程。
确保每个case
只调用一个函数解决对应业务逻辑。
修改每个上述函数(后称业务函数)的参数表,只允许有一个参数,尽量不要有返回值。
这是为了配合使用Consumer
函数式接口,使得每个业务函数都可以看作一个对象(像是C语言中的函数指针),能够给Consumer
“赋值”(说法可能不严谨,体会一下意思)。
- 构建一个
HashMap
成员,将Consumer
作为HashMap
的值,将选项作为HashMap
的键。
例如:
1
| HashMap<Integer, Consumer<ArrayList<String>>> actionsMap = new HashMap<>();
|
在这里我们通过1个Integer
类型的键值来获取1个Consumer
类型的值,这个Consumer
类型的值就是我们的业务函数。
给Consumer
指定的类型就是唯一参数的声明类型。
- 实际调用时,通过
HashMap
的get
方法获取对应的Consumer
,再调用Consumer
的accept
方法。
例如:
1
| actionsMap.get(1).accept(args);
|
这里等价于case
为1时,调用相应的函数。
这里args
是传给业务函数的实际参数。accept
方法是Consumer
提供的方法,用来接收参数并调用业务函数。
- 现在可以去掉
switch
语句块了。
除了Consumer
函数式接口,还有其他很多函数式接口,感兴趣可以自行了解。
这种方法称为表驱动法,可以有效减少代码量,提高可读性。
Jnuit使用心得
注解类型
只说说笔者用到的吧。
- @Test:这个注释说明依附在 JUnit 的 public void 方法可以作为一个测试案例。
- @Before:有些测试在运行前需要创造几个相似的对象。在 public void 方法加该注释是因为该方法需要在 test 方法前运行。
- @After:如果你将外部资源在 Before 方法中分配,那么你需要在测试运行后释放他们。在 public void 方法加该注释是因为该方法需要在 test 方法后运行。
执行过程
before()
方法针对每一个测试用例执行,但是是在执行测试用例之前。
after()
方法针对每一个测试用例执行,但是是在执行测试用例之后。
在 before()
方法和 after()
方法之间,执行每一个测试用例。
测试用例
单元测试用例是一部分代码,可以确保另一端代码(方法)按预期工作。
好的测试用例有一下几个特点:
- 已知输入和预期输出,即在测试执行前就已知。
- 已知输入需要测试的先决条件,预期输出需要测试后置条件。
单元测试对测试用例有以下几个要求:
每一项需求至少需要两个单元测试用例:一个正检验,一个负检验。
如果一个需求有子需求,每一个子需求必须至少有正检验和负检验两个测试用例。
OOpre学习体会
面向对象编程
面向对象编程(Object-Oriented Programming,OOP)是一种编程范式,它使用“对象”来设计软件。对象是包含数据(也被称为属性)和操作这些数据的方法的实体。面向对象编程的主要目标是提高软件的可重用性、灵活性和可维护性。
OOP有四大基本原则:封装、继承、多态和抽象。
封装
封装是将对象的状态(属性)和行为(方法)包装在一起的过程。这使得对象的内部实现对外部是隐藏的,只有通过对象的公开接口才能访问对象的状态和行为。这样可以减少代码间的耦合度,提高代码的可维护性。
继承
继承是一种创建新类的方式,新创建的类继承了一个已有类的属性和方法。这样,我们可以创建一种层次结构,从而实现代码的复用和扩展。
多态
多态是指同一操作作用于不同的对象,可以有不同的解释和行为。多态可以增加代码的灵活性和可扩展性。
抽象
抽象是将复杂系统模型化的一种方法。在OOP中,抽象可以通过接口和抽象类来实现。通过抽象,我们可以隐藏具体的实现细节,只展示用户或者对象需要的功能。
从面向过程到面向对象
面向过程是面向对象的基础,面向对象可以说是面向过程的抽象。比如汽车有开车,加减速和刹车,关于汽车的操作有好多,每一个都需要一个具体的过程来实现,把这些过程抽象的总结起来就可以形成一个类,这个类包括的汽车所有的东西,所有的操作。
面向过程是具体化的,流程化的。解决一个问题,需要一步一步分析需要怎样,然后需要怎样,一步一步实现的。面向对象是模型化的,抽象出一个类,这是一个封闭的环境,在这个环境中有数据有解决问题的方法,你如果需要什么功能直接使用就可以了,至于是怎么实现的,你不用知道。
从代码层面来看,面向对象和面向过程的主要区别就是数据是单独存储(结构体)还是与操作存储在一起(类)。