面向对象第二单元总结

一、设计策略

第一次作业

第一次作业中是可捎带的单电梯,整体难度不大。我分成了五个类,分别是管理电梯线程和输入线程的MainClass类、负责获得输入的Input类、负责控制电梯运行及调度的Elevator类、作为托盘的Controller类和装请求的Person类。

由于这次是单电梯,在多线程的协同中需要考虑的比较少,涉及到线程同步的地方主要是Controller类中的请求队列,一方面输入线程在获取到输入之后要将请求装入请求队列,另一方面电梯要从请求队列中获取主请求和要捎带的请求。因此我把Controller类中添加请求和获得请求的方法写为同步方法,将请求的获取和请求的处理以及捎带分别交给Input类和Elevator类去完成,Controller类只负责存储请求以及保证线程安全。此外,为防止电梯在获取请求时暴力轮询消耗CPU资源,我在电梯取请求时判断如果没有请求就wait(),如果输入线程新增请求就notifyAll()。为使电梯线程能正常结束,我在控制器里设置了标记,在电梯获取请求时判断,如果队列中没有请求并且标记被设置,则结束电梯线程。

第二次作业

第二次作业是多部相同的可稍带电梯,我在第一次作业的基础上加了MainController类分配请求,然后修改了对于单部电梯的调度算法,其他地方几乎没有改变。

对于多部电梯而言,在多线程的协同方面不仅要考虑每个子控制器中请求队列的数据共享,还要考虑多部电梯之间请求数据的处理。为了尽量减少多线程带来的风险,我没有在主控制器中设置线程,而是在输入线程获得请求之后就立即把这个请求分配出去,由于主控制器中拥有子控制器,所以分配的过程和第一次作业基本一样,只需要在主控制器中确定分配给哪个电梯,然后像第一次作业那样分配就可以,由于是立即分配,因而不会影响到下次的输入。但是这次作业出现了致命的bug,在后面具体分析

第三次作业

第三次作业增加了新增电梯的请求,并且每种电梯的可装载人数和可到达的楼层都不同。在设计时,我只多写了一个类对输出进行了同步封装,其他类的功能都没变,只修改了具体细节。

​在多线程的协同方面,由于这次和上次作业相比只是电梯本身的变化和输入请求的变化,因此只在拆分请求后控制数据冒险时涉及到了不同线程的协同,其余都和上次作业一样。对于电梯本身的变化,可以将变化放在Elevator类内部处理,对于处理新增电梯的请求,可以放在MainController类中在处理。与上次作业不同的是,由于需要换乘,在将请求拆分之后如何保证前一半请求先执行,后一半请求后执行是这次作业主要面临的问题。我采用的策略是先将拆分后的前一半请求分配出去,将后一半请求存在主控制器中,每次电梯把人送到之后就去访问这个容器,并根据当前情况找到最优的方案分配出去。

二、架构设计分析

​这一单元的三次作业用了相同的架构,第二、三次作业分别在上次作业的基础上增加功能,并且没有大的改动。第二次作业是在第一次作业的基础上加了主控制器,将第一次作业的控制器变为子控制器,主调度器只需要关心如何把请求分配给子调度器,而不需要关心完成请求的过程。此外,由于第一次作业采用的是扩展的ALS策略,在强测中性能表现并不是很好,我就在第二次作业中将扩展的ALS策略改成了贪心策略。第三次作业则是在第二次作业的基础上对电梯内部的功能进行修改,以及在主控制器中添加了处理新增电梯请求的功能和请求拆分并分配的功能。

​对于第三次作业来说,我将电梯运行及电梯容量控制等放到了电梯类,将每个子控制器和电梯作为一个基于第一次作业的整体 ,主控制器主要负责处理及分配请求以及存储拆分后的第二段请求,输入类只负责获取输入和传递输入结束信号。扩展时,将要扩展的功能分类,如果和电梯运行有关就放到电梯类,比如有第四种类型的电梯。如果是新增请求类型就把他的处理放到主控制器类中,比如新增维修电梯,需要在主控制器里解析该请求并通过子控制器停止电梯运行。

​总的来说,我这一单元三次作业的架构设计还算比较合理,第一次作业的架构完全可以用在三次作业中,并且在功能扩展时也只改动了相应的类,相比第一单元而言,终于体会到了迭代开发。在架构的设计时,我时时刻刻想着自己只干自己的事,千万不要去影响其他类。当功能分配合理时,扩展功能时只需要改动这个类中的代码,而不需要改动其他类,比较方便且稳妥。

​基于SOLID原则的分析

  • SRP:每个类或者方法都有明确的职责。我在设计架构时充分考虑了各个类的职责,分工比较明确。在方法中也将功能细分,防止一个方法中出现过于庞大的逻辑。
  • OCP:无需修改已有实现,通过扩展增加新功能。我认为我做的不是很好,比如在第三次作业进行电梯分类时,我需要修改已有代码以实现不同的电梯。
  • LSP:本单元我没有使用继承,所以不好体现。
  • ISP:本单元我没有使用接口,所以不好体现。
  • DIP:上层依赖于抽象,抽象不依赖于细节。我认为我做的不是很好,在分析请求时写了很多十分面向过程的逻辑。

三、代码度量与UML图

第一次作业



可以看出主要是电梯运行的方法复杂度较高,我认为这里设计的不是很好,写的十分面向过程。

第二次作业



由于第二次我更换了电梯的调度算法,将原来的电梯选择如何运行改成了控制器选择如何运行,所以在控制器中复杂度较高,但是性能比原来有所提高。

第三次作业



第三次作业复杂度较高的地方除了控制器选择如何运行之外,还有是主控制器对电梯的拆分。由于我在拆分时进行了逻辑比较复杂的优化,导致和拆分请求有关的方法复杂度都比较高。

四、bug分析

​第一次作业和第三次作业中互测和强测均没有发现bug,也没有找到别人的bug。

​第二次作业中出现了非常严重的bug。在第二次作业中,除了输入线程之外,由于涉及到多个电梯和电梯个数的获取,需要用一个循环分别创建并开始电梯线程。但是我的写法是在主类中先开始了输入线程,再开始电梯线程,这样的话,由于我在获取请求之后立即分配,导致当获取请求时如果电梯线程还没被创建,这个请求就会丢失!这个错误导致我在强测中只过了两个点,损失惨重。解决方案是将输入线程放在电梯线程之后,先创建电梯线程,再创建输入线程。这次的问题使我吸取了教训,在多线程编程时,应该充分考虑在任意两条语句之间插入时间片之后,这两条语句还能否正确执行,并且不影响其他功能。错误代码如下:

Thread thread = new Thread(input);
thread.start();
for (int i = 0;i < num;i++) {
    Controller controller = new Controller();
    Elevator elevator = new Elevator(controller,(char)(65 + i));
    maincontroller.addController(controller);
    Thread thread1 = new Thread(elevator);
    thread1.start();
}

在找自己bug的时候,我采用了定时投入和自动检测的方法。在输入时,我写了输入程序,根据输入数据的时间戳定时投放输入,又写了测试程序,根据输入和我的程序的输出,从请求是否正确送达、请求是否遗漏、电梯运行是否符合逻辑和人进出电梯是否符合逻辑四个方面展开测试。

在找别人bug的时候,我将其他人的代码进行适当修改以适应我的测试程序,在第一次作业中找到了别人的bug,这个bug是在电梯执行完所有请求时结束输入,电梯会停不下来,但是评测机没法定时结束输入,我没有提交数据。第三次作业中没有发现别人的bug。

在构造测试数据的时候,我主要针对了几个方面对我的程序和别人的程序进行测试。第一次作业中,主要是捎带时电梯和人是否符合逻辑、在输入结束时电梯线程能否正确停止。第二次作业中,主要是电梯内人数是否会超过最大载客量、对于0层的处理是否正确、多部电梯是否都能正确停止。第三次作业中,主要是换乘时是否能正确把人正确送到、换乘时是否没有正确处理数据冒险、不同类型电梯的容量是否超过载客量、电梯是否会在不该停的地方开关门。

五、心得体会

在这一单元之前我从来没有接触过多线程的程序设计,总的来说收获很大,在以下三个方面也深有体会。

首先是多线程的调试难度很大。相比单线程而言,多线程需要考虑的东西更多,不确定性也使得bug复现变得困难,增大了调试难度。在单线程的程序中,如果发现有bug,我通常是在某些地方添加输出语句,然后一步步缩小范围,最终找到有问题的代码。在多线程的程序中,同样的输入很可能会有不同的输出,因此我在测试时增大了测试次数,在出现错误时,先通过输出分析可能原因,然后去查找相应代码或者是构造类似数据使其复现。与上一单元不同的是,我用了大量时间去阅读自己写的代码,逐句分析其功能是否合理以及是否有由于手抖而写错的地方。

其次是对于架构重要性和对象运用的理解。在大一的程序设计和数据结构中,写的是面向过程的单线程的程序,并且代码量不是很大,也没有考虑过架构的设计,都是将重点放到了功能上。而在这一单元的作业中,我发现好的架构不仅能减小bug出现的概率,也能减轻以后迭代开发的负担。在第一单元中,由于初次接触面向对象编程,我不能很好的运用对象和设计架构,导致第一单元的三次作业用了三个不同的架构,这样不仅加大了编程难度,也更容易出现bug。而在第二单元的作业中,在完成第一次作业之前我用了很长时间去设计架构以配合生产者消费者模式,最终这个架构完全适合三次作业的迭代开发,在后续作业中基本只是修改一些功能,并没有大的修改。好的架构不仅可以降低扩展的难度,也可以减少bug的出现。

最后则是线程安全的问题。多线程程序中,不同线程会访问共享数据,因此要加锁保证访问的安全性。比如电梯线程和输入线程会同时访问请求队列。在写代码的过程中,要时刻考虑最坏最复杂的数据访问情况,确保访问数据的稳妥。为避免多线程带来的问题,我在设计时也尽量减少了数据共享。

总的来说这一单元收获还是非常大的,虽然第二次作业损失惨重,但是这也让我吸取了教训,在以后的多线程程序设计中会更加谨慎的去设计和实现。经过两个单元的学习之后,可以明显的感觉到自己对于面向对象的理解与假期完成pre时的不同,也期待着后两个单元的更深层次的学习。

posted @ 2020-04-15 23:13  Jaripon  阅读(139)  评论(0编辑  收藏  举报