2023秋BUAA-OOpre·总结笔记

OOpre课程的简单建议

经过这学期的学习,我收获颇丰,这门课程的开设于笔者而言很有意义

笔者的建议是:

加大课程训练力度,可以考虑 包括但不限于 加强深入迭代、增加hack互测与博客作业形式、增加研讨课和实验课、引入上机比赛等等,达到全面模拟并提前熟悉OO正课节奏的效果

伟大的领袖毛泽东曾说:“不打无准备之仗,方能立于不败之地。”
《礼记·中庸》中记载:“凡事预则立,不预则废。”
生活中,人们也常说:“机会都是留给有准备的人的。

由此可见,任何事情成功的前提便是做充分的准备,对于这门重要的OO课程也是如此。

下面是笔者的学习心得和过程总结,还请不吝赐教。

代码架构分析

最后一次作业代码架构

笔者用从IntelliJ IDEA导出的一张类图以供参考。
最后一次作业类图
总体而言,这份代码基本能够一次性通过历次强测 ,也符合checkstyle检查规范。但仍然存在诸多不足。这里浅显地分析两点。

  • 第一是类的设计问题Adventure类的Adventure这个单词似乎没有很好地表达“冒险者”的意思。上一次作业原本想改,但是不想再大动干戈,避免出现其他奇怪的错误。还有PrintInfo这个类,原本是用来处理输出信息的,但是后来的迭代没再用到,也算是一个败笔。
  • 第二个是封装性问题。对于很多的新增特性,比如背包,笔者在Adventure类中新增了三个属性来处理,但是应该封装成一个Package类更加妥当。战斗日志也可以单独封装,但笔者在Main类中直接对其进行处理。可以看到这样导致每个类分工不明确

Main类重构历程

笔者对代码的重构主要集中在Main类上。

第四次作业前

第二次作业时,笔者的Main类结构如下:

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测试。因此从第三次作业开始,笔者尝试通过重构来解决这些问题。下面是第三次作业的结构:

//addAdventure等函数的具体实现不再列出
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类结构如下:

//只展示架构核心内容
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));
        }
    }

下面是笔者具体的修改过程。

  1. 确保每个case只调用一个函数解决对应业务逻辑。

  2. 修改每个上述函数(后称业务函数)的参数表,只允许有一个参数,尽量不要有返回值

这是为了配合使用Consumer函数式接口,使得每个业务函数都可以看作一个对象(像是C语言中的函数指针),能够给Consumer“赋值”(说法可能不严谨,体会一下意思)。

  1. 构建一个HashMap成员,将Consumer作为HashMap的值,将选项作为HashMap的键

例如:

HashMap<Integer, Consumer<ArrayList<String>>> actionsMap = new HashMap<>();

在这里我们通过1个Integer类型的键值来获取1个Consumer类型的值,这个Consumer类型的值就是我们的业务函数。
Consumer指定的类型就是唯一参数的声明类型

  1. 实际调用时,通过HashMapget方法获取对应的Consumer,再调用Consumeraccept方法

例如:

actionsMap.get(1).accept(args);

这里等价于case为1时,调用相应的函数。
这里args是传给业务函数的实际参数。accept方法是Consumer提供的方法,用来接收参数并调用业务函数。

  1. 现在可以去掉switch语句块了

除了Consumer函数式接口,还有其他很多函数式接口,感兴趣可以自行了解。
这种方法称为表驱动法,可以有效减少代码量,提高可读性。

Jnuit使用心得

注解类型

只说说笔者用到的吧。

  • @Test:这个注释说明依附在 JUnit 的 public void 方法可以作为一个测试案例。
  • @Before:有些测试在运行前需要创造几个相似的对象。在 public void 方法加该注释是因为该方法需要在 test 方法前运行。
  • @After:如果你将外部资源在 Before 方法中分配,那么你需要在测试运行后释放他们。在 public void 方法加该注释是因为该方法需要在 test 方法后运行。

执行过程

before() 方法针对每一个测试用例执行,但是是在执行测试用例之前。
after() 方法针对每一个测试用例执行,但是是在执行测试用例之后。
before() 方法和 after() 方法之间,执行每一个测试用例

测试用例

单元测试用例是一部分代码,可以确保另一端代码(方法)按预期工作。

好的测试用例有一下几个特点:

  1. 已知输入和预期输出,即在测试执行前就已知。
  2. 已知输入需要测试的先决条件,预期输出需要测试后置条件。

单元测试对测试用例有以下几个要求:

每一项需求至少需要两个单元测试用例:一个正检验,一个负检验。
如果一个需求有子需求,每一个子需求必须至少有正检验和负检验两个测试用例。

OOpre学习体会

面向对象编程

面向对象编程Object-Oriented Programming,OOP)是一种编程范式,它使用“对象”来设计软件。对象是包含数据(也被称为属性)和操作这些数据的方法的实体。面向对象编程的主要目标是提高软件的可重用性、灵活性和可维护性。

OOP有四大基本原则:封装、继承、多态和抽象

封装

封装是将对象的状态(属性)和行为(方法)包装在一起的过程。这使得对象的内部实现对外部是隐藏的,只有通过对象的公开接口才能访问对象的状态和行为。这样可以减少代码间的耦合度,提高代码的可维护性。

继承

继承是一种创建新类的方式,新创建的类继承了一个已有类的属性和方法。这样,我们可以创建一种层次结构,从而实现代码的复用和扩展。

多态

多态是指同一操作作用于不同的对象,可以有不同的解释和行为。多态可以增加代码的灵活性和可扩展性。

抽象

抽象是将复杂系统模型化的一种方法。在OOP中,抽象可以通过接口和抽象类来实现。通过抽象,我们可以隐藏具体的实现细节,只展示用户或者对象需要的功能。

从面向过程到面向对象

面向过程是面向对象的基础,面向对象可以说是面向过程的抽象。比如汽车有开车,加减速和刹车,关于汽车的操作有好多,每一个都需要一个具体的过程来实现,把这些过程抽象的总结起来就可以形成一个类,这个类包括的汽车所有的东西,所有的操作。

面向过程是具体化的,流程化的。解决一个问题,需要一步一步分析需要怎样,然后需要怎样,一步一步实现的。面向对象是模型化的,抽象出一个类,这是一个封闭的环境,在这个环境中有数据有解决问题的方法,你如果需要什么功能直接使用就可以了,至于是怎么实现的,你不用知道。

从代码层面来看,面向对象和面向过程的主要区别就是数据是单独存储(结构体)还是与操作存储在一起(类)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值