📢完整的题目要求和源代码请按下面指示前往我的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中,抽象可以通过接口和抽象类来实现。通过抽象,我们可以隐藏具体的实现细节,只展示用户或者对象需要的功能。
从面向过程到面向对象
面向过程是面向对象的基础,面向对象可以说是面向过程的抽象。比如汽车有开车,加减速和刹车,关于汽车的操作有好多,每一个都需要一个具体的过程来实现,把这些过程抽象的总结起来就可以形成一个类,这个类包括的汽车所有的东西,所有的操作。
面向过程是具体化的,流程化的。解决一个问题,需要一步一步分析需要怎样,然后需要怎样,一步一步实现的。面向对象是模型化的,抽象出一个类,这是一个封闭的环境,在这个环境中有数据有解决问题的方法,你如果需要什么功能直接使用就可以了,至于是怎么实现的,你不用知道。
从代码层面来看,面向对象和面向过程的主要区别就是数据是单独存储(结构体)还是与操作存储在一起(类)。