综述

主要任务就是写一个电梯模拟器,读入每一个人的请求然后让电梯把他们送到想去的地方。

从第一次到第三次作业,三次的主要任务都是相同的,但是每次都增加了很多的细节,每次的难度都逐步增长,电梯复杂度和瞎跑度都大大提高;同时我们也对于多线程的设计、写法、调试也逐渐有了心得和经验。

以综合性最强的第三次作业为例。它要求的电梯和一般的普通的常见的电梯,又有很多不同:首先这玩意是目标选层电梯,在进电梯之前就先选好楼层,然后坐等电梯送你去;然后这玩意开关门十分鬼畜,开门一瞬间(即使只有一条缝),我们的乘客都可以从这一条缝挤进电梯;再然后这个电梯好像是支持全自动的:假如A电梯能从1楼到3楼(但不到5楼),B电梯能从3楼到5楼(但不到1楼),这时候一个想从1楼道5楼的乘客,他只需要在1楼设置好楼层,然后进A电梯,随后我们的电梯系统能够自动完成换乘的事情,而根本不用乘客去考虑:我需要先到3楼然后摆摆腿出来换乘。这就好比你从沙河上了昌平线然后地铁上睡了一觉,原地醒来就到了知春路。

我们呢先不管这些逻辑上的不熟悉,来考虑一下电梯的设计。

需求

设计一个电梯系统,包含三个电梯,名为A、B、C,他们的可停靠楼层、最大载客量、上下层速度都不一样。可停靠楼层如下图所示:

要求对于所有不同时输入的请求,都有相应的调度,能够在最短时间内送人到达目的地。

设计

输入处理:利用官方给的阻塞式端口输入,然后判断这个请求能不能由一台电梯独立解决;不行则拆解成两个请求。比如从-3到3的请求,我就拆解成-3到1,再从1到3。然后将请求加入一个队列。

电梯调度:调度器为每个空闲的电梯分配一个合适的任务,让他去跑。那么什么叫“合适的任务”呢?就比如本题的C电梯,它的合适的请求就是出发楼层和到达楼层都在C电梯的可停靠楼层范围内的那些请求,比如1-3、5-15,但像2-4、6-14就不是了。当然对于一组有先后顺序的请求,比如一个原本为从-3到3的请求,被拆解成-3到1,再从1到3——在-3到3完成之前,1到3也不算一个“合适的”请求。

捎带和被捎带问题:设计一个楼层类Floor,用来记录在着一层楼等待的乘客。电梯每到一个可停靠楼层,我们就先把想要这一层楼离开电梯的兄弟给踢走,然后在着一层楼中先找到那些能捎带上的兄弟,把他们带上。如果更详细地再分析一下,这其实还包含着主请求的变更,比如你在1楼接了一个FROM-1-TO-3,然后在2楼顺了一个FROM-2-TO-5的兄弟;那么电梯到达了3楼之后,把第一个兄弟踢走之后需要把主请求修改为FROM-2-TO-5,好让电梯接着走。

输出:电梯每执行完一步操作就对应输出“arrived”,“open”,“close”等。

多线程的设计

设计五个线程:主线程(兼输入)、调度器线程、三个电梯线程。调度器与主线程之间通过线程安全的“队列”进行交互,调度器与电梯之间直接通过调用异步的方法进行通信。

历程

重构前:对于每个请求,我都随机派一个电梯去起始楼层 x 接人。而这分为两种情况:若该电梯能到达 x ,开门就完事了;要是到不了,再把这个“去楼层 x 接人”的请求放回队列。假如成功接到了人,电梯也还要动脑子:这个人想去3楼,我能不能到3楼呢?哎呀,好像不行。那,我又该在几楼把他放下来呢?我是不是该通知C电梯,让给他帮我一手,把这兄弟送到目的地。

重构后:拆解请求,让前文“想”的这个过程交给调度器,把任务分解、分配给傻瓜电梯,这样电梯就不用动脑子去想我该在哪里把这个人放下来,而是简单机械地执行调度器给的请求就好了(对电梯和程序员都友好)。

重构是件很纠结的事情:本来写了快一千行,但是好像又不满意,bug越写越多;重构吧,又怕遇到新的问题白白浪费时间。

结果

前两次作业没有问题,就是老老实实按照指导书算法写,正确性没有什么问题。

第三次作业嘛。重构一下,就出事了:因为最大楼层不知道为什么写成了19(显然20才对啊,真是蠢),然后导致第20层楼捎带的时候会爆炸,表现在“这个人莫名其妙地在19楼进了电梯”,所以就爆炸了。出现这个问题,确实应该怪自己,怪自己畏惧近千行代码没有敢去一行行看,去找逻辑错误;怪自己没有想到用随机生成数据去做覆盖性测试,不然这样的问题肯定能发现。

性能分析

类图:

方法复杂度:

(仅显示复杂度较高的方法)

明显可以看出disassemble方法(也就是那个拆解一个请求为两个所用的方法),由于包含了大量的if-else语句导致设计复杂度很高,但是本质复杂度很低。

另外两个复杂度大头相信大家也差不多:线程的run()方法,好像没什么方法来做有效的优化。

illegalFromFloor()方法中,采用了很多类似于

if(A == true) return true;

这样丑陋的代码,导致本质复杂度很高。

lift()方法由于包含了很多与捎带有关的和与电梯状态的修改,但是写法又是基于简单的for-if,所以本质复杂度不高但是设计复杂度比较高,所以这也是容易产生bug的地方,需要程序员仔细检查代码逻辑。

类复杂度:

改进和SOLID原则分析

  • 有些方法(参考性能分析-方法复杂度)的本质复杂度较高,体现的是程序员对语言的的不够熟练。
  • 有些特殊情况不能正常退出。
  • 电梯和调度器有些功能的分离还不是很彻底,因为有一些方法到底是属于电梯好还是属于调度器好,我还没有定论。有少许违背单一责任原则(SRP)原则。
  • 对于扩展不是很方便,虽然利用了工厂来建立新电梯,使得第四台电梯的加入十分方便,但是假如有其他“反人类”的需求比如乘客1不喜欢A电梯非要坐B电梯,那么对于既有的代码不能够保持原封不动。有违单一责任原则(OCP)原则。
  • 没有类的父子关系。不违背里氏替换原则(LSP)原则。
  • 高层模块“调度器”对于不同的底层模块“电梯”都能够同样地执行。符合依赖倒置原则(DIP)原则。
  • 仅使用了Runnable一个接口。不违背分离原则(ISP)原则。

心得体会

通过这三次作业,逐渐地了解了多线程设计的思想。从第一次作业的毫无头绪、仿照课件,到查阅锁的使用,再到研究设计模式和SOLID原则,自己的代shi码shan也逐渐地OO了起来。对我来说,几大难题分别是:多线程的同步控制和电梯逻辑的控制。对于多线程的控制是通过了大量的本地测试和根据例子进行仿照,后来才开始系统的了解锁的使用,确立哪些东西需要加锁哪些不需要,哪些需要保护哪些不需要;线程之间可以有哪几种通讯的方式。另外一个难点是多线程环境下对于每个线程的逻辑debug。我写的电梯逻辑一开始是充满了许多bug的,后来是通过”走一步print一步“的思想来肉眼跟踪找到bug的。业界有人认为:多线程是程序员在工作中遇到的最大的魔鬼。在这三次作业中,我也感受到了,因为多线程的bug隐藏的一般很深,有些bug产生的条件也比较苛刻,甚于充满偶然性。

OO随笔之纠结的第二单元——多线程电梯的更多相关文章

  1. OO第二单元多线程电梯总结

    OO第二单元多线程电梯总结 第一次作业 设计思路 Input为输入线程,负责不断读取请求并将读到的请求放入调度器中. Dispatcher为调度器,是Input线程和Elevator线程的共享对象,采 ...

  2. BUAAOO第二单元多线程电梯作业总结

    第二单元多线程作业需要保证线程安全

  3. OO第二单元多线程电梯总结分析

    一.概述 这一部分的作业考察的关注点与上一次的作业有所不同,上一次的考察重点主要集中在输入输出的判定以及多态的考察上面,而这一次是让我们进行多线程程序的调度与开发.这次开发过程中最大的感受就是自己之前 ...

  4. OO第二单元——多线程(电梯)

    OO第二单元--多线程(电梯) 综述 第二单元的三次联系作业都写电梯,要求逐步提高,对于多线程的掌握也进一步加深.本次作业全部都给出了输入输出文件,也就避免了正则表达式判断输入输出是否合法的问题. 第 ...

  5. 「BUAA OO Unit 2 HW8」第二单元总结

    「BUAA OO Unit 2 HW8」第二单元总结 目录 「BUAA OO Unit 2 HW8」第二单元总结 Part 0 前言 Part 1 第五次作业 1.1 作业要求 1.2 架构设计 1. ...

  6. oo第二单元——多线程魔鬼电梯

    在初步认识了面向对象思想后,立刻进入了多线程的学习,本单元的难点主要是锁的理解,需要保证线程安全的同时防止死锁的发生,也要尽可能缩小锁的范围,提高性能.这一单元以电梯为载体,让我们从生活出发,从电梯运 ...

  7. OO第二单元总结——电梯调度问题

    一.设计策略. 在三次作业中,多线程程序的实现分以下几个步骤: 1. 主线程Main类的创建多个线程. 2. 共享对象的synchronized锁保证线程之间的互斥访问. 3. 采用notifyAll ...

  8. OO第二单元总结——电梯

    在电梯系列的作业中,笔者的整体架构几乎没有发生改变.现介绍如下,对于一个电梯系统,主要的工作步骤就是获取乘客请求.分派请求.执行请求.针对这样的工作模式,笔者设计了Elevator.Uselist两个 ...

  9. OO随笔之魔鬼的第一单元——多项式求导

    OO是个借助Java交我们面向对象的课,可是萌新们总是喜欢带着面向过程的脑子去写求导,然后就是各种一面(main)到底.各种方法杂糅,然后就是被hack的很惨. 第一次作业:萌新入门面向对象 题目分析 ...

随机推荐

  1. C语言结构体及其内存布局

    code[class*="language-"], pre[class*="language-"] { color: rgba(51, 51, 51, 1); ...

  2. 2021年的UWP(6)——长生命周期Desktop Extension向UWP的反向通知

    上一篇我们讨论了UWP和Desktop Extension间的双向通讯,适用于Desktop Extension中存在用户交互的场景.本篇我们讨论最后一种情况,与前者不同的是,Desktop Exte ...

  3. JAVA题目:小芳的妈妈每天给她2.5元,她都会存起来,但是,每当这一天是存钱的第五题或者5的倍数的话,她都会去用掉6块钱。 问:至少经过多少天可以存到100块?

    1 /*题目:小芳的妈妈每天给她2.5元,她都会存起来, 2 但是,每当这一天是存钱的第五题或者5的倍数的话, 3 她都会去用掉6块钱. 4 问:至少经过多少天可以存到100块? 5 */ 6 /*分 ...

  4. CI/CD版本回滚Jenkins解决方案

    一.创建项目 填写项目名,关系到项目路径对应请谨慎命名 二.项目配置 1.配置字符串参数和选项参数 2.代码仓库配置 3.构建环境 4.构筑脚本配置 5.点击左下方的保存或者应用 三.使用方法 1.发 ...

  5. 使用gradle插件发布项目到nexus中央仓库

    目录 简介 Gradle Nexus Publish Plugin历史 插件的使用 Groovy DSL Kotlin DSL 插件背后的故事 总结 简介 Sonatype 提供了一个叫做开源软件资源 ...

  6. 通过Dapr实现一个简单的基于.net的微服务电商系统(五)——一步一步教你如何撸Dapr之状态管理

    状态管理和上一章的订阅发布都算是Dapr相较于其他服务网格框架来讲提供的比较特异性的内容,今天我们来讲讲状态管理. 目录:一.通过Dapr实现一个简单的基于.net的微服务电商系统 二.通过Dapr实 ...

  7. Spring Boot 2.x 快速集成Kafka

    1 Kafka Kafka是一个开源分布式的流处理平台,一种高吞吐量的分布式发布订阅消息系统,它可以处理消费者在网站中的所有动作流数据.Kafka由Scala和Java编写,2012年成为Apache ...

  8. ASP.NET CORE使用WebUploader对大文件分片上传,并通过ASP.NET CORE SignalR实时反馈后台处理进度给前端展示

    本次,我们来实现一个单个大文件上传,并且把后台对上传文件的处理进度通过ASP.NET CORE SignalR反馈给前端展示,比如上传一个大的zip压缩包文件,后台进行解压缩,并且对压缩包中的文件进行 ...

  9. 案例分析–Note-taking Management Softwares

    项目 内容 这个作业属于那个课程 2021春季学期软件工程(罗杰.任健) 这个作业的要求在哪里 案例分析 我在这个课程的目标是 团队协作,利用软件工程的思维和方法开发出一款具有实用价值的软件 这个作业 ...

  10. [ssh登录]ssh登录报警

    1.报警脚本 mkdir /scripts vim /scripts/telegram_ssh.sh #!/bin/bash text="用户: $PAM_USER 用户IP: $PAM_R ...