OO第二单元总结

OO第二单元总结

一、设计策略

第一次作业

这次作业的核心的类有输入类,调度器类,电梯类和乘客类。调度器使用了单例模式和消费者-生产者模式,电梯线程是生产者,乘客线程是消费者。第一次作业中我没想到可以把乘客放进队列里统一分配,而是让每个乘客都自己作为一个线程,每个乘客线程都是一个消费者。由于线程过多,造成了debug上的困难,并且过多的线程也会较耗费系统资源。在研讨课上,我听到同学分享说把乘客放进队列里统一调配,因此第二次作业就使用了这个方法。
这次作业对我来说难点主要是对synchronized,wait(),notifyall()等多线程的关键字和方法不熟悉,一开始乱用一通,之后花了好多时间debug才找出wait和notify错用的问题。

第二次作业

第二次作业主要的改变是有了负楼层,增加了电梯数量以及限制了电梯最大载客量。
在第一次作业互测时,我参考了同学的代码架构,思路清晰了不少,并且在第二次作业中进行了一些模仿,比如让main入口更为简洁、以及比较优雅地判断输入是否已结束。
我在第一次作业的基础上进行了重构。这次作业与第一次作业主要的差别就是把第一次作业的乘客类和调度器类合成为这次作业的请求队列类,请求队列类里有一个包含所有乘客的队列,以及对乘客调度的方法。而电梯类的每个电梯实例都是一个单独的线程,我并没有把所有电梯放进一个队列里统一管理,这是因为考虑到之后的作业可能会出现电梯移动的时间不同,或电梯开关门时间不同,或是老师和助教在研讨课剧透的电梯停靠楼层不同。当电梯线程运行时,电梯会到请求队列里获得当前楼层的乘客,然后把乘客运到目的地,直到输入结束并且请求队列里没有乘客请求了。

第三次作业

第三次作业主要的改变是把电梯分为3种,电梯的上下楼速度不同、最大载客量不同、停靠楼层不同,并且可以动态加入电梯。
乔同学在十五号放映室里写的对第二次作业进行解析的文章启发了我,与其让所有乘客挤在请求队列类的队列里,何不创建一个楼层类,让楼层类自己管理在当前楼层等待的乘客呢?因此,我再次进行了重构。
在这次作业中,核心的类是控制器类,楼层类,乘客类,和电梯类。每一楼都会有自己的楼层实例,存储着在当前楼层的乘客,并且楼层类里包含分配乘客上电梯的方法。每个电梯都是一个电梯类实例的线程,跟控制器交互以取得乘客。
我觉得这次作业最大的挑战在于电梯的停靠楼层不同,因为需要考虑电梯该如何协作以把乘客运到目的地。而且调配不当的话可能会出现死循环的bug,比如在我最初的程序中,若乘客要从3楼去5楼,并且接受请求的是C类电梯,则会出现电梯不断在3楼和5楼往返接人放人的死循环bug。我的解决方法分出了许多特例分别对待,比如需要同时针对乘客的起始位置和终点位置判断当前电梯是否可以接受请求;又如若电梯接受了请求但乘客要抵达的目的地不是电梯的停靠楼层,则需把乘客送到哪一层。实现方式非常不优雅,并且容易出现新bug。由于对于电梯的协作没有进行严谨的分析,因此我花了巨多时间在debug,并且ddl前一小时才通过中测。

二、第三次作业架构设计的可扩展性

基于SOLID的程序评价

  1. 单一责任原则(SRP):
    整体架构大致满足单一责任原则。main类负责启动、input类负责解析输入、调度器类负责电梯和楼层的协同、楼层类负责接受调度器的请求,把乘客分配给电梯、电梯类负责向调度器请求获得乘客,并把乘客运送到目的地、乘客类有起始楼层和目标楼层,以及进出电梯时的输出语句。
  2. 开闭原则(OCP):
    非常羞愧的是,OO课程已过大半,但是我的编程习惯还是停留在面向过程的层次。这次作业的3种电梯其实可以采用继承的方式来实现,这样如果要加入一种新类型的电梯,我可以通过扩展来加入新的电梯类型。而在我的程序当中,若要增加新类型的电梯,我需要对电梯类进行改动,违反了开闭原则。
  3. 可替换原则(LSP):
    此次作业没有使用继承,不满足可替换原则。
  4. 接口分离(ISP):
    只有电梯类继承了Runnable接口,此外并没有自己创建的接口。
  5. 依赖反转原则(DIP):
    此次作业没有使用抽象接口,不满足依赖反转原则。

三、度量分析

第一次作业

复杂度分析

UML类图


优点分析:把电梯线程作为生产者,乘客线程作为消费者。生产者和消费者线程仅仅与托盘进行交互,而不与对方直接进行交互,程序逻辑简单,降低了程序的整体耦合度。
缺点分析:可以看到Controller类的复杂度很高,这是因为controller作为调度器,同时也作为生产者-消费者模式里的托盘,一方面要不断地与电梯线程和乘客线程交互,一方面又要进行计算行为以调配乘客。一个可能的改善方法是把调度器的调度职责和托盘职责分开,把调度器类分为两个类。

第二次作业

复杂度分析

UML类图

优点分析:整体复杂度比第一次作业有所降低,这是因为我对设计进行了一些优化。
缺点分析:电梯类的复杂度很高,这是因为电梯类的run方法有许多的逻辑判断,并且电梯类需要主动判断何时应该向请求队列类获取请求,这些操作增加了其复杂度。

第三次作业

复杂度分析

UML类图

UML协作图

  1. 主线程
  2. 输入线程
  3. 电梯线程

缺点分析:从时序图上可以看到电梯线程的判断逻辑太复杂。从OO度量图上可以看到电梯线程和楼层类线程极其之复杂,有许许多多的逻辑判断。一些可能的改善方法是让不同的电梯类型继承电梯类,以及把楼层类与电梯类中关于调配的方法再分化出一个二级调配器,以简化臃肿不堪的类。

四、分析自己程序的bug

第一次作业

在强测中没bug。
在互测中有1个bug。

bug分析:

在作业中我把电梯设计成在同一层开关门后就不再开门。但是没有考虑到的是:如果电梯在终点层开关门后就暂时没有别的请求,电梯停靠在当前楼层。而过了一段时间以后,来了新的请求,并且新请求的起始层就是当前电梯所在楼层,这时候电梯就应该再次开门。

第二次作业

强测和互测都没有被发现bug

第三次作业

在强测和互测中有很多bug,但都属于一个同质bug,只有一个互测bug不属于这个同质bug,由于此bug太难复现,所以到目前为止我还不知到到底是什么原因引发了这个bug。

bug分析:

在Person类中设置了stopinfloor状态。用来表示若电梯无法到达Person要抵达的楼层,则Person应在当前电梯离开的楼层。但当Person离开电梯后,我忘了把stopinfloor设为无效,导致下次Person进电梯时出了bug。

五、分析自己发现别人程序bug所采用的策略

由于我没有搭建自动评测机,所以只是把我平时遇到比较tricky的bug记下来,然后用来hack别人。
第一次和第二次作业我都没有hack别人,第三次作业成功hack了别人2次,都是同一种类型的bug,就是在乘客请求都结束后,再动态加入电梯,然后结束请求,这时可能程序会陷入死锁,无法正常结束。

六、心得体会

在这单元接触了多线程编程后感觉就像打开了编程世界的一座大门。从一开始面对wait和notifyall无所适从,经过三次作业的训练后,现在已经可以比较熟练地应用这些多线程语句了,并且感觉多线程还蛮有趣的。

在做作业期间时常碰到超时的情况,这通常是因为发生了轮询,或者发生了死锁,或者发生了无限循环。若要在本地查询是否超时,可以使用JProfiler来查看CPU时间。

在线程安全方面,我觉得在写代码前需要有清晰的规划,不然乱用多线程语句的话可能会产生莫名其妙的bug,并且多线程bug往往是难以复现、难以debug的。

在代码设计原则方面,到目前作业为止,我觉得我采用的架构都不够好,都没有满足SOLID原则。之后作业的设计目标都会是尽量满足SOLID原则。

此外,第三次作业让我深刻地认识到了自动评测机的重要性。由于前两次作业的判断逻辑都比较简单,没有什么bug,所以不觉得自动评测机很重要。然而到了第三次作业,电梯之间的协作逻辑比较复杂,用肉眼根本无法看出是否有出错,所以这时才发现自动评测机很重要。

posted @ 2020-04-17 18:00  samuel234  阅读(136)  评论(0编辑  收藏  举报