面向对象第二单元总结
第一次作业 FAFS傻瓜式调度
1.1 实现思路
作为第一次作业,主要目的是让我们熟悉多线程的基本操作,这次的设计也非常简单,主线程负责从标准输入中读入PersonRequest类型的数据,调用control类里面的方法,将其保存到control里面的queue类型的数据结构,由于是FAFS的调度方式,所以使用queue类型的方式存放数据就行,电梯内部也只需要放置一个PersonRequest类型的变量request,根据request的fromfloor和tofloor去接乘客并送到对应的目的地即可。由于测试没有卡cpu时间,电梯以轮询的方式等待新的指令和结束符号。
1.2 代码结构、数据
UML的协作图如上图所示,主线程创建control类型,然后创建电梯的时候将control传入进去,之后电梯运行中间在电梯内部调用control,不依赖主线程,主线程和电梯线程并发操作,通过对control的synchonize实现同步。
属性方面,电梯内部放置了必要的电梯状态、乘客request、获取命令的control类的对象。control内一个是指令队列queue,结束标志符end。
作业1类的数据和方法数据如上图,可以看到类的总代码行数不多,只有99行,平均33行,主要集中电梯的运行上。电梯里面运行逻辑非常简单,所以全部写在run函数里面,control类里面的方法虽然多,但行数都是3行,都是简单的对control里面的元素进行设置或者取值操作,没有控制和分支。主函数也只是简单的启动和将标准输入的数据调用control进行处理,所以只有一个main方法,考虑到标准输入里面可能输入正常的数据或者结束信号,所以有一个分支。电梯类里面保存属性虽然多,但基本都是运行时间、开关门时间、电梯状态的定义,分支也只是上行和下行的分支。
1.3 程序结构分析
优点:简单、满足设计需求、线程安全。
缺点:电梯没有模块化,复用性差。
SOLID分析:
S:单一功能原则满足,主进程类在完成多线程启动后只负责读入指令和将指令放入control,control类中放着数据以及访问和修改数据的操作,电梯负责运送指令,即输出操作。只有主类由于作为main方法,多了启动多线程的功能
O:开闭原则不满足,由于整个设计比较简单,各类的设计 没有考虑开闭原则。主类的功能几乎没变,control类使用的的是queue类型储存指令集,没有考虑后续拓展的设计。电梯类也是,由于比较简单,run函数包含了所有操作,没有模块化的设计,所以后续作业得重构代码。
L:由于结构过于简单,没有子类,没有继承,没有使用里氏替换原则的地方。
I:同上,没有接口设计,没有使用单一接口原则
D:没有依赖倒置原则,没有使用抽象层,电梯类和主类直接依赖control类。不过电梯的代码规模还是比较小的,所以基本没有层次区分。
总结:
现在回顾第一次作业的分析,设计上非常不成熟,只是能满足任务而已,没有使用solid原理以及考虑后续的作业的设计,导致第二次第三次我除了主类都需要进行重构。
第二次作业 ALS可捎带电梯
2.1 实现思路
这次的电梯由于是可捎带的,所以电梯内部保存了乘客队列,而且整个的指令等待队列也有很大的改变,为了方便电梯在每层楼进行捎带,所以将单一的指令队结构改变为链表结构,一个链表将每层连接在一起,每层放一个上行链表和下行链表。电梯使用look算法,算法的实现是在电梯中。这次没有使用轮询,使用了wait和notify的组合。
2.2 代码结构、数据
作业二的UML的协作图如上图所示,其中最顶层的requestList类实际上是保存了每层的的指令列表,一个上行和一个下行链表,control依旧是封装了requestList和访问requestList的方法,同时作为锁实现电梯和主线程之间的同步。
属性方面,电梯里面有必要的电梯的上下行、停靠3种状态、开关门时间、单层的运行时间、乘客列表passenger,还因为look算法的实现在电梯内,电梯还保留了底层bottom和顶层top的楼层数,防止电梯访问越界、以及request保存了当前电梯内部的乘客最远的楼层数作为电梯运行的目的地。
类的数据和方法数据如上图所示,代码总行数317行,主要集中在电梯类,有219行。由于电梯运行和查找指令的方法都封装在电梯中,所以电梯集中了大多数的行数。主函数功能基本没变,基本上是直接复用第一次作业的代码,只是修改了底层-3和顶层20这些细节上的设定,放入指定和设置结束的标志的代码没有变。RequestList类和Control类也只是封装了数据和数据的设置和调用的方法,由于这次的作业没有多线程合作的问题,也没有输入的指令进行处理的问题,没有涉及到输入的指令拆分处理的函数,只是通过输入的指令的fromFloor和toFloor将其放到对应的楼层对应的链表而已,只有一个由于没有第0层,所以有一个将-3到20层的不连续的楼层数映射到0到22这个连续的楼层数trueFloor()。
这次的作业的主要工作是对电梯类进行重构使得其能够实现运行时捎带的功能。由于设计的时候电梯内部是模块化的处理,所以主要运行的run方法行数增加不多,只有59行,其余的10个方法实现电梯的具体运行功能,比如启动、上下客、改变楼层、look算法等等,具体到每个方法,规模都不是很大,大部分在20行一下,实现一些简单的功能、主要是实现look算法的checkAll()方法行数比较多,共33行。控制分支方面,启动的时候根据结束标志位和对楼层指令集进行检查确定是否停止的分支,启动的时候根据命令决定电梯上行下行还是在当前楼层上客的分支,电梯内部没有乘客后,根据look的结果决定上行还是下行的分支,分支比较多,但都是电梯运行的过程中必要的。除了电梯类之外,Control类和RequestList类跟作业1一样,方法很多,但行数非常少,主要是设置和访问数据的函数,主要分支控制是将传入的从-3开始的不连续的楼层数,映射到连续的从零开始的楼层数的trueFloor()方法中有根据输入进行不同的映射的分支。RequestList中,根据输入的request的fromFloor和toFloor将其放到哪个链表的分支,根据电梯传进来的状态属性,返回对应的上下行的链表的控制分支。
2.3 程序结构分析
优点:满足作业设计需求;线程安全;进行了模块化的设计和一定程度按照单一功能原则,代码复用性较第一次作业高。
缺点:Control还是作为一个保存数据和调用数据的方法类存在,而不是单独的线程,并行性较差;没有完全使用单一功能原则,复用性还是不够。
SOLID原则:
S:单一功能原则,基本满足,主要不满足的是主类负责启动多线程和接受输入的指令并调用Control类的函数将其放入对应楼层的指令链表中,跟第一次作业一样。主要不满足的是电梯类,既有电梯运行上下客的电梯基本功能,还包含了look算法进行寻找指令的实现。
O:开闭原则,基本满足,主要也是电梯类的算法问题。
L:没有子类,没有继承的设计,没有使用里氏替换原则。
I:同上,没有接口设计,没有使用单一接口原则
D:没有依赖倒置原则,没有使用抽象层,电梯类和主类直接依赖control类。不过电梯的代码规模还是比较小的,所以基本没有层次区分。
总结:第二次作业了,相对于第一次作业,设计上已经一定程度上实现模块化和应用solid原则,不过由于当时对于SOLID原则没有了解,设计的时候还是不够完整,代码的复用性还是不够好。
第三次作业 多电梯调度
3.1 实现思路
这次作业实现的是三个电梯线程,而且每个电梯线程的可停靠楼层不同,算法使用的是ALS算法和scan算法,将三个电梯的数据保存在Control中,Control类根据各电梯当前状态和可达楼层,同时将输入的指令进行拆分和分配,由于要保证拆分的指令的执行的循序性,增加了一个保存拆分的后半段指令的链表buffer,各电梯在下客完之后将当前楼层下客的id列表传入Control,Control根据列表将buffer中的指令的后半段从buffer内取出,放入对应的电梯的指令列表中。现在指令的拆分和分配算法以及电梯检索目标的算法都在control类中实现,control类从之前的保存数据和调用数据的类变成能够实现调度指令功能的类,但依旧作为锁出现,也没有作为一个单独的线程。
3.2 代码结构、数据
作业三的UML的协作图如上图所示,LiftA、LiftB、LiftC是继承自电梯类的三个子类,能涵盖电梯类的所有功能,区别主要是具体的ID和每层楼的运行时间这些具体的参数。具体结构跟前两次作业一样,主类和电梯类依赖Control类对各种数据进行访问和修改之类,Control类中增加了Buffer类,保存拆分指令的后半段,增加了ElevatorStatus类,保存电梯状态,电梯通过调用control类里面的函数对其进行更新。Control类实际上是保存指令、访问修改指令、分发调用指令的算法的总和,同时作为锁对三个电梯和主类之间进行同步操作。线程间的协作关系跟第二次作业基本一致,主类、各个电梯类调用Control类里面的方法对Control类里保存的命令和电梯状态这些数据进行更新、增加、取得操作。
属性方面,相较于第二次作业,电梯类新增了ID和容量上限passengerSize,control类里面新增了大量的属性,包括三个电梯的可达队列,以及保存当前指令三个电梯是否可达、当前各电梯是否空闲,这些分配指令算法所依赖的属性。ElevatorStatus里有电梯的上下行、当前楼层、乘客人数、上下行的起点和目的地这些电梯状态属性。
作业三的类和方法的数据如上图所示。这次的由于增加了拆分指令和调度三个电梯的功能,任务复杂度急剧增加,代码总行数达到755行,类平均行数有84行,由于三个Lift子类保存的只有创建函数,行数非常少,主类只是增加了创建和运行线程的代码,基本上没有变化,Control类增加的拆分和分配指令的算法,所以行数变得非常多,电梯类由于查找指令的算法被放到了Control类,总行数减小了不少。
方法方面,方法的数量很多,但平均行数不是很多,有很多方法的还是调用和修改某项变量的,只有几行;方法的行数基本集中在拆分、分配、查找算法的方法。ElevatorStatus类、RequestList类、BuffRequest类由于保存的属性多,所以方法数目多,集中在访问和修改属性,但是方法的平均行数少;Control里面主要是拆分查找这些复杂方法,数目不多,但是方法比较复杂,平均行数多;电梯类变化不大,运行各个步骤作为单独的方法存在,相对于第二次作业,只有一些细节上的修改。控制分支方面,主要集中在控制类里面,主要是增加指令的时候要根据各电梯的状态对指令进行拆分和分发操作、根据电梯的不同运行状态返回下一个运行的楼层;电梯类中根据电梯内的乘客数决定在当前楼层上的乘客数,以及根据返回的楼层数进行的方向改变这些必要的分支。有分支的方法不多,主要集中在算法类的方法中。
3.3 程序结构分析
优点:满足作业设计需求;线程安全;遵循单一功能原则和模块化设计,代码的复用性高。
缺点:Control没有作为一个单独的线程,而是作为锁存在,三个电梯不能同时访问数据区域,并行性差;没有根据buff内的指令预先调度电梯的设计,运行效率还有提升的空间。
SOLID原则:
S:单一功能原则,电梯类、各种数据类都满足;控制类中增加指令和返回电梯的楼层的功能或许可以拆开,不过逻辑上说这些都是调度器应该具备的功能;主类中依旧是创建和输入指令的功能没有分开。
O: 开闭原则满足,这次电梯不在内部实现查找算法,其下一个运行楼层完全取决于Control类的返回,Control类对这些算法进行拓展,不需要修改电梯的获取指令的操作;同样对于主类输入指令操作,对其进行拓展支持指令分割的时候不需要修改主类部分的代码。
L: 这次有继承,LiftA、LiftB、LiftC三个子类是能够完全取代父类电梯类,使用了里氏替换原则。
I: 没有接口设计,没有使用单一接口原则的地方
D:没有依赖倒置原则,没有使用抽象层,电梯类和主类直接依赖control类。不过电梯的代码规模还是比较小的,所以基本没有层次区分。
总结:这次的代码基本上是按照模块化和单一功能原则来设计的,代码的复用性上已经足够合理了,只是由于代码比较复杂,时间分配还是不够合理,代码没有做过多的优化,运行效率还不够。线程是足够安全的,但是可以把一些数据和算法分离出来,各电梯访问各自的命令集,维护各自的ElevatorStatus类,提高线程间的并行效率。
同时由于没有完整的构建测试数据集,没有考虑到边界条件,导致我的电梯不会访问顶层,最终导致了我的电梯被强测爆掉了。
总结
从设计原则上,通过这这三次作业,我逐渐对于SOLID原则有了更深的了解,尤其是S和O方面,并以此来设计我的代码,但是由于代码还是比较简单,LID方面基本没有涉及。不过就S和O方面的使用,让我的代码的复用率逐渐提高,使得我的可以减少不必要的重构时间让我集中在必须要重构的调度算法方面。之后我会尽量使用SOLID原则设计代码来提高编程效率。
线程安全方面,由于所有的数据都是由Control类进行创建和管理的,所以只要将Control类锁上,线程间是足够安全的,线程间的协作效率差,一个线程的操作会阻碍另一个线程访问与其无关的数据和方法,这部分其实可以更好的设计,提高运行效率,不过由于时间和精力的问题,我优先在保证安全的基础上使用更简单同步方式,如果有足够的时间和精力我会尽可能的使用更高效的同步方式。