软件工程基础大作业:结对项目-电梯调度

一、问题假设

一幢21层的大厦,有4部电梯,乘客的体重:平均70kg,最大120kg,最小40g)。
其他的常量包括:电梯的速度,电梯门开关时间,乘客进出电梯的时间。
大厦的楼层为-1,0,…,20,-1层是地下停车场,1层是大厅。以下是4部电梯的参数:

电梯编号服务楼层乘客人数限制重量限制
1所有楼层10800kg
21-10层10800kg
3-1,1-10层201600kg
4-1,1,11-20层202000kg

注意: 以上参数信息是可以修改,即电梯调度程序可以在初始化时读取配置信息来设置上述参数值。

二、PSP

PSP2.1Personal Software Process Stages预估耗时(分钟)实际耗时(分钟)
Planning计划3030
· Estimate·估计这个任务需要多少时间2020
Development开发15day20day
· Analysis·需求分析(包括学习新技术)200300
· Design Spec·生成设计文档3060
· Design Review·设计复审(和同事审核设计文档)1530
· Coding Standard· 代码规范 (为目前的开发制定合适的规范)2020
· Design· 具体设计6090
· Coding· 具体编码9001200
· Code Review· 代码复审6090
· Test· 测试(自我测试,修改代码,提交修改)120200
Reporting报告180240
· Test Report· 测试报告6060
· Size Measurement· 计算工作量2020
· Postmortem & Process Improvement Plan· 事后总结, 并提出过程改进计划3060
合计(day为8小时)894512020

由于初期对于该项目的低估,导致实际时间多于预期时间,包括开发、需求分析(包括学习新技术)、生成设计文档、具体设计。
由于第一次使用系统的软件工程方法所以在设计复审(和同事审核设计文档)中使用的实际时间多于预期时间。
在第三阶段需求中,我们需要实现实时电梯调度算法,但此算法与我们最初设想的方式有比较大的不同,同时还需要学习该算法,所以所用时间对于预期时间。
在代码复审环节,我们低估了阅读他人代码的难度,即使在约定了代码规范的情况下,我们阅读对方的代码速度还是较慢的。
第一次使用VS2019进行单元测试并无相关经验,并且为了获取测试分支覆盖率,由于VS2019社区版并未提供该功能,所以后使用OpenCppCoverage拓展工具来获取测试分支覆盖率。
由于我们两人第一次使用Markdown来编写相关文档,所以在报告中,还花费时间去学习和熟悉了Markdown的使用。
由于前期对于该项目的低估,导致我们在项目进行时出现了一些小插曲,所以在最后总结上仔细地梳理了一下我们的问题,并自我反思提出改进。

三、Information Hiding, Interface Design, Loose Coupling

Information Hiding——信息隐藏

信息隐蔽是开发整体程序结构时使用的法则,即将每个程序的成分隐蔽或封装在一个单一的设计模块中,定义每一个模块时尽可能少地显露其内部的处理。信息隐蔽原则对提高软件的可修改性、可测试性和可移植性都有重要的作用。
在结对编程的过程中,信息隐藏主要运用于C++的类对象的属性以及部分方法,将需要隐藏的属性和方法设置为private,防止在后续编写代码时直接修改不期望被修改的属性从而造成错误。同时定义接口隐藏信息,所有的隐藏信息都只能通过接口获得,但这些接口仅限于获取不能修改。

Interface Design——接口设计

在使用计算机的过程中,人和计算机是以人机界面为媒介传递信息的。用户通过接口向计算机提供各种数据和命令,让计算机完成指定任务。
在结对编程过程中,所有的类隐藏数据都只能通过接口获取,同时一些简单的方法也是通过一个函数来实现通过接口调用。

Loose Coupling——松耦合

软件工程中对象之间的耦合度就是对象之间的依赖性。指导使用和维护对象的主要问题是对象之间的多重依赖性。对象之间的耦合越高,维护成本越高。因此对象的设计应使类和构件之间的耦合最小。耦合是影响软件复杂程度和设计质量的一个重要因素,在设计上我们应采用以下原则:如果模块间必须存在耦合,就尽量使用数据耦合,少用控制耦合,限制公共耦合的范围,尽量避免使用内容耦合。
在结对编程过程中,大部分的接口相对独立达到数据耦合,但也有少部分存在控制耦合。

四、模块接口的设计与实现过程

类定义

最终定义两个大类即乘客、电梯,二者的属性以及方法以及类之间的关系见第五点UNL图中的类图。

函数定义

最终接口定义如下表:

接口输入参数补充的函数说明
乘客/passenger初始化函数(构造函数)/passenger()体重、最初出发楼层、最终目标楼层
生成乘坐方案/void generateRideScenarios()最初出发楼层、最终目标楼层根据所给的出发楼层和目标楼层生成可行的乘坐方案
获取乘坐方案-目标楼层/int fetchRidePlan_TargetFloor()
设置当前出发楼层/void setCurrentDepartureFloor()当前楼层
设置当前目标楼层/void setCurrentTargetFloor()当前楼层
设置当前乘坐方向/void setCurrentDirection()
设置算法类型/void setType()算法类型编号由于后期设计不同的调度算法,所以可以设置乘客属于哪个算法中的
设置时限/void setDeadline()电梯编号、运行时间FD-SCAN 算法中的时限
添加乘坐电梯/void addRideElevator()电梯编号当乘客乘坐一个电梯后记录下来,方便后续将每个乘客的乘坐电梯信息打印出来
获取乘客体重/int getWeight()
获取乘客编号/int getPassengerNumber()
获取乘客当前出发楼层/int getCurrentDepartureFloor()
获取乘客当前目标楼层/int getCurrentTargetFloor()
获取乘客当前乘坐方向/int getNowDirection()
获取算法类型/int getType()
获取时限/int getDeadline()电梯编号
延长时限/void delayDeadline电梯编号、运行次数
判断是否到达/bool judgeArrived()在乘客下电梯时用于判断是否到达最终目标楼层
生成记录/string genRecord()打印出该乘客的乘坐记录
清空乘客数量/static void resetPassengerCount()将总乘客数量置零
电梯/elevator初始化函数(构造函数)/elevator()电梯编号、服务楼层、人数限制、重量限制
生成邻接矩阵/void generatesAdjacencyMatrix()服务楼层可直接到达的楼层标记为1,不可为0,用于乘客生成乘坐方案
向上或向下运行一层楼/void runOneFloor()
获得时间/int getTime()
获取运行状态/int getRunningStatus()
获取最大承载人数/int getMaximumNumberPassengers()
获取最大承载重量/int getMaximumLoadWeight()
获取当前承载人数/int getCurrentNumberPassengers()
获取当前承载重量/int getCurrentLoadWeight()
获取当前楼层(0->0)/int getCurrentFloor()
获取当前楼层用于界面显示(0->-1)/int getCurrentFloorToShow()由于数组限制,-1层无法用对于下标表示,故选择使用0下标表示-1层,但展示时需将0下标转换为-1层
判断当前楼层是否可服务/bool judgeServiceFloor()楼层编号
判断是否有乘客进入电梯/bool judgePassengersEnterElevator()
判断是否有乘客离开电梯/bool judgePassengersLeaveElevator()
判断是否在该层停靠开门/bool judgeStop()
判断是否继续运行/bool judgeKeepRun()
判断是否时限最早/bool judgeDeadlineNearest()时限FD-SCAN 算法需要
判断从当前位置开始移动是否可以满足其时限要求/bool judgeFeasible()时限、出发楼层、目标楼层FD-SCAN 算法需要
根据当前电梯里面和外面的队列中最小的deadline决定运行方向/calRunningStatus()FD-SCAN 算法需要,据当前电梯里面和外面的队列中最小的deadline决定运行方向,找到最早的那个乘客,乘客在外面等就朝这个乘客运行,乘客在电梯里面就朝乘客目的地运行
乘客进入电梯/void passengersEnterElevator()乘客
乘客离开电梯/passenger* passengersLeaveElevator()
判断是否超重/bool judgmentSuperHeavy()
判断是否超载/bool judgeOverload()
设置运行状态/void setRunningStatus()状态标识
设置服务楼层/void setServiceFloor()需修改楼层编号列表
设置最大承载人数/void setMaximumNumberPassengers()人数限制
设置最大承载重量/void setMaximumLoadWeight()重量限制
设置内部按钮/void setInternalButtons()按钮标识、楼层
设置外部按钮,0表示向下,1表示向上/void setExternalButtons()按钮标识、楼层、方向
其他初始化全局邻接矩阵/void initializeAdjacencyMatrix()
乘客加入楼层电梯队列/void passengerJoinElevatorsFloorLine()调度算法类型、当前楼层、电梯编号、乘客、目标方向
超重乘客加入楼层电梯队列/passengerSuperHeavyJoinElevatorsFloorLine()当前楼层、电梯编号、乘客、目标方向先进先出原则,如果超重或超载则退出电梯,但下次优先进入电梯。超载情况不判断,因为一旦超载就关闭电梯门,但超重则会让后续乘客进行尝试
乘客退出楼层电梯队列/void passengersExitElevatorsFloorLine()调度算法类型、当前楼层、乘客编号、目标方向
超重乘客排队队列转移到正常乘客排队队列/void changePassengerQueueup()电梯编号 、当前楼层、目标方向当下次电梯来时,首先让上次超重乘客上电梯

关键函数

1.电梯运行函数

电梯如何运行的大概思想目前有两种,一为永不停歇的方式,每台电梯在所服务的楼层一直循环移动,并且在每一层都停下让乘客上下电梯即为BUS算法;二为电梯运行时存在一个方向,如若该方向所能到达楼层还有乘客是同向的则让其上电梯而不去管非同向的乘客。当此方向到头即电梯内无乘客时,电梯在该楼层保持静止。在这两种方法的基础之上加上操作系统磁盘寻到调度方法的SCAN和LOOK变为四种方法,四种方法的伪代码如下图:
电梯运行函数伪代码

五、UML图

功能模型——用例图

用例图

静态模型——类图

类图

动态模型——泳道图

泳道图

六、Design by Contract, Code Contract

Design by Contract——契约式设计

契约式设计或者Design by Contract (DbC)是一种设计计算机软件的方法。这种方法要求软件设计者为软件组件定义正式的,精确的并且可验证的接口,这样,为传统的抽象数据类型又增加了先验条件、后验条件和不变式。这种方法的名字里用到的“契约”或者说“契约”是一种比喻,因为它和商业契约的情况有点类似。
契约式设计包括以下优点:

1.获得更优秀的设计
更系统的设计 契约式设计鼓励程序员思考诸如“例程的先验条件是什么”这样的问题,这	样有助于程序员理清概念。
更清楚的设计 使用者和提供者之间的权利和义务得到了共享,同时获得了清晰的描述。
更简单的设计 程序的先验条件清楚地描述了使用该程序的限制,而且非法调用的结果也很清楚,所以我们鼓励程序员不要开发过于通用的程序,而要设计小巧的、目标专一的例程。
控制对继承的使用 例如,当要用到多态性和动态绑定时,检查契约可以确保重定义例程的先验条件在子类中不会被加强。
系统地运用异常 当例程被非法使用(先验条件失败)或者例程没有遵循契约的规定(后验条件或者不变式失败)时,就会发生异常。
2.提高可靠性
(1)编写契约可以帮助开发者更好地理解代码
(2)契约有助于测试(契约可以随时关闭或者启用)
3.更出色的文档
(1)契约乃是类特性的公用视图中的固有成分
(2)契约是值得信赖的文档(运行时要检查断言,以保证制定的契约与程序的实际运行情况一致)
(3)契约是精确的规范,同时也可以作为测试的可靠指导
4.简化调试
契约能把错误牢牢地定位
5.支持复用
(1)出色的文档
(2)正确运用了复用代码的运行时检测

1.我们将函数的调用前置条件设置明确,为了调用函数,必须为真的条件,在其违反时,函数决不调用,传递好数据是调用者的责任。
2.每个函数内部都有明确的结束状态,不会无休止的循环。
3.在编码过程中我们并未使用类不变项(class invariant),我们将类是否处于有效状态穿插于各个函数之间。

Code Contract——代码协定

代码协定类使你可以在代码中指定前置条件、后置条件和对象固定。
前置条件是输入方法或属性时必须满足的要求。
后置条件描述在方法或属性代码退出时的预期。
对象固定条件描述了不存在任何条件问题的类的预期状态。对象固定描述处于良好状态的类的预期状态。
代码协定包含用于标记代码的类、用于编译时分析的静态分析器以及运行时分析器。
代码协定包括以下优点:

改进测试:代码协定提供静态协定验证、运行时检查和文档生成。
自动测试工具:可通过过滤掉不满足前置条件的无意义的测试参数,使用代码协定来生成更有意义的单元测试。
静态验证:静态检查器无需运行程序即可决定是否存在任何协定冲突。 它可检查隐式协定(如 null 取消引用和数组绑定)和显式协定。
参考文档:文档生成器扩充具有协定信息的现有 XML 文档文件。 还提供了可与 Sandcastle 一起使用的样式表,因此,生成的文档页具有协定部分。

七、程序的代码规范,设计规范

达成的共识:

(1)变量和控件命名使用驼峰命名法。
(2)变量和控件的命名不能随意,必须能够清晰表示他的功能,英文翻译。
(3)类中每个变量以及函数均写明注释,表达清楚这是什么或功能是什么。对于函数内的临时变量,若具有重要作用或理解较为困难的也写明注释。
(4)采取统一的编码风格即缩进数量,注释方式等等,以降低编码过程中阅读同伴编写的代码的困难度。

程序中存在的异常以及处理方式:

(1)由于大厦的楼层为-1,1,...,20,-1层是地下停车场,1层是大厅。我们使用一个数组来表示电梯的服务楼层serviceFloor[21],显然大厦不可能存在0层,则数组下标0表示-1层;其余数组下标均与楼层号对应,例如下标2则表示楼层2层是否为电梯的服务楼层。
(2)在每个函数内部都对异常的输入、输出或者结果都有进行判断,当出现异常情况时,会终止程序并提示错误信息。

八、界面模块的详细设计过程

界面划分

由于我们需要显示乘客+电梯在系统中的运行情况,并且有专门的状态显示(时间,载客人数,KPI等),所以存在一个电梯调度主界面。并且在该项目的问题假设中,参数信息可以修改,所以我们需要能够界面化设置参数信息,所以存在一个电梯参数初始化界面。同时在第三阶段需求中,我们需要同时展示两个电梯调度的页面,所以之后选择添加一个单独的乘客生成器界面。

电梯参数初始化界面

电梯参数初始化界面
考虑到可能会修改参数,且重新点击这些楼层较麻烦,所以该界面不会随生成主界面而关闭。要修改参数只需要关闭主界面,在参数初始化界面进行修改再点击确认即可
再次修改电梯参数

电梯调度主界面

电梯调度主界面
每部电梯在每一层都有一个上下按钮和楼层显示,还有楼层的数字显示、运行状态显示。电梯内部也有对应楼层的按钮、重量和人数显示、楼层和运行状态显示。
右侧最上方显示该窗口对应的电梯调度算法、乘客总数还有开始、结束按钮,中间为乘客生成器,生成的乘客仅加入该窗口对应的电梯,下方为乘客乘坐方案记录,在乘客到达最终目标楼层后会在这里显示他的全部乘坐记录。
根据电梯运行和生成乘客的动作,会按下或取消对应的按钮,数字显示也会随之改变。
乘客生成器中可以随机生成,也可以手动输入。可以在开始运行前先加入乘客,也可以运行过程中加入乘客。
运行前先加入一些乘客:
加入乘客
运行过程中:
运行过程中
可以看到按钮状态、参数的改变,并且有乘客乘坐方案记录生成。

乘客生成器

由于需要使用相同的参数和乘客模拟数据,同时运行两个调度界面,所以生成了两个参数相同的界面,除了右上角的算法不同。
此外还有一个单独的乘客生成器,这个与电梯调度主界面的区别在于:这个调度的乘客生成器的按钮同时控制两个主界面,生成的乘客也同时加入两个界面;而电梯调度主界面里的乘客生成器和按钮仅针对该界面。
乘客生成器
两个电梯调度主界面和乘客生成器的总界面如下:(生成后会叠放在一起,需要移动一下)
两个电梯调度主界面和乘客生成器的总界面

九、界面模块与其它模块的对接

将代码分为界面模块、核心模块、控制模块:

  1. 界面模块:即实现程序的界面,由Qt实现,包括电梯参数初始化界面
  2. 核心模块:定义电梯类和乘客类的属性和函数
  3. 控制模块:将界面模块和核心模块组合起来,修改调用核心模块进行判断、修改参数,同时调用界面模块修改界面的显示。

项目实现的功能截图如下:
两个电梯调度主界面和乘客生成器的总界面

十、代码质量分析

使用的软件分别为Visual Studio 2019和Qt Creator 4.8.0 Based on Qt 5.12.0 (MSVC 2015, 32 bit)。

Visual Studio 2019

Visual Studio 2019代码质量分析工具分析的方法为:分析->运行Code Analysis->针对解决方案

VS2019无警告的结果图如下:
VS2019代码质量分析

Qt Creator 4.8.0 Based on Qt 5.12.0 (MSVC 2015, 32 bit)

QT无警告的结果图如下:
QT代码质量分析

十一、测试

单元测试

VS2019进行单元测试的具体方法参考VS2019 c++单元测试
在获得项目首个版本之前,以及实现不同电梯调度算法时都对电梯调度较为关键的四个函数和一个乘客中一个关键的函数进行了至少十个测试用例的单元测试,对于其他电梯和乘客中较为简单的函数分别进行了至少一个测试用例的单元测试但能够确保程序能正确处理各种情况,电梯的四个函数分别为judgeStop(判断是否在该层停靠开门)、judgeKeepRun(判断是否继续运行)、judgePassengersEnterElevator(判断是否有乘客进入电梯)、judgePassengersLeaveElevator(判断是否有乘客离开电梯),乘客的函数为generateRideScenarios(生成乘坐方案)。其中由于FD-SCAN算法的复杂性,为了确保算法正确,还单独为FD-SCAN算法涉及的。。。个函数进行了单元测试。
模块的单元测试代码见总文件中的UnitTest1.cpp。具体说明见测试文档。
同时由于VS2019社区版中并未提供获取单元测试的测试分支覆盖率指标的工具(该工具在企业版中),所以之后使用VS2019的OpenCppCoverage拓展工具来获取,具体方法参考OpenCppCoverage拓展工具
单元测试的测试分支覆盖率指标如下图:
测试分支覆盖率

集成测试

测试方法

写了一个不具有复用性的bool runElevator(passenger* p)函数//运行电梯。

在函数内分别对三种算法(BUS、LOOK、FD-SCAN)进行运行,根据电梯的运行类型选择。根据集成测试的概念,在单元测试的基础上,将所有模块按照设计要求组装成为子系统或系统,进行集成测试。用runElevator函数完成所有模块的组装,同时运用单元测试的方法,创建三个TEST_METHOD分别对应三个不同的算法。

BUS算法集成测试

测试用例:

电梯1,服务于1~4层和8层,初始位于1层

乘客1,出发楼层4,目标楼层1

进行一次乘客完整的上下电梯以及电梯运行

期望结果:

乘客1按下4层向下的电梯按钮。电梯一直运行到4层,发现有人要上电梯,这时打开电梯门让乘客上电梯。乘客1进入,按下1层按钮,但此时电梯不会立马改变方向,而是保持原方向继续运行直到达到该方向的服务楼层尽头才改变方向。最后向下运行到达1层时,乘客1下电梯。

实际结果:
BUS算法实际结果

LOOK算法集成测试

测试用例:

电梯1,服务于1~4层和8层,初始位于1层

乘客1,出发楼层4,目标楼层1

进行一次乘客完整的上下电梯以及电梯运行

期望结果:

乘客1按下4层向下的电梯按钮。电梯发现有人要上电梯,这时朝着乘客方向运行,直到到达4层。乘客1进入,按下1层按钮,此时电梯会立马改变方向,这是LOOK算法与BUS算法的不同之处。最后向下运行到达1层时,乘客1下电梯。

实际结果:
LOOK算法实际结果

FD-SCAN算法集成测试

测试用例:

电梯1,服务于1~4层和8层,初始位于1层

乘客1,出发楼层4,目标楼层1

进行一次乘客完整的上下电梯以及电梯运行

期望结果:

乘客1按下4层向下的电梯按钮。电梯每运行一次根据最小时限计算一次方向,得到向上运行,一直运行到4层,计算方向为向下,且该层向下有乘客按下按钮,则打开电梯门让乘客上电梯。乘客1进入,按下1层按钮。电梯计算运行方向,为向下,则开始向下运行。电梯每运行一次计算一次方向,得到向下运行,一直运行到1层时,乘客1下电梯。

实际结果:
FD-SCAN算法实际结果

测试结果

三个TEST_METHOD的测试结果如下图:
集成测试

系统测试

讨论BUS算法和LOOK算法

电梯速度为运行一层楼花费1s,开关门时间为200ms,一名乘客上下电梯的时间为100ms,体重单位为kg。

电梯参数设置如下:

电梯编号服务楼层初始楼层人数限制重量限制
0-1~111101000
19~179101000
21和18~20131000
3-1~111110400
  • 测试用例1:

    测试输入:

    开始时:

    乘客1,体重45,起始楼层10,目标楼层17

    乘客2,体重50,起始楼层1,目标楼层10

    乘客3,体重55,起始楼层13,目标楼层10

    期待输出(按照结束的先后顺序):

    BUS:

    乘客1:10->电梯1->17

    乘客2:1->电梯0->10

    乘客3:13->电梯1->10

    实际运行图片如下:
    BUS1

    LOOK:

    乘客1:10->电梯1->17

    乘客2:1->电梯0->10

    乘客3:13->电梯1->10

    实际运行图片如下:
    LOOK1

    虽然两种调度方法期望结果相同并且图中两种算法方向相同,但是从实际运行中可以看出,由于BUS算法在每一层都会停靠开门,所以在时间上会比LOOK算法差。

  • 测试用例2:

    测试输入:

    开始时:

    乘客1,体重80,起始楼层7,目标楼层9

    乘客2,体重70,起始楼层6,目标楼层19

    期待输出(按照结束的先后顺序):

    BUS:

    乘客1:7->电梯0->9

    乘客2:

      6->电梯3->1
      1->电梯2->19
    

    实际运行图片如下:
    BUS2

    LOOK:

    乘客1:7->电梯0->9

    乘客2:

      6->电梯3->1
      1->电梯2->19
    

    实际运行图片如下:
    LOOK2

    虽然两种调度方法期望结果相同,但是从实际运行中由于BUS算法会在服务楼层之间一直运行,而LOOK算法遇到请求时会立即改变方向,当乘客2乘坐电梯3来到1楼按下电梯2向上的按钮之后,LOOK算法会直接去接乘客2,而BUS算法会继续保持原先的方向运行即使无人在电梯上,所以导致BUS算法的乘客2所耗费的时间远多于LOOK算法。

  • 测试用例3:

    测试输入:

    开始时:

    乘客1,体重20,起始楼层1,目标楼层9

    乘客2,体重40,起始楼层18,目标楼层2

    乘客3,体重50,起始楼层14,目标楼层4

    期待输出(按照结束的先后顺序):

    BUS:

    不定

    实际运行图片如下:
    BUS3

    LOOK:

    乘客1:1->电梯0->9

    乘客3:

      14->电梯1->9
      9->电梯0->4
    

    乘客2:

      18->电梯2->1
      1->电梯0->2
    

    实际运行图片如下:
    LOOK3

    由于BUS算法中电梯一直处于运行状态,所以对于情况较为不一样时,结果会随着乘客出现的时机而改变,因为乘客的选择多样,所以BUS算法中哪一部电梯先到达是不可预见的,但LOOK算法中,当无请求时电梯会停止运行,所以每一次的结果是相同的。

  • 测试用例4:

    测试输入:

    开始时:

    乘客1,体重100,起始楼层11,目标楼层9

    乘客2,体重100,起始楼层11,目标楼层9

    乘客3,体重100,起始楼层11,目标楼层9

    乘客4,体重100,起始楼层11,目标楼层9

    乘客5,体重100,起始楼层11,目标楼层9

    期待输出(按照结束的先后顺序):

    BUS:

    乘客1:11->电梯3->9

    乘客2:11->电梯3->9

    乘客3:11->电梯3->9

    乘客4:11->电梯3->9

    乘客5:11->电梯1->9

    实际运行图片如下:
    BUS4

    LOOK:

    乘客1:11->电梯3->9

    乘客2:11->电梯3->9

    乘客3:11->电梯3->9

    乘客4:11->电梯3->9

    乘客5:11->电梯1->9

    实际运行图片如下:
    LOOK4

    由于电梯3的重量限制为400kg,有五名乘客都要从11层前往9层,最先到达11层的可服务电梯是电梯3,但由于超出重量限制,所以只有乘客1/2/3/4上了电梯,乘客五则是得到了另一可服务电梯1。

LOOK算法和FD-SCAN算法

电梯速度为运行一层楼花费1s,开关门时间为200ms,一名乘客上下电梯的时间为100ms,体重单位为kg。

电梯参数设置如下:

电梯编号服务楼层初始楼层人数限制重量限制
0-1~111101000
19~179101000
21和18~20131000
3-1~111110400
  • 测试用例1:

    测试输入:

    开始时:

    乘客1,重量70, 起始楼层1,目标楼层12

    乘客2,重量50, 起始楼层2,目标楼层1

    乘客3,重量60, 起始楼层2,目标楼层4

    期待输出(按照结束的先后顺序):

    look:

    乘客3:2->电梯0->4

    乘客2:2->电梯3->1

    乘客1:

      1->电梯0->9
      9->电梯1->12
    

    实际运行图片如下:
    在这里插入图片描述

    fd-scan:

    乘客2:2->电梯3->1

    乘客3:2->电梯0->4

    乘客1:

      1->电梯0->9
      9->电梯1->12
    

    实际运行图片如下:
    在这里插入图片描述

  • 测试用例2:

    测试输入:

    开始时:

    乘客1,重量70, 起始楼层1,目标楼层12

    乘客2,重量60, 起始楼层2,目标楼层6

    在电梯0向上运行到楼层3的时候:
    乘客3,重量50, 起始楼层2,目标楼层1

    期待输出(按照结束的先后顺序):

    look:

    乘客2:2->电梯0->6

    乘客3:2->电梯3->1

    乘客1:

      1->电梯0->9
      9->电梯1->12
    

    实际运行图片如下:
    在这里插入图片描述

    fd-scan:

    乘客3:2->电梯0->1

    乘客2:2->电梯0->6

    乘客1:

      1->电梯0->9
      9->电梯1->12
    

    实际运行图片如下:
    在这里插入图片描述

  • 测试用例3:

    测试输入:

    开始时:

    乘客1,重量40, 起始楼层19,目标楼层1

    乘客2,重量41, 起始楼层19,目标楼层-1

    乘客3,重量120, 起始楼层19,目标楼层12

    乘客4,重量119, 起始楼层19,目标楼层18

    期待输出(按照结束的先后顺序):

    look:
    乘客1:19->电梯2->1

    乘客2:

      19->电梯2->1
      1->电梯0->-1
    

    乘客3:

      19->电梯2->1
      1->电梯0->9
      9->电梯1->12
    

    乘客4:19->电梯2->18

    实际运行图片如下:
    在这里插入图片描述
    在这里插入图片描述

    fd-scan:

    乘客4:19->电梯2->18

    乘客1:19->电梯2->1

    乘客2:

      19->电梯2->1
      1->电梯0->-1
    

    乘客3:

      19->电梯2->1
      1->电梯0->9
      9->电梯1->12
    

    实际运行图片如下:
    在这里插入图片描述
    在这里插入图片描述

  • 测试用例4:

    测试输入:

    开始时:

    乘客1,重量40, 起始楼层19,目标楼层1

    乘客2,重量41, 起始楼层19,目标楼层-1

    乘客3,重量120, 起始楼层19,目标楼层12

    在电梯3向下运行到楼层18的时候:
    乘客4,重量119, 起始楼层19,目标楼层18

    期待输出(按照结束的先后顺序):

    look:

    乘客1:19->电梯2->1

    乘客2:

      19->电梯2->1
      1->电梯0->-1
    

    乘客3:

      19->电梯2->1
      1->电梯3->9
      9->电梯1->12
    

    乘客4:19->电梯2->18

    实际运行图片如下:
    在这里插入图片描述
    在这里插入图片描述

    fd-scan:

    乘客1:19->电梯2->1

    乘客2:

      19->电梯2->1
      1->电梯3->-1
    

    乘客3:

      19->电梯2->1
      1->电梯0->9
      9->电梯1->12
    

    乘客4:19->电梯2->18

    实际运行图片如下:
    在这里插入图片描述
    在这里插入图片描述

十二、性能分析

性能分析工具

使用Visual Studio 2019内置的Profiling Tools进行性能分析。

CPU使用情况

分析

CPU使用情况如下图:
CPU使用情况
在程序运行过程中CPU最高使用率为13%,平均为9%。

从饼状图可以看出,UI和内核占了绝大部分,同时UI是占比最多,虽然内核占比也较高,但是UI是内核的快两倍。

从调用树中也可以看出该情况:
调用树1
调用树2

原因

由于我们的UI界面的设计,在程序运行时会同时存在多个界面,正常情况会存在四个界面,分别为一个电梯参数初始化界面、两个电梯调度主界面以及一个乘客生成器。并且电梯调度主界面是较为复杂的,所以导致UI使用CPU情况较为严重。

优化

所以我们可以从UI界面进行优化,通过简化UI界面来对程序的性能进一步提升。不过由于时间有限,所以并未做出优化,也是此次项目的一个遗憾。

GPU

分析

CPU使用情况如下图:
GPU使用情况
从图中可以看出,仅仅出现了一次较高的峰值,从体来说GPU的使用率是比较低的。

十三、结对编程

结对过程

结对过程中,为了提高进度,减少交叉编程带来的复杂性,我们首先进行了对于项目的讨论,在将项目分为两大部分,分别实现类和接口的定义以及界面的实现。在之后的编程过程中将两大部分再划分为多个小部分,在完成一个小部分的时候就与同伴进行交流,避免分开编程导致后期无法结合。同时期间每隔几天进行线上讨论,提出各自新的想法或看法等。

合作分工

我们两人共同参与分析、设计、编码、测试。合作过程中由吴泽学负责实现类和接口的编码,陈星宇负责界面的实现的编码。在之后的阶段需求当中,由于界面的实现以及该项目的具体实现的需要,项目的具体实现的需要是需要实现类之间的信息传递,无法使用VS2019完成,所以导致陈星宇的代码工作量过大,其中包括LOOK算法和FD-SCAN算法的具体实现。所以陈星宇主要负责界面的实现、项目的具体实现、BUS算法、LOOK算法和FD-SCAN算法的分析与具体实现同时FD-SCAN算法的单元测试和集成测试,以及软件相关文档中关于以上内容的编写。吴泽学主要负责实现类和接口、BUS算法和LOOK算法的分析、单元测试和集成测试以及其他的单元测试、大部分的博客撰写、大部分的文档编写其中还包括运行说明和性能分析报告。结对后期两人共同参与类和接口的更改以及性能分析,不过由于时间有限,并未对可优化部分进行调优。

由于最终合并分支出现冲突,可能会覆盖掉吴泽学的commit,所以以下附上吴泽学分支的commit。
吴泽学的commit

结对照片

下面是我们两人在讨论的结对照片,并附上使用QQ软件进行讨论的截图。
讨论的结对照片
讨论截图1
讨论截图2

合作方式

我们采取的合作方式是在不同部分的驾驶员和领航员。驾驶员(Driver)是控制键盘输入的人。领航员(Navigator)起到领航、提醒的作用。因为我们将项目分为两个大部分,分别进行编程,在编程过程中为避免后期出错,所以在各自的部分中我们都担任着驾驶员的角色,而在对方的部分中我们都担任着领航员的角色。

结对编程的优点

每人在各自独立设计、实现软件的过程中不免要犯这样那样的错误。在结对编程中,因为有随时的复审和交流,程序各方面的质量取决于一对程序员中各方面水平较高的那一位。这样,程序中的错误就会少得多,程序的初始质量会高很多,这样会省下很多以后修改、测试的时间。具体地说,结对编程有如下的好处:
(1)在开发层次,结对编程能提供更好的设计质量和代码质量,两人合作能有更强的解决问题的能力。
(2)对开发人员自身来说,结对工作能带来更多的信心,高质量的产出能带来更高的满足感。
(3)在心理上, 当有另一个人在你身边和你紧密配合, 做同样一件事情的时候, 你不好意思开小差, 也不好意思糊弄。
(4)在企业管理层次上,结对能更有效地交流,相互学习和传递经验,能更好地处理人员流动。因为一个人的知识已经被其他人共享。
总之,如果运用得当,结对编程能得到更高的投入产出比(Return of Investment)。

结对编程的缺点

(1)与合不来的人一起编程容易发生争执,不利于团队和谐。

(2)经验丰富的老手可能会对新手产生不满的情绪。

(3)一山不容二虎,开发者之间可能就某一问题发生分歧,产生矛盾,造成不必要的内耗。

(4)开发人员可能会在工作时交谈一些与工作无关的事,分散注意力,造成效率低下。
等等……

个人优缺点

  • 吴泽学

    • 优点:
      • 对算法有较为良好的了解
      • 积极热情,有责任心
      • 专注,善于沟通
    • 缺点:
      • 自制力不够
  • 陈星宇

    • 优点:
      • 规划能力比较强
      • 自制力较强,能够督促对方及时跟进完成任务
      • 思维活跃,从多个角度解决问题
    • 缺点:
      • 比较粗心,有细节错误
  • 如何说服你的伙伴改进TA 的缺点
    采用三明治法则,将说服过程分为三步,面包、肉、面包。
    第一层面包为认同、欣赏、关爱、幽默感。
    第二层肉为建议、批评。
    第三层面包为鼓励、希望、信任、支持。
    当我们发现对方的缺点时,并不是严厉地批判对方的缺点,而是先表扬再批评,最后表示期待,成功卸下对方防御心理,并且能鼓励对方激励对方使对方很容易接受批评与建议,从而乐于接受自己的缺点并努力尝试改正。

十四、第一阶段需求——接口定义

设计一组可用于电梯调度的接口和类定义。主要考虑:
1.简单
目前定义两个大类即乘客、电梯,二者的属性以及方法以及类之间的关系见第五点UNL图中的类图。
2.如何提供足够的信息给调度器,以便于能顺利完成调度?
为了完成电梯的调度需要电梯的参数信息包括服务楼层、乘客人数限制、重量限制、电梯的外部上下行按钮标识以及内部楼层按钮标识,还需要乘客的信息包括当前楼层、目标楼层以及乘客体重。
3.实际驱动电梯的组件是什么?
在获取乘客信息之后同时关联内部电梯信息,电梯内的调度组件会根据已设置的调度策略进行调度:
当电梯处于静止状态时,外部按钮被按下后,会通过调用具体的电梯对象的电梯运行函数runElevator()驱动电梯运行。
以下为后期实现后对于该问题方案的修改。
实际驱动电梯的组件包括核心类的函数和控制类的函数以及Qt的timer控件。
判断电梯是否执行开关门、运行下一层楼等动作的函数、执行动作时修改参数的函数等是定义在核心类中的,实际驱动电梯则需要调用这些判断函数然后执行对应动作的函数修改对应参数。又考虑到该程序会有界面,所以想到使用Qt编写界面类和控制类,通过Qt的timer可以实现一个定时的作用,相当于执行动作的时间,再通过信号与槽函数机制进入下一个动作。
驱动过程:每个动作对应一个定时器,在控制类先调用核心类的判断函数,判断是否执行该动作,对应是否开启定时器以及修改界面、核心类参数,定时器结束则进入下一个动作的控制类函数。比如说:电梯关门后,在控制类的函数中,先调用核心类的函数判断是否运行到下一层楼,若为真,则调用核心类的函数修改电梯楼层参数,同时开启对应电梯运行速度的定时器。定时器结束则相当于运行了一层楼,根据信号与槽函数机制进入控制类的另一个函数,继续判断下一个动作。
4.对何规定乘客的行为?例如当乘客需要从3层到20层时,但是当前电梯不能直达,乘客应如何行动?
乘客的乘坐策略包括直达和换乘两种。
1. 直达策略。即乘客选择可以直接从当前楼层到达目标楼层的方法,目前乘客在3层,为了到达20层,直达方式只能选择乘坐1号电梯。
2. 换乘策略。由于只有1号和4号电梯可以到达20层,并且乘坐1号电梯属于直达策略,所以需要找到方法换乘4号电梯。根据已知的电梯服务楼层信息可知,换乘方法有两种,一是乘坐2号电梯到达1层然后换乘4号电梯到达20层;二是乘坐3号电梯到达1层然后换乘4号电梯到达20层。
目前规定乘客的行为是通过最短路径的思想,建立电梯邻接图再使用BFS算法找出到达目标楼层的最短乘坐路径。

十五、第二阶段需求——实现一个基准调度算法

模仿公交车的调度算法,实现一个“BUS”算法,这大概是用于电梯调度中性能最差的一个算法,也就是说你下一阶段实现的算法的性能至少应高于这个算法。
算法的思想是:将电梯当作公交车,从-1层一直到最高层(20层),每一层都停,并且开门,让乘客进出,然后关门,继续向上走。直到最高层,再向下。
在第一阶段中我们已经实现了总体大部分的编程,还并未完成项目的首个版本。在得到第二阶段需求后,我们先暂停了计划的电梯调度方式,转为实现基准调度算法——BUS算法。BUS算法的实现较为简单,因为只要该电梯服务于该楼层,则不管是否有乘客需要上下电梯,电梯都会在该层停下并给予时间用于乘客上下电梯,若无乘客上下电梯则关门继续在原方向上运行。所以在代码实现中,只需要根据电梯当前运行的方向对于是否到该方向的尽头进行判断,同时根据运行方向让楼层加一或减一。
BUS算法的伪代码如下图:
BUS算法伪代码

十六、第三阶段需求——实现你的算法,测试及展示

需求

阅读项目wiki中电梯调度算法,自行了解算法的思想,尝试实现其中2种算法,并进行性能的比较。
目前的这个测试程序只有命令行界面, 请给它设计UI界面, 显示乘客/电梯的运动, 并实现之。
使用相同的大厦+电梯参数,同样的乘客模拟数据,可以同时运行两个调度界面,分别加载一个调度算法,显示乘客+电梯在系统中的运行情况,并且有专门的状态显示(时间,载客人数,KPI等)。
模拟运行完之后,可以直观看到哪个调度算法是快速的。

在此需求阶段,我们选择完成LOOK算法和FD-SCAN 算法。

LOOK算法

LOOK 算法是扫描算法(SCAN,也是第2阶段中的基准调度算法)的一种改进。对LOOK算法而言,电梯同样在最底层和最顶层之间运行。

但当 LOOK 算法发现电梯所移动的方向上不再有请求时立即改变运行方向,而扫描算法则需要移动到最底层或者最顶层时才改变运行方向。所以此算法只需在BUS算法的基础上加以修改即可。

LOOK算法的伪代码如下图:
LOOK算法伪代码

FD-SCAN 算法

FD-SCAN(Feasible Deadline SCAN)算法首先从请求队列中找出时限最早、从当前位置开始移动又可以满足其时限要求的请求,作为下一次 SCAN 的方向。并在电梯所在楼层向该请求信号运行的过程中响应处在与电梯运行方向相同且电梯可以经过的请求信号。这种算法忽略了用 SCAN 算法响应其它请求的开销,因此并不能确保服务对象时限最终得到满足。

在查阅资料过程中了解到了FD-SCAN算法的过程,但是对于其关注的对象——时限,在我们的电梯调度中并未提到。通过阅读该算法的例子,大致了解到时限就是该任务的最大时间限制。结合到电梯调度中,就相当于等电梯的最长等待时间,所以根据实际情况,将每个乘客的时限定为出发楼层与目标楼层的差值成正比。

FD-SCAN 算法的伪代码如下图:
FD-SCAN 算法伪代码

界面

具体见第八点界面模块的详细设计过程。

十七、其它收获

由于这是我们小组两人第一次尝试两人完成一个项目的开发,所以这一次的大作业对于我们两人来说既是挑战也是收获。
本次大作业使用的语言是C++,软件为VS2019和QT,使用的方法为较为系统的软件工程方法。不仅仅使得课堂中的内容付之于行动,同时提高了我们两人的动手能力。也是在此次大作业中学到了更多的新的知识,如Design by contract、Code contract又或者是如何使用VS2019进行单元测试,又如更加熟悉QT的使用方法。
从一开始的半知半解,到共同讨论解决问题的方法,然后共同查阅资料,再到一起修改测试。虽然期间也出了一些错误,但是总体来说,还是收获满满。
描述一下你的程序和其他程序的优劣:
由于在第三阶段需求中需要实现两种电梯调度的算法,我们实现的是一种传统调度算法和一种实时电梯调度算法,并未实现群控算法,所以在程序的创新性和效率方面有些不足。

  • 1
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值