面向对象设计与构造第二次总结作业
写在前面
通过这三次作业的训练,初步掌握了多线程的设计方法。这三次作业让我对这门课有了更深刻的认识,这门课的确是一门重课,不仅是大难度、高强度,还有它的持久性,吴际老师称其为“昆仑课程”毫不为过。这是身体与心理的双重砺炼。
第七次作业——模拟出租车的乘客呼叫与应答系统
---作业内容
本次作业模拟出租车的乘客呼叫与应答系统,训练线程安全设计方法,同时应用面向对象分析方法和设计原则来开展分析和设计。
作业涉及的对象主要是地图、出租车和出租车调度系统。乘客的请求从控制台输入,乘客发出请求后,系统向以乘客为中心的4x4区域内的出租车广播,收到广播的出租车进行抢单,广播有一定的时间窗口,窗口关闭后,系统从所有抢单的出租车中,根据出租车信用和出租车与乘客的距离来选择一辆合适的出租车。出租车服务结束后,需要把此次服务的路径、时间等信息输出到文件中。
---作业设计
下面这张图片是这次作业的类图。主要的类有出租车Taxi类,调度Scheduler类和地图RoadMap类。从线程的角度看,共有100个出租车线程,因请求而产生的调度线程,主调度线程,输入处理线程和主线程。输入处理线程负责接收控制台输入并处理,主调度线程对每一请求创建一个调度线程,调度线程在4x4区域广播然后选择一辆出租车,出租车服务结束后,调度线程输出信息,然后结束。线程之间的关系可以从后面的协作图中看到。
RoadMap可以存储地图,找出两点间的最短路径,判断由某点向某个方向是否有路。路径是一个Path对象,由方向+距离表示。作业中每条路的长度都相同,所以就省略了距离信息,只记录方向。Path对象是不可变的,因此是线程安全的。整个程序里只有一个RoadMap对象,但把它设计为不可变对象,因此也是线程安全的,不同的出租车可以同时调用shortestPath()方法。
由于要输出出租车的轨迹,出租车某一时刻的状态,为了管理这些信息,分别用PathRecord对象和TaxiInfo对象来记录。PathRecord对象是可变的,可以添加新的轨迹。TaxiInfo对象是不可变的。此外,不可变对象还有Request对象,Point对象,这些不可变对象都是线程安全的。
出租车类有serve(), wander(), rest(), takeOrder()方法,分别对应出租车的服务,等待,停止,接单状态。出租车线程主要与调度线程进行交互,因此,将线程协同与同步控制的重点放在出租车线程和调度线程的设计上。
---度量分析
圈复杂度最大值是13,主要来自于RoadMap的构造方法和出租车类的run()方法。NestedBlockDepth最大值是4,主要是两层循环加上if-else嵌套导致的。总共有三处三层嵌套的代码和三处两层嵌套的代码。
代码总行数约是860行,其中Taxi类,Scheduler类和RoadMap类各占约200行。方法代码总行数约570行,每个方法代码行数最大值是30,平均值是6.2,方法总数是92个。
---优缺点
这次作业的bug是忽略了对出发地和目的地相同的请求的处理,以及出租车接单时正好在乘客的位置上的情况。这两种情况都会导致在寻找最短路径构造Path对象时出现空指针。我认为导致问题出现的原因有如下:
- 作业指导书明确了出发地与目的地相同算作无效请求,可是在程序写的过程中忘了这一点,写完了之后没有再看指导书,也没有测试到。
- 对于每个请求,出租车有两段路程,一是从出租车接单时位置到达乘客位置,二是从乘客位置到达目的地。指导书的要求保证了第二点不会出问题,而第一点是没有保证的,所以需要在编码时考虑到。
- 构造Path对象时应该考虑到路径为空的情况,但设计和实现的时候都没有发现这个问题。
我认为这次写的作业优点如下:
- 类的职责相对比较均衡。
- 在线程的协同和同步控制上基本满足了需求。
缺点如下:
- 一些设计的性能上有提升空间。例如寻找乘客周围4x4区域的出租车,我使用的是对100个出租车都查询一下的方法,其实可以做一些优化。
- 复杂度有些高。例如前面提到的出租车线程的run()方法,尽管没有在run()方法里面展开细节,可还是有多层循环或分支嵌套的情况。在写的时候没什么感觉,现在再次回顾时一下子难以判断出当时为什么这么写,这么写对不对。
每个方法代码量少,但方法数目多,这一点我不知道算优点还是缺点。从上面截图中可以看到方法的平均代码行数是6.2,但方法数却有92个。对于阅读代码者来说,过多的方法数导致难以掌控整体结构,单个方法太庞大导致难以理解局部。如果您愿意同我分享您的见解,请在下面评论。
第六次作业——监控文件属性变化
---作业内容
作业内容是实现一个监控程序,针对给定监控范围内的监控对象,以扫描方式探查监控对象相关属性的变化,从而触发规定的处理动作。监控范围指计算机文件系统中的一棵目录树,监控对象则是位于监控范围内的具体文件。处理动作包括恢复,记录详细信息,记录概要。作业的目标是训练针对线程安全问题,如何平衡线程访问控制和共享对象之间的矛盾。
---作业设计
从线程的角度上说,一条监控命令对应一个监控线程,和一个文件扫描线程。Monitor类能判断监控对象的触发器(包括重命名,移动,大小改变,最后修改时间改变)是否触发,并执行相应处理动作。Snapshot类是文件属性的快照,它的结构就是一颗目录树。SnapshotManager是快照管理类,FileScanner是文件扫描线程,它创建快照并把快照交给SnapshotManager管理。Analyzer类和TaskPerformer类分别是快照分析类和任务执行类,监控线程(Monitor)从SnapshotManager中取出最近的快照,并交给Analyzer分析,如果触发,则让TaskPerformer执行处理动作。Summary和Detail类是管理详细信息和概要的类,这些信息会被定时写到文件中去。
Snapshot对象是不可变的,它记录有扫描时刻目录下所有文件的属性,是一个递归的结构,因此在分析快照时使用起来比较方便。TaskTuple对象也是不可变的,记录有一条监控命令对应的监控对象,触发器及任务。Summary类、Detail类、SnapshotManager类都被设计为线程安全的类,因为所有的监控线程都共享同一个Summary和Detail对象,可能同时被多个线程访问,SnapshotManager在文件扫描线程和监控线程间共享,也需要是线程安全的。SafeFile是线程安全的文件访问类。
---度量分析
圈复杂度最大值是12,来自对输入处理的方法。通过对过去几次作业的度量分析,我发现输入处理的圈复杂度较高,可能是因为需要对多种输入错误的情况做处理,如果您有什么降低复杂度的方法,欢迎在下方评论。
嵌套深度最大值是5,共有1个4层嵌套和3个3层嵌套的代码(方法内部)。嵌套深度最大值来自Monitor的run()方法,循环和try-catch就占了两层,再加上if-else分支又占了两层,总共就是4层。其他几个3层嵌套都上是一层循环加上两层if-else。这种多层嵌套的代码可读性不强,正确性也难以判断,所以要尽量避免。
所有属性的个数是50,方法数是69,总代码量约670行。每个方法代码行数,平均值是6.4行,大部分在10-20行,有两个方法是30来行。
---优缺点
这次作业的bug是,监控重命名时,若同一目录下有多个大小相同,最后修改时间相同的文件,就会判断出错。判断重命名的标准是原来的文件消失(绝对路径找不到了),新增了一个文件,这个文件的大小和最后修改时间与消失的文件相同。我忽略了“新增”的要求,就出现了这个bug。
优点如下:
- 整体结构比较清晰。
- 达到了训练线程安全,掌握如何平衡线程访问控制和共享对象之间的矛盾的目标。
缺点如下:
- 使用了最简单的方法来比较文件名,文件路径,其实可以用散列的方法来提高速度。
- 一些地方嵌套深度比较深,不易读,容易出错。
第五次作业——多电梯调度系统
---作业内容
本次作业是设计一套由3部电梯组成的多电梯调度系统,通过采用线程机制,在第三次作业所实现程序的基础上完成新的调度系统程序。电梯能够支持捎带,调度系统按照运动量均衡策略来调度楼层请求。
---作业设计
由于这次写的程序类太多,类的关系错综复杂,就不给出类图了。下面这张图是一个大体的结构,每个方框表示一个对象,每个椭圆表示一个放请求和取请求的托盘(因为第一次写多线程程序不太熟悉,所以就显式地用“托盘”来表示)。单个箭头表示请求的流动方向,两段都有箭头的,是表示两个线程间有交互。RequestSimulator是请求模拟器,它从控制台中读取输入,产生请求。MultiScheduler是总的调度器,它负责将电梯直接请求分发给各部电梯,并且对楼层请求以捎带和运动量均衡策略进行调度。再往下走是Scheduler,它负责对单步电梯的调度,例如处理捎带请求等等,这就归约到了第三次作业的问题。
---度量分析
圈复杂度最大值是20,来自Scheduler的调度方法。代码总行数约1200行,方法总数是151个。可以发现后面两次作业(6-7)没有达到这么大的规模,原因可能是电梯调度逻辑的复杂性。
---自我评价
此次作业bug主要有两个。一是对输入的处理上,一行多个空请求的处理与说明文档描述不符,是我疏于测试的问题,如果测试了就能够发现。二是楼层请求的捎带有问题,具体是什么问题就没有深究了,我猜测可能是逻辑上的问题加上对线程机制不熟悉的问题。
缺点如下:
- 逻辑混乱。例如调度器的调度方法,电梯的各种方法。一来是继承前面两次作业的设计导致难以修改,牵一发而动全身。二来是加入了多线程后难以梳理出各部分的关系。
- 复杂度太高。高的复杂度导致容易出错,难以测试,难以读懂。
- 违背了很多设计原则例如OCP、LSP。
由于距第5次作业的时间比较久远了,代码写得混乱,不忍直视,所以就简略地分析一下。
结束
感谢您能够花时间阅读本文。这是我对自己三次作业的总结,大部分内容都是对我的作业的分析,可能读者不太了解具体细节,所以,写作时我尽可能避开细节,这样就显得有些宽泛。如果您有疑问或者建议,请在下方评论。