结对项目——电梯调度
结对成员:王梓屹1120192548 陈泓铭1120192529
文章目录
一.问题假设:
一幢21层的大厦,有4部电梯,乘客的体重:平均70kg,最大120kg,最小40g)。
其他的常量包括:电梯的速度,电梯门开关时间,乘客进出电梯的时间。
大厦的楼层为-1,0,…,20,-1层是地下停车场,1层是大厅。以下是4部电梯的参数:
编号 | 服务楼层 | 乘客人数限制 | 重量限制 |
---|---|---|---|
1 | 所有楼层 | 10 | 800kg |
2 | 1-10 | 10 | 800kg |
3 | -1,1-10 | 20 | 1600kg |
4 | -1,1,11-20 | 20 | 2000kg |
注意: 以上参数信息是可以修改,即电梯调度程序可以在初始化时读取配置信息来设置上述参数值。
二.时间预估表(PSP)
时间预估表(PSP)如下:
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 60 | 40 |
·Estimate | ·估计这个任务需要多少时间 | 60 | 40 |
Development | 开发 | 2400 | 21(day) |
·Analysis | ·需求分析 (包括学习新技术) | 720 | 10 (day) |
·Design Spec | ·生成设计文档 | 120 | 240 |
·Design Review | ·设计复审 (和同事审核设计文档) | 30 | 30 |
·Coding Standard | ·代码规范 (为目前的开发制定合适的规范) | 60 | 30 |
·Design | ·具体设计 | 180 | 240 |
·Coding | ·具体编码 | 960 | 10(day) |
·Code Review | ·代码复审 | 90 | 150 |
·Test | ·测试(自我测试,修改代码,提交修改) | 240 | 360 |
Reporting | 报告 | 240 | 240 |
·Test Report | ·测试报告 | 120 | 100 |
·Size Measurement | ·计算工作量 | 30 | 20 |
·Postmortem & Process Improvement Plan | ·事后总结, 并提出过程改进计划 | 90 | 30 |
合计 | 2700 | 1520+21(day) |
我们在看到项目需求后,认为项目中除了需要实现调度策略的算法外,还需要实现一个可视化界面来呈现调度的效果,根据我们以往学习过的课程,计划使用C++(开发工具为Qt Creator)或C#(开发工具为Visual Studio)实现,我们认为Qt的使用和编码相对更加复杂,且以我们的能力呈现的效果可能不及使用C#进行编码的效果,因此我们决定采用C#作为编程语言。
在最终编制PSP表时,我们发现很难准确统计每一项的用时到分钟级别,许多项目(如开发)只能根据回忆,统计这一部分大概有多少天在进行,因此最终花费的时间的统计很可能并不十分准确。
三.类与接口定义
最终实现的电梯类和乘客类定义如下。电梯类elevator存储电梯的各个信息,包括电梯最大限重,载重,当前楼层,目标楼层,上下一层的时间,开关门时间,电梯状态等参数,以及控制电梯运行的函数move(),控制电梯门开关的函数work(),控制电梯运行方向的函数handle()。乘客类Passenger存储乘客的信息,包括当前楼层,目的楼层,体重,进出电梯所需时间等。
在编程实现的过程中我们发现,由于时间不足,我们并未实现换乘的模拟,电梯在运行过程中关注的仅仅是乘客的体重,出发楼层和目标楼层,因此我们在代码实现中隐去了乘客类,仅仅采用随机生成重量,在电梯对象中存储目的地列表和对电梯进行承载人数和重量限制来模拟乘客。但是我们保留了乘客类的定义,以在未来设计换乘路线的过程中使用。
电梯类:
class elevator{
private int statement = 0;//运行状态,上行为1,下行为-1,无工作为0
private bool doorisopen;//电梯门状态,true为开
private int currentfloor = 1;//当前楼层
private int nextstation;//下一站
private int doortime;//开关门时间
private int floortime;//上下一层楼时间
private int mweight;//最大载重
private int nweight=0;//电梯载重
private int mnum;//最大人数
private int nnum=0;//电梯中的人数
public int[] servefloor = new int[21];//服务楼层
public int[] requestfloor = new int[21];//到某目的楼层的人数
public int[] floorweight = new int[21];//到某目的楼层的乘客总重量
public int[] orderfloor = new int[21];//某楼层要乘电梯的人数
public int Statement { get => statement; set => statement = value; }
public bool Doorisopen { get => doorisopen; set => doorisopen = value; }
public int Currentfloor { get => currentfloor; set => currentfloor = value; }
public int Nextstation { get => nextstation; set => nextstation = value; }
public int Mweight { get => mweight; set => mweight = value; }
public int Mnum { get => mnum; set => mnum = value; }
public int Nweight { get => nweight; set => nweight = value; }
public int Nnum { get => nnum; set => nnum = value; }
public ArrayList orderlist = new ArrayList();//命令数组,即需要前往的楼层数组
static readonly object locker = new object();//数据锁
};
乘客类:
class passenger{
private int pid;//乘客编号
private int pweight;//乘客重量,暂时均设为public
private int nowfloor;//当前电梯出发楼层
private int tofloor;//当前电梯目标楼层
private int elenum;//当前选择电梯,按照0 1 2 3进行管理,输入为1 2 3 4
private bool direct;//当前运动方向
private int[,] path = new int[4, 21]; //创建二维数组,存储路径
public Passenger(int id, int weight, int start, int end;
};
接口定义如下:
类 | 接口 | 函数说明 |
---|---|---|
elevator | Elevator() | 构造函数 |
Setfloortime() | 设置上下楼层时间 | |
Getfloortime() | 获取上下楼层时间 | |
Listen_1() | 监听命令数组,实现BUS调度算法 | |
Listen_2() | 监听命令数组,实现FCFS调度算法 | |
Listen_3() | 监听命令数组,实现LOOK调度算法 | |
Work_2() | 实现FCFS算法中调用,控制电梯向某个方向运动和开关门 | |
Work_3() | 实现LOOK算法中调用,控制电梯向某个方向运动和开关门 | |
DoOrder_3() | 实现LOOK算法中调用,调用Work_3函数驱动电梯 | |
Passenger | Getpid() | 获取乘客编号 |
Setpid() | 设置乘客编号 | |
Getpweight() | 获取乘客体重 | |
SetPweight() | 设置乘客体重 | |
Gettofloor() | 获取乘客目的地 | |
Settofloor() | 设置乘客目的地 | |
Getelenum() | 获取乘客当前乘坐电梯号 | |
Setelenum() | 设置乘客当前乘坐电梯号 | |
Setpath() | 设置乘客路径 | |
Form1 | Form1_Load() | 初始化4个电梯对象:设置限制楼层,并设置电梯基本参数 |
InsideOrder() | 处理电梯内部的楼层命令,电梯对象和目标楼层为参数 | |
OutSideOrder() | 处理电梯外部即不同楼层的命令,寻找最优的电梯,目标楼层和需求方向为参数 | |
button_Click() | 这些按钮是用来响应在界面上不同按钮的点击效果,主要包含开关门、警报、内部楼层需求、外部楼层需求,不同的按钮函数内部调用不同的函数处理 | |
timer1_Tick() | 计时器1:更新显示五个电梯的电梯状态,模拟电梯运动 | |
timer2_Tick() | 计时器2:更新显示五个电梯的电梯的门的状态,电梯内人数,电梯载重 |
四.问题回答
3.1 如何提供足够多的信息给调度器,以便顺利完成调度?
我们在电梯类内设置了目标楼层数组Servefloor,用于存储电梯要去的各个楼层;设置了电梯状态status,用于表示电梯此时的运动方向和状态;设置了参数currentfloor用于表示电梯当前的楼层。
3.2 实际驱动电梯的组件是什么?
我们设置了一组函数work()和Listen(),其中Work()实现控制电梯按照输入的方向移动,Listen()读取需求信息,控制电梯前进。
3.3 如何规定乘客行为?例如当乘客需要从3层到20层时,但电梯不能直达,乘客该如何行动?
根据需求,电梯的性能如下:
编号 | 服务楼层 | 乘客人数限制 | 重量限制 |
---|---|---|---|
1 | 所有楼层 | 10 | 800kg |
2 | 1-10 | 10 | 800kg |
3 | -1,1-10 | 20 | 1600kg |
4 | -1,1,11-20 | 20 | 2000kg |
可以看到几台电梯的性能不同,每台电梯有不同的抵达区间。我们考虑了现实生活中的情况,日常生活中人们乘坐的电梯一般会说明电梯的可达楼层,所以一般来说乘客在发出乘坐电梯请求时,也相应地能够选择那些能够到达目的地的电梯。因此在项目中我们考虑直接规定乘客乘坐的电梯,以及乘客的路线。由于实现的时间不足,我们规定至少有一部电梯能够抵达所有的楼层,在设置乘客相关信息时认为乘客不换乘,规定的乘客路线是可以由选择的电梯完成的。在未来的改进中会尝试对乘客的路线进行规划,起到模拟乘客换乘的效果,但换乘路线依旧进行人工设置。
下图为模拟乘客进入电梯行为的流程图。
五.关键函数设计
我们实现了三个调度算法,即BUS调度,FCFS调度和LOOK调度,三个调度方法用伪代码表示如下:
BUS调度算法:
if(没有命令){
空闲状态
}
else{
检查命令数组,消除重复的楼层
if(目标楼层>当前楼层){
上行状态
while(当前楼层<20) {
上一层楼
if(到达目标楼层){
处理人数和重量
}
开关门
}
继续监听命令数组
}
else if(目标楼层<当前楼层){
下行状态
while(当前楼层>0) {
下一层楼
if(到达目标楼层){
处理人数和重量
}
开关门
}
继续监听命令数组
}
else 空闲状态
}
FCFS调度算法:
if(没有命令){
空闲状态
}
else{
检查命令数组,消除重复的楼层
if(目标楼层!=当前楼层){
取下一目标楼层为命令数组第一个
计算当前楼层与目标楼层的距离
while(电梯没到达目标楼层){
判断电梯应该上行还是下行
根据运行方向电梯移动一层
检查命令数组,消除重复的楼层
if(目标楼层=当前楼层){
把目标楼层从命令数组删除
}
}
if(目标楼层=当前楼层){
开关门
处理人数和重量
}
继续监听命令数组
}
else{
停止状态
把目标楼层从命令数组删除
开关门
}
}
LOOK调度算法:
if(没有命令){
空闲状态
}
else{
对命令数组排序
检查命令数组,消除重复的楼层
if(目标楼层>当前楼层){
上行状态
目标楼层为命令数组第一个
计算目标楼层与当前楼层距离
while(没到目标楼层){
上一层楼
监听命令数组
检查命令数组,消除重复的楼层
if(收到比目标楼层更近的请求){
更改目标楼层与距离
}
if(目标楼层=当前楼层){
把目标楼层从命令数组删除
}
}
if(目标楼层=当前楼层){
处理人数和重量
}
继续监听命令数组
}
else if(目标楼层<当前楼层){
下行状态
目标楼层为命令数组最后一个
计算目标楼层与当前楼层距离
while(没到目标楼层){
下一层楼
监听命令数组
检查命令数组,消除重复的楼层
if(收到比目标楼层更近的请求){
更改目标楼层与距离
}
if(目标楼层=当前楼层){
把目标楼层从命令数组删除
}
}
if(目标楼层=当前楼层){
处理人数和重量
}
继续监听命令数组
}
else{
停止状态
把目标楼层从命令数组删除
开关门
}
}
六.Information Hiding, Interface Design, Loose Coupling
4.1 Information Hiding —— 信息隐藏
信息隐藏是模块化设计中的一项重要原则,指的是把数据结构和实现过程放在一起,使得相关内容彼此靠近,对外提供相对完整,独立的功能,对隐藏信息的访问只能通过接口进行操作。信息隐藏提高了软件的可修改性和重用性,因为修改涉及的是模块内部,避免了与外界的交互,这样使得修改的影响面局限于一个较小的范围内。
在结对编程中,Elevator类和Passenger类中的大部分参数都设置为私有,必须通过设置的接口函数才能进行改动。
4.2 interface design —— 接口设计
接口(软件类接口)是指对协定进行定义的引用类型。其他类型实现接口,以保证它们支持某些操作。接口指定必须由类提供的成员或实现它的其他接口。与类相似,接口可以包含方法、属性、索引器和事件作为成员。
4.3 loose coupling —— 松耦合
耦合度是指模块间的紧密程度,耦合度越低,模块之间的紧密程度就越松散,模块独立性就越强。在考虑模块耦合度时,应遵循“尽量使用数据耦合,少用控制耦合,限制公共耦合范围,坚决避免使用内容耦合的原则”。
在结对编程中,各个模块,函数之间传递的多为基本数据变量,以数据耦合为主,但是在电梯类当中依然存在一些如数组,列表等数据结构为公共信息,采用了公共耦合。
七.UML图
用UML图表示,使用visual studio生成的类图如下:
八.Design by Contract与Code Contract
代码协定通常称作契约式编程,包括如下三个部分:
1.前置条件(precondiction):为了调用函数,必须为真的条件,在其违反时,函数决不调用,传递好数据是调用者的责任。
2.后置条件(postcondion):函数保证能做到的事情,函数完成时的状态,函数有这一事实表示它会结束,不会无休止的循环
3.类不变项(class invariant):从调用者的角度来看,该条件总是为真,在函数的内部处理过程中,不变项可以为变,但在函数结束后,控制返回调用者时,不变项必须为真。
九.程序的代码规范与异常处理
1.达成的代码规范
1.变量和控件的命名不能随意,对于有一定功能的控件,变量和函数我们采用对应的英文翻译命名
2.在重要函数和命名意义不太明确的变量后写明详细注释
3.格式要符合规范,缩进换行等要严格把控。
2.异常处理
1.由于大厦的楼层为-1,1,…,20,没有0层。我们使用一个数组来表示电梯的服务楼层servefloor[21],则-1层的表示可能越界,因此我们采用数组下标0表示-1层;其余数组下标均与楼层对应,例如下标2则表示楼层2层。
十.界面设计
1.左侧有为电梯内部视图,从左到右为1号,2号,3号,4号四部电梯,显示四部电梯的内部按键和运行情况。每部电梯都有20个楼层按钮、两个开关门按钮。蓝色的格子用来表示电梯,可以上下移动,模拟电梯的上下运行。每一部电梯的下部实时显示着当前的楼层数,电梯门的状态,当前电梯内的人数和重量等参数。
在每次电梯停下后,需要点击各部电梯内部视图下方的关门键驱动电梯继续运行。
2.右侧表示电梯外部视图,属于总控制台,除了顶层只有下行键和底层只有上行键,其他楼层都有上行键和下行键。 点击某一层的上/下行键后会自动分配一台电梯来到对应层,通过停留乘客上下的时间来模拟乘客进入电梯,之后点击电梯内部视图的各个楼层可以控制电梯前往目标楼层,在目标楼层停留乘客上下的时间模拟乘客离开电梯。
3.中间部分为读取和设置电梯参数部分,在这一部分下方的三个按钮可以选择四部电梯的运行算法,1对应采用BUS调度算法,2对应采用FCFS调度算法,3对应采用LOOK调度算法,在其下方可以实时显示电梯的运行时间。在上方的文本框部分,可以通过输入相应的数字实现对某部电梯的参数设置,其中可达楼层和不可达楼层之间每个数字之间使用逗号分隔输入,运行速度即一部电梯移动一层楼所需时间,停留时间为乘客上下电梯的用时。在输入电梯编号后可以通过点击读取按钮显示对应电梯的各项信息。
十一.界面模块与其他模块的对接
将代码分为界面、核心、控制器:
1.界面:即程序的界面,包括Form1的一部分和相关联的文件。
2.核心:定义Elevator类,Passenger类的属性和函数。
3.控制器:Form1的一部分,调用核心模块实现模拟电梯运行,同时调用界面模块显示电梯运行效果。
实现的功能截图如下:
十二.代码性能分析
在完成项目的首个版本后进行代码性能分析,采用Visual studio 2019的调试->性能探查器进行性能分析
CPU使用情况如下:
通过性能测试结果和调用树可以看到,占用CPU资源较多的部分为UI,JIT和内核,在程序刚刚加载的阶段耗用CPU资源略多。
GPU使用情况如下:
由于本项目的程序界面较为简单,可以看到GPU的使用率长期保持在1%左右的状态,无太大波动
内存使用情况如下:
可以看到进程内存的使用率在39MB左右
十三.代码质量分析
使用visual studio的代码质量分析工具进行代码质量分析,具体流程为分析->运行Code Analysis->对项目运行代码分析
分析完成无警告的截图如下:
十四.结对编程
1.关于结对编程
每人在各自独立设计、实现软件的过程中不免要犯这样那样的错误。在结对编程中,因为有随时的复审和交流,程序各方面的质量取决于一对程序员中各方面水平较高的那一位。这样,程序中的错误就会少得多,程序的初始质量会高很多,这样会省下很多以后修改、测试的时间。结对编程具有以下优点:
(1)在开发层次,结对编程能提供更好的设计质量和代码质量,两人合作能有更强的解决问题的能力。
(2)对开发人员自身来说,结对工作能带来更多的信心,高质量的产出能带来更高的满足感。
(3)在心理上, 当有另一个人在你身边和你紧密配合, 做同样一件事情的时候, 你不好意思开小差, 也不好意思糊弄。
(4)在企业管理层次上,结对能更有效地交流,相互学习和传递经验,能更好地处理人员流动。因为一个人的知识已经被其他人共享。
总之,如果运用得当,结对编程能得到更高的投入产出比(Return of Investment)。
2.个人优缺点:
王梓屹:
- 优点:
- 对算法有较好的理解
- 善于解决编程过程中遇到的技术问题以及出现的bug
- 较为细心
- 缺点:
- 缺乏对项目的整体规划能力
陈泓铭:
- 优点:
- 对本次结对编程中用到的各种工具较为了解
- 对项目的整体架构有一定的规划
- 能够从多角度解决问题
缺点:
- 编程能力相对较弱,容易出现细节错误
- 时间安排能力较差,难以控制进度
在说服伙伴改进缺点的过程中,我们的沟通采用了三明治法则,这样的沟通可以分为三层,即
面包:表达认同、赏识、肯定、关爱对方的优点或积极面
肉片:提出建议、批评或不同观点
面包:给与鼓励、希望、信任、支持和帮助
我们在沟通中,会经常对对方的工作表达认可,支持和鼓励,在需要提出建议时会委婉地表达自己的意见,并相信对方可以做的更好。
3.分工状况
- 王梓屹:
编写类的定义
实现Elevator类
实现Form1类中insideorder和outsideorder函数的编写
实现电梯运动和显示功能
实现三种电梯调度算法(BUS、FCFS、LOOK)
修改BUG
编写设计文档中详细设计部分
测试及编写测试文档
- 陈泓铭:
实现passenger类
实现了乘客的随机生成以及上下电梯后电梯载重和人数的改变
实现电梯参数设置功能
代码整合、模块对接
UI设计
编写博客和设计文档
提交代码、文件到gitlab
4.结对编程的过程
我们在结对编程中采用了驾驶员和领航员的模式,两人交替完成编码的任务,每一部分完成后另一位同学进行代码复审。在项目进行过程中我们主要采用线上交流,并安排了部分线上会议。
十五.感悟与不足
1.收获与感悟
此次项目在编程时遇到了很多困难,经历过很多失败,有过痛苦和挣扎,但也有解决问题时收获的惊喜,但总体上讲这次结对项目让我我收获了很多。在此次项目之前,我对多线程了解很少,刚开始时毫无头绪,查阅了很多的资料才逐渐明白,充分利用Thread的start ()、sleep()等方法,通过这次项目,我加深了对多线程的理解,初步学会了C#语言实现多线程编程的方法。
同时,我们考虑过要求中提到的多种语言,但由于需要设计UI界面,而我们认为C#在UI界面与其他模块的对接,和UI界面的设计方面较为简单且效果较好,所以我们选择了使用c#编写这个程序。虽然我们都只是在小学期简单接触过C#,对C#的语法并不熟悉,但我们并没有轻言放弃,通过查阅资料和自主学习,我们对c#的理解运用也加深了,编程能力也得到了提高。
2.不足
1.每次寻找合适的电梯时都是从第一个电梯开始进行顺序遍历,因此会导致后面的电梯利用率比较低。
2.UI设计有待改进,界面主要是作为操作接口和展示运行状态,因此设计并不美观,在数据的安全性上也有所欠缺。
3.我们在开始时对题目的理解出现了偏差,我们在很长一段时间内希望能够实现一个模拟真正的电梯使用情况的程序,但后来我们逐渐认识到事实上电梯内外的界面更多是起到展示电梯运行情况的作用,但由于时间不足,我们依然继续了原有的思路来实现项目。