第二单元作业总结

设计策略

本单元开始,我们开启了新世界的大门——多线程编程。多线程是一项程序员既爱又恨的机制,爱在多线程的使用可以有效增加CPU的使用效率、加快程序的运行;恨在多线程的引入会带来复杂的进程同步问题,造成难以发觉、甚至难以复现的bug。如何既能享受到多线程带来的程序运行速度上的提升,又能保证程序不出错,这是我们在编写多线程程序时,必须要仔细思考的问题。

设计思想

受到Linux系统“简单的就是好的”的思想的影响,我决定在线程的布置上“能省则省”,绝不多开一个没有必要的线程。这样做一来线程数越少越容易管理,二来可以尽量避免线程对线程的直接调用,从而极大地降低了死锁产生的概率。

在规划构架时,我发现消费者-生产者模式十分符合上述设计思想。因为经典消费者-生产者模型中,仅存在生产者和消费者这两类线程,并且这两类线程不存在直接调用的关系,而是通过一个共享的类实现数据的交换。采用这样的构架可以有效减少程序中进程的数量(分配器Scheduler不需要设为进程,而是作为共享类Tray的形象),同时这样做可以在两类线程中打入一个“楔子”,避免两类线程直接调用产生死锁。

U2H1

第一次作业,本着能省则省的思想,我只开辟了三个线程,分别为main、inputThread、eleThread,并以Scheduler作为中间共享类。同时,为了实现逻辑的耦合,我将所有的逻辑代码放在了电梯中,让电梯根据目前排队等待的人自行决定分派的对象。

图片名称

U2H2

第二次作业,考虑到现在程序中有多部电梯在运行,每部电梯自己去抢人可能会降低电梯运行的效率,故将逻辑分成两部分,一部分为调度器Scheduler分配人员的逻辑,另一部分为电梯内部的“自制逻辑”

U2H3

第三次作业,由于出现了三种不同的电梯,我使用了继承。虽然程序相比第二次作业有着不少的重构,但是线程间调用关系、逻辑分布与第二次作业并没有太大差异,在此不再赘述。(对于构架的分析将在第二部分进行)

图片名称

同步控制方法

基于以上的构架设计,中间的调度器Scheduler成为了我的程序中唯一出现线程间交互的场所,并且线程冲突只可能发生在电梯内部ArrayList的读写和Sheduler的访问上,因此只要将Sceduler中设计到人员分配的函数和ArrayList的读写方法设计为线程安全方法即可避免线程冲突带来的错误。测试证明这样的设计是比较鲁棒的,至少在3次强测、互测、自测中没有出现死锁的问题。

结束方法

前两次作业,我设计的结束方法基本相同,即在inputThread读入ctrlD时,调用Scheduler的add方法加入一个特殊的人。Scheduler在发现这个特殊的人后,并不会将这个人真正的添加到电梯中去,而是直接将结束标志位置1,并notifyAll唤醒电梯,进入终止步骤。

第三次作业的的结束方法与前两次基本相同,但是根据换乘的需求对结束方法进行了改造。由于换乘时,电梯会向Scheduler退回人,因此如果再按照前两次的方法判断是否结束可能会导致换乘的人被关在电梯外。为了解决这个问题,我将判断是否开始结束步骤的任务交给了main线程,并在Scheduler类中加入了记录当前还未到达目的地的人的数量。只有inputThread结束后,且main线程发现所有乘客均已到达目的地后才会结束线程,从而解决了上述问题。

图片名称

图片名称


第三次作业架构设计的可扩展性分析

重构过程

Extract Method 1(提炼函数)

写作业一时,我没有好好地去构思每个电梯的循环应该是什么样子的,哪些代码是必要的,哪些有事可以合并的,因此得到了52行的run()方法。这使得我在写第三次作业的时候都快要忘记为什么要写这么繁琐的代码了所以我开始了重构。run()最重要的职责就是简单明了地让看代码的人看出这个线程在重复不断地干什么,因此我将run()分解成了scheduling、open、getIn、getOff、close等顾名思义的小函数,只展示出电梯的运行逻辑,隐去了底层实现,使得臃肿的run()缩短到了23行。

图片名称

Extract Method 2(提炼函数)

第三次作业中共三种电梯。为了使调度策略最优,每种电梯应当按照不同的调度策略执行运送任务。为了是继承时能够尽量少的复写代码,我将电梯的调度逻辑单独写成了一个函数(选人逻辑、运行逻辑),这样我只用在A、B、C类电梯中覆写其调度逻辑这一个函数即可,最大程度地实现了代码的复用。

第一张图为父类电梯线程运行逻辑的一部分,其中chooseFromInEle()为电梯的选人逻辑。第二张图为A类电梯的选人逻辑。这样一来就可以非常轻松地根据电梯停靠楼层的特点,为每类电梯书写运行逻辑。

图片名称

图片名称

工厂模式

在我阅读调度器Scheduler类添加电梯的代码段时,我发现了大量的重复代码,以及多重if-else语句,我决定利用工厂模式将代码变得清爽一些

重构前

图片名称

重构后

图片名称

Move Method(搬移函数)

对于大量if-else语句堆叠还有另一种重构方法,即搬移函数,将函数挪到更加合适的类中

重构前

图片名称

重构后

load本为电梯的固有属性,因此将load的计算挪至电梯类内部

图片名称


图片名称

可扩展性分析

开闭原则(Open Close Principle):由于我在重构时提炼了各个电梯的选人逻辑、调度逻辑、开关门逻辑、超载判断逻辑,因此可以很容易地在需求改变之后对电梯中的某些函数进行复写,从而轻松实现新的需求。除此之外,大量的代码复用可以避免“霰弹式修改”的尴尬局面。一旦出现任何bug,只修改一处即可,不会出现要同时去A、B、C类电梯代码中修改的尴尬局面。

里氏代换原则(Liskov Substitution Principle):所有的继承类都恪守了里氏代还原则。这次作业中,我的父类电梯更像是一个实现了方法的接口类,其子类均没有任何超出父类定义的方法。

接口隔离原则(Interface Segregation Principle):使用了接口隔离原则,例如EleFactor仅仅用来保存电梯停靠的楼层,这样既可以让所有的电梯使用楼层常数,又不会影响电梯类的继承能力

迪米特法则,又称最少知道原则(Demeter Principle):限制每个类“能看到”的类。例如电梯线程只能看见调度器。输入线程只能看见调度器。

由于实现了以上原则,我的程序可以很轻松的进行电梯多样化需求的扩展,以及运送对象上的扩展(人还是货物)。除此之外,由于在重构时对长函数零容忍,并尽量拆分函数的逻辑,因此可以使得在未来扩展时修改尽量短的代码以到达目的。


程序结构分析

由于对代码进行了重构,实现了大量的代码复用,因此仅600行即实现了电梯的功能,并且平均每个点得分98.4843,还算说得过去

经过重构后,将90%的类的行数限制在了60行以下,实现了高内聚、低耦合,但是其中依然存在EleThread这种God类。对此,我已经想到了将EleThread拆分为电梯轿厢和电梯内部调度器的重构方式,这样可以消除这一God类的存在,并且更好地解耦。但是,由于这样的重构会造成“中间人问题”(想要克服的话需要在每个方法前加上ele.XXXX),解决过程过于繁琐,我就没拆分

代码行数统计

图片名称

UML类图

图片名称

Metrics

图片名称

由图可以看出,经过重构后的代码逻辑分支少、循环少、复杂度低

bug分析

前两次作业中没有出现bug

第三次作业出现了两个bug,分别为FROM-3-TO-4拒载,和电梯人数上限设置出现问题。

由于我在程序中插入了exception语句,使得我在互测时快速定位到了我的bug出现的位置,并进一步判断出是B电梯的转运逻辑出现了问题。由于我之前已经完成了电梯逻辑的拆分,因此我迅速去EleB类中parseToFloor函数中修改了这一bug。这次作业使我真真正正地体会到额继承的强大之处,和重构对理解代码提供的帮助

第二个问题是此前留下来的大坑。在前两次作业中,我没有充分理解static域的意义,直到这次OS实验点醒了我。此前我在EleThread类中MAXLOAD写为静态变量,这就使得所有电梯的人数上限是由最后创建的电梯决定的,因此出现了A类电梯超载的情况。在取消掉MAXLOAD变量的静态属性后,这一bug消失

刀人策略

互测进行时,我便发现了自己的bug,并对此构造了测试数据hack到了同学。除此之外,覆盖式测试经常能够命中bug,这一策略在近几次的互测中屡试不爽。

心得体会


重构是对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。——Martin Fowler

这次作业使我充分体会到了重构的强大威力,它不仅可以帮你理解自己的代码,同时还可以尽可能多地帮你进行复用和覆写,从而极大地降低修改代码的成本。由于我在这次作业中较好地对代码进行了重构,因此几乎没做改动就完成了作业三的编写,并同时实现了不同电梯类的个性化设置。从今往后,我会尽可能地使用重构、继承等强大武器,培养良好的编程习惯。

猜你喜欢

转载自www.cnblogs.com/EricYan2000/p/12722349.html
今日推荐