北京航空航天大学OO_Pre课程总结暨第八次作业

前言

(在学期初选课时我绝对不会想到一学分的OO_Pre会如此硬核,以至于它和CO形成了无缝衔接,让我享受到了无比充实的大二上前半期的生活)

在八周的OO_Pre课程中,我在大一的C语言程序设计和数据结构的基础上开始学习面向对象编程的思想和方法,并通过五次的项目迭代和两次项目Debug进一步巩固了在课上学到的技能

项目架构

整个OO_Pre课程安排的作业层层递进并且较为统一(印象中只有第七次作业对之前的要求进行了更改),再加之本人在假期自学了一些Python和C++的内容,对面向对象有一定的了解,因此在作业初期进行的比较顺利。对于类以及类对应的方法的安排,我才用了朴素的逻辑安排——动作的发起者是谁,就将对应的方法放在对应的类中,例如将delBottle,addEquipment等方法置于Adventurer中,将starUp方法置于Equipment中,isEmpty方法置于Bottle中。

最初架构

在第一次作业中,受之前面向过程编程思想的影响,我选择使用在main中使用大量的switch-case语句来区分不同输入对应的不同情况

switch (opt) {
       //加入一个 ID 为 `{advId}`、名字为 `{name}` 的冒险者
       case 1:
       name = scanner.next();
       Adventurer adventurer = new Adventurer(advId, name);
       adventurers.put(advId, adventurer);
       break;
       //给 ID 为 `{advId}` 的冒险者增加一个药水瓶,药水瓶的 ID、名字、容量分别为 `{bot_id}`、`{name}`、`{capacity}`
       case 2:
       id = scanner.nextInt();
       name = scanner.next();
       capacity = scanner.nextInt();
       adventurers.get(advId).AddBottle(id, name, capacity);
       break;

但这样会带来许多问题,例如不直观(事实上,如果不依赖注释我甚至看不懂我自己的代码想干什么),不美观等。

第一次架构修改——将main中的方法分离出来

出于上述原因,在第二次作业中,我修改了这一部分的架构(其实是主要驱动力是方法60行的限制以及这种输入方式难以写Junit)

for (int i = 0; i < n; i++) {
            String[] argv = scanner.nextLine().split(" +");
            order.run(argv, adventurers);
        }

我将程序的主体部分全部放到了一个叫Order的类中,main仅仅作为程序的入口以及数据读入的入口

第二次架构修改——在Order类中增加处理日志的方法

第三次作业中加入了战斗的功能,但是我当时出于偷懒的心理,并未新建一个Log类,而是将处理日志的方法全部写在Order类里,同时忽视了方法调用过程中“中间人”的破坏性,这破坏了Order类的功能统一性,使得某些方法的代码显得臃肿不堪,为我之后的迭代造成了较大的干扰。

 public boolean LogAttack(String advName1, String advName2, String equName,
                             HashMap<Integer, Adventurer> adventurers,
                             ArrayList inBattleAdv, HashMap<String, Integer> nameToIdAdv) {
        if (inBattleAdv.contains(advName1) && inBattleAdv.contains(advName2)) {
            if (adventurers.get(nameToIdAdv.get(advName1)).getHoldEqus().containsKey(equName)) {
                adventurers.get(nameToIdAdv.get(advName2)).reduceHitPoint(adventurers.
                        get(nameToIdAdv.get(advName1)).getHoldEqus().get(equName).getStar()
                        * adventurers.get(nameToIdAdv.get(advName1)).getLevel());
                System.out.println(nameToIdAdv.get(advName2) + " " +
                        adventurers.get(nameToIdAdv.get(advName2)).getHitpoint());
                return true;
            }
        }
        System.out.println("Fight log error");
        return false;
    }

例如以上方法的功能是实现进入战斗状态的冒险家的战斗,很难想象我当时是怎么看着这一坨代码debug了几乎一晚上

第三次架构修改——增加继承与接口

在第六次作业中增加了Bottle和Equipment的几个下属分类。我使用继承完成了这个任务,并为Bottle和Equipment的下属类中覆写了use方法;我还为Adventurer,Bottle,Equipment,Food创建了共有方法的接口Commodity。

最终架构

出于完成最后以此迭代的需要,我创建了Store类,并通过私有化构造函数的方式保证Store的唯一性。
我在Store中实现了returnPrice,buy,addCommodity方法,最终的项目文件如下图所示
项目源文件
其中main仅仅作为程序入口以及输入部分,绝大部分的指令都在Order类中执行,Order类再通过调用其它类的方法,主要是Adventurer类的方法,完成对指令的处理。值得注意的是,我的日志处理部分放在Order类中,这一点有待改进。(反正作业写完了,管他呢)

Junit使用心得体会

我本人对Junit是又爱又恨,因为就但从OO_Pre的几次作业迭代来看,Junit在事实上并没用帮我找出一些难以找出的bug,它仅仅是帮我找出了一些十分低级简单的bug(而且这些bug我认为即使不依靠Junit,我单单靠“观察”也能找出),而为了顺利地写出Junit测试我则需要花费几乎和写代码相同的时间。这在我看来有点得不偿失。
但另一方面,我又能理解Junit测试在大型项目中的便利性与必要性。它能随着项目一起进行迭代的特点也的确在一定程度上避免了我对项目进行迭代之后对原本项目功能造成破坏。
以下是个人对使用Junit的经验:

  1. 注重覆盖率数据。很多时候项目出bug的地方就在其未被Junit测试覆盖到的地方;
  2. 注意边缘数据和浮点数。因为OO_Pre对各个变量的范围要求各有不同,在加之在程序运行过程中可能会发生的各种潜在类型转移和浮点数截断等情况,在编写Junit测试样例需要特别注重边缘数据和浮点数。
  3. 适当佐以AI工具。我发现在编写测试样例的时候借助Github Copilot可以实现简单数据的测试,也可以在注释中写出需要测试的数据,让AI工具实现对测试相关数据代码的编写,例如我只需要写这些注释
//        11
//        1 123 advName
//        7 123 1 foodName 11 10000
//        4 123 2 equName 10 20000 RegularEquipment
//        2 123 3 botName 10 30000 RegularBottle
//        2 123 11 botName2 10 55000 RegularBottle
//        10 123 11
//        10 123 3
//        22 123
//        23 123 5 bot RegularBottle
//        23 123 6 bot RegularBottle
//        23 123 7 bot RegularBottle

AI就可以帮我补出如下内容

		adventurers = new HashMap<>();
        inBattleAdv = new ArrayList<>();
        nameToIdAdv = new HashMap<>();
        argv = new String[]{"1", "123", "advName"};
        order.run(argv, adventurers, nameToIdAdv);
        argv = new String[]{"7", "123", "1", "foodName", "11", "10000"};
        order.run(argv, adventurers, nameToIdAdv);
        argv = new String[]{"4", "123", "2", "equName", "10", "20000", "RegularEquipment"};
        order.run(argv, adventurers, nameToIdAdv);
        argv = new String[]{"2", "123", "3", "botName", "10", "30000", "RegularBottle"};
        order.run(argv, adventurers, nameToIdAdv);
        argv = new String[]{"2", "123", "11", "botName2", "10", "55000", "RegularBottle"};
        order.run(argv, adventurers, nameToIdAdv);
        argv = new String[]{"10", "123", "11"};
        order.run(argv, adventurers, nameToIdAdv);
        argv = new String[]{"10", "123", "3"};
        order.run(argv, adventurers, nameToIdAdv);
        argv = new String[]{"22", "123"};
        order.run(argv, adventurers, nameToIdAdv);
        argv = new String[]{"23", "123", "5", "bot", "RegularBottle"};
        order.run(argv, adventurers, nameToIdAdv);
        argv = new String[]{"23", "123", "6", "bot", "RegularBottle"};
        order.run(argv, adventurers, nameToIdAdv);
        argv = new String[]{"23", "123", "7", "bot", "RegularBottle"};
        order.run(argv, adventurers, nameToIdAdv);

而我需要做的只是在每个order.run()之后加assert断言语句,大大节省了调整输入格式所花费的时间,提高了编写Junit测试的效率。

学习OO_Pre的心得体会

  1. 模块化编程。我从面向过程转移到面向结果编程的最大的体会就是模块化编程。之前在学习C语言程序设计和数据结构的过程中,我并未有意识地进行模块化编程,只有在需要进行大量的代码复用的时候才会将需要复用的部分单独脱离出来写成函数;而如今在进行面向对象编程的过程中,Java语言的特性迫使我使用模块化编程的思想编写我的项目。在这个过程中,我逐渐感受到了模块化编程在我编写项目的过程中带来的便利:高度模块化的程序大大提升了我的代码可读性,便于对项目进行debug和迭代,使得我不再需要像之前那样依靠大量的文字注释实现对代码的理解;
  2. 面向过程与面向对象的区别:
    面向过程编程如我们之前学习的C语言,很明显在处理大型的,需要维护和迭代的项目时效果不如面向对象编程。但是面向过程编程强调代码的短小精悍,程序运行的效率较高 ;
    而面向对象编程高度模块化和结构化,对对象行为的描述符合人类的思维,利于代码的重用和对项目进行维护和迭代。但也存在代码冗长,时空花销较大
    的缺点;
    对于OO_Pre课程的作业自然是选择面向对象编程效果更佳,但若是我们可以轻易梳理出逻辑运行过程的简单问题如“求前n个自然数的全排列数”,则显然是面向过程编程效果更好;
  3. 代码复用。在学习OO_Pre的过程中,我开始可以对需要实现的功能进行进一步细分,并将多个需求中所需要的相同的功能单独脱离出来写成一个方法。这样便于我进行代码复用,节省了码代码所需要的时间,但也用过被复用的代码出现了debug导致多个功能一同出bug的情况。所以在进行代码复用时一定要仔细思考,不能一味照搬,尽量避免连锁反应;
  4. 版本管理。在第二次迭代中因为出现了bug,所以我对部分代码进行了大刀阔斧的修改,但修改之后却发现一些原本没有出bug的数据点出现了bug。这时我想起了git的版本管理功能,将修改前的代码调出并进行对比,快速定位到了bug的所在地并对其进行了修复。适时进行版本管理可以在很大程度上避免代码,方便我们对各个版本的代码进行对比;

对OO_Pre课程的建议

  1. 修改强测给分规则。我认为强测扣分应该与修改bug的个数相关而不是与通过的强测点数相关。当前的强测给分规则与强测数据内容的关联较大。假如有两个同学,其中一个同学的项目的bug较多,但是恰好强测点中较少的点覆盖到了这些bug,另一个同学的项目bug较少,但是恰好强测点中较多的点覆盖到了这些bug。这可能会导致bug较少的同学甚至可能在强测中得到更少的分数,我认为这是不合理的。
  2. 加强强测数据点的覆盖。我本人在第二次迭代中忽视了Food存在同名现象,没有考虑在存在多个同名Food的情况下使用Food时使用ID最小的那一个,而是将它和Equipment的处理方式相同。然而这个明显的bug完美地活过了P3,P4,P5,P6并最终在P7击中了我,导致我喜提强测20分。虽然这个问题主要是我本人对项目的考虑不周造成的,但这样一个明显的bug居然可以活过四次强测显然是不合理的。因此我认为强测数据点的覆盖有待加强。

总结

总体而言,我对这半个学期的OO_Pre课程评价很高——学地十分充实,也确确实实有了许多收获。这里特别感谢OO_Pre课程的老师和助教团队,他们态度友善,乐于助人,能够在较短的时间内解决同学们学习上的困惑,使得我能够始终以一个较为轻松的心态(除了看到我P7强测20分的时候)完成OO_Pre课程。我也十分期待在下个学期正式进行OO的学习,掌握新的知识和技能,迎接新的困难和挑战。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值