结对编程——电梯调度系统
我们的结对编程作业是:现有一新建办公大厦,共有21层,共有四部电梯,所有电梯基本参数如下表所示:
电梯编号 |
可服务楼层 |
最大乘客数量 |
最大载重量 |
1 |
全部楼层 |
10 |
800 kg |
2 |
单层 |
10 |
800 kg |
3 |
双层 |
20 |
1600 kg |
4 |
全部楼层 |
20 |
2000 kg |
其使用规定如下:
1、楼层号为0~20,其中0号为地下一层;
2、有楼层限制的电梯不在响应楼层停靠,如单双层;
3、所有电梯采用统一按钮控制
请根据上述要求设计并实现一个电梯控制程序,如果有图形显示就更好了。
搭档:葛轩。
葛轩博客地址:http://home.cnblogs.com/u/gexuan/
Coding地址:https://coding.net/u/Kingsman/p/Elevator-Dispatching/git
在过去的两周里,我们一起讨论了这个问题应该如何解决。我们的进度大致如下表所示:
日期 | 工作内容 |
2016.3.25 | 选择编程所使用的语言:JAVA |
2016.3.26 | 分析问题,设计出最终想达到的效果:在一个窗口中显示出四部电梯的运行状况。 |
2016.3.27——2016.4.5 | 编程 |
2016.3.25:
之所以选择使用JAVA来编写本次团队作业,是因为自己目前正在自学JAVA,趁热打铁,而且如果想实现有图形显示的程序,用JAVA会比较容易一些,使用容器、组件就可以完成。
2016.3.26:
这道题在刚刚接手时不知道该如何处理,下意识的想法就是到CSDN等大型IT论坛来看看大牛们的一些算法和解决方案,并结合自身的知识来一点点的完成任务,在两个人的商讨下,决定尝试写出一个有界面,可以用鼠标键盘来操作的软件。
2016.3.27——2016.4.6:
我们花费了相当长的时间来编写代码,由于需要考虑的因素很多,需要用到的知识也很多,有许多还需要现学,才能达到作业要求,但是我们最终……还是没能完成所有的功能,毕竟两个人的能力有限,借鉴了一些牛人们的设计思路,边模仿边接下来就来详细的叙述一下设计流程。
两人编程时的照片
一、模块设计
本次编程花费最多精力的是界面的设计,界面中有按钮、文本框、复选框、多个Panel面板,需要细心地编写一个一个的模块(类),很容易少写或者写错某句,导致程序无法编译通过。
窗口类ElevatorFrame——创建窗口,调用其他的模块,是界面的最底层。
class ElevatorFrame extends JFrame {
public ElevatorFrame(String str) {
super(str);
setSize(1000, 600);
Container contentPane = getContentPane(); //AWT容器
ElevatorPanel panel = new ElevatorPanel();
contentPane.add(panel); //建立关联
}
}
在ElevatorFrame中初始化了一个新ElevatorPanel类,ElevatorPanel类是用来承载所有其他组建的主面板,放入到容器AWT中。
ElevatorPanel类——用来放置电梯,按钮,文本框、复选框等组件的面板
class ElevatorPanel extends JPanel { public ElevatorPanel() { setLayout(new BorderLayout()); JLabel label =new JLabel("Elevator Test",JLabel.CENTER); add(label, BorderLayout.NORTH); MainPanel mainPanel = new MainPanel(); add(mainPanel); validate(); //验证子组件 ControlPanel controlPanel = new ControlPanel(mainPanel); add(controlPanel, BorderLayout.SOUTH); } }
整个界面是从底部到顶部一层一层的互相包裹,所以为了使每一个类和主方法能更简洁,提高可读性,在代码中也就采用了一层一层的调用思想,MainPanel是用来显示四部电梯的面板,ControlPanel是用来显示电梯外部按钮的面板。
MainPanel类——用来控制4部电梯的上下行,并显示4部电梯的面板。
此类花费了很长的时间来编写,因为这是电梯调度的核心算法之一,4部电梯在多线程的状态下需要根据外部按钮发出的信息来判断应该调用哪部电梯去接乘客才是最优的方案,编写了许多的方法来实现这一功能,在这里介绍其中的几个比较核心的方法。
// 派发命令的函数 public void Command() { ArrayList runnableElevators = getRunnableElevator(); if (runnableElevators.size() == 0) { for (int i = 0; i < 21; ++i) { Up[i] = 0; Down[i] = 0; } return; } if (hasUpTask()) { upTask(runnableElevators); } else if (hasDownTask()) { downTask(runnableElevators); } }
派发命令方法Command()。此方法是调度四部电梯的一个核心方法,首先要判断如果没有处于工作状态的电梯,则直接返回;如果有,则判断是否有楼层有乘坐电梯的请求,如果有,则调用对应的“向上”和“向下”的调度电梯方法,所有的调度情况最终都需要调用此方法才可以完成一次调度,这也决定了电梯调度的方法只允许电梯先完成一个方向的所有请求后才可以改变方向。
// 当有电梯正在向上,且某楼层有“向上”请求,并且发出“向上“请求的楼层高于正在向上电梯的当前楼层,则调用此方法 private void upingElevator(ArrayList runnableElevators) { if (runnableElevators.size() == 0) { return; } for (int i = 0; i < Up.length; i++) { if (Up[i] == 1) { int nearest = -1; int nearestElevator = -1; for (int j = 0; j < runnableElevators.size(); j++) { if (((SubPanel) runnableElevators.get(j)).getCurrentState() == 1) { //这里需要调用SubPanel类中的getCurrentState()方法,故需要强制类型转换 int temp = ((SubPanel) runnableElevators.get(j)).getCurrentFloor(); if (temp > nearest && temp < i) { nearest =((SubPanel) runnableElevators.get(j)).getCurrentFloor(); nearestElevator = j; } } } if (nearest != -1) { ( (SubPanel) runnableElevators.get(nearestElevator)).setTask(i); //设置使距离请求楼层最近的电梯接收向上命令 Up[i] = 0; nearest = -1; nearestElevator = -1; } } } }
upingElevator方法,是用在当有电梯正在向上,且某楼层有“向上”请求,并且发出“向上“请求的楼层高于正在向上电梯的当前楼层时:首先判断用户乘坐电梯的需求是否为向上,如果是,则在正在运行的电梯中找到正在向上的距离目标楼层最近的一个电梯来接乘客。还有downingElevator方法,与此方法类似,用来处理“向下”的情况。在此方法中用到了强制类型转换,是为了获得电梯当前所处的楼层和给电梯设置指令,而这两个属性和方法都在SubPanel类中,故需要强转。
// 对停着的电梯,楼层有“向上”请求,选择最靠近的电梯 private void stopElevatorUp(ArrayList runnableElevators) { if (runnableElevators.size() == 0) { return; } for (int i = 0; i < Up.length; i++) { if (Up[i] == 1) { int nearest = 22; int nearestElevator = -1; for (int j = 0; j < runnableElevators.size(); j++) { if (((SubPanel) runnableElevators.get(j)).getCurrentState()== 0) { int temp = ((SubPanel) runnableElevators.get(j)).getCurrentFloor(); if (Math.abs(i - temp) < nearest) { nearest = Math.abs(i - temp); nearestElevator = j; } } } if (nearestElevator != -1) { ( (SubPanel) runnableElevators.get( nearestElevator)).setTask( i); Up[i] = 0; nearest = 22; nearestElevator = -1; } } } }
stopElevatorUp方法,用于当有停着的电梯时如何来接乘客,方法很简单,判断距离请求楼层的距离,最近的优先,但是出于对电梯性能的考虑,当有同一方向正在运行的电梯距离目标楼层和停着的电梯一样时,优先使用正在运行的电梯。
ControlPanel类——管理面板下方人数,载重,外部按钮的类
class ControlPanel extends JPanel { JTextField People=new JTextField(6); JTextField Weight=new JTextField(6); JComboBox currentFloorCombo; MainPanel mainPanel; //显示四部电梯的区域的panel的引用,方便消息传递 public ControlPanel(MainPanel mainPanel) { this.mainPanel = mainPanel; Border b = BorderFactory.createEtchedBorder(); //边框 setBorder(b); addButton(); } // 加入操作按钮:向上、向下按钮 private void addButton() { final JButton upButton = new JButton("UP"); upButton.setEnabled(false); final JButton downButton = new JButton("DOWN"); downButton.setEnabled(false); JButton peoButton = new JButton("People"); JButton weiButton = new JButton("Weight"); // 为upButton按钮提供监听器类 ActionListener UpL = new ActionListener() { public void actionPerformed(ActionEvent e) { String InputStr = (String) currentFloorCombo.getSelectedItem(); int floorNo = Integer.parseInt(InputStr); if (floorNo != -1) { mainPanel.addCommand(floorNo, 1); } else { mainPanel.addCommand(floorNo, 2); } } }; upButton.addActionListener(UpL); // 为downButton按钮提供监听器类 ActionListener DownL = new ActionListener() { public void actionPerformed(ActionEvent e) { // 从下拉列表框中,读出在电梯外,哪一层有按钮命令 String InputStr = (String) currentFloorCombo.getSelectedItem(); int floorNo = Integer.parseInt(InputStr); mainPanel.addCommand(floorNo, 2); } }; downButton.addActionListener(DownL); //为peoButton按钮提供监听器类 ActionListener Peo=new ActionListener() { public void actionPerformed(ActionEvent e) { //读出people文本区域的数字,大于零小于十,否则不允许使用 up down按钮 String str=People.getText(); String str1=Weight.getText(); int wei=Integer.parseInt(str1); int peo=Integer.parseInt(str); if((peo>0&&peo<=10)&&(wei>0&&wei<=800)){ upButton.setEnabled(true); downButton.setEnabled(true); } else{ upButton.setEnabled(false); downButton.setEnabled(false); } } }; peoButton.addActionListener(Peo); ActionListener Wei=new ActionListener() { public void actionPerformed(ActionEvent e) { //读出people文本区域的数字,大于零小于十,否则不允许使用 up down按钮 String str=Weight.getText(); String str1=People.getText(); int peo=Integer.parseInt(str1); int wei=Integer.parseInt(str); if((wei>0&&wei<=800)&&(peo>0&&peo<=10)){ upButton.setEnabled(true); downButton.setEnabled(true); } else{ upButton.setEnabled(false); downButton.setEnabled(false); } } }; weiButton.addActionListener(Wei); add(People); add(peoButton); add(Weight); add(weiButton); addComboBox(); add(upButton); add(downButton); } // 加入下拉列表框,模拟在不同的楼层按下“向上”、”向下“按钮 private void addComboBox() { currentFloorCombo = new JComboBox(); currentFloorCombo.setEditable(true); for (int i = 0; i <= 20; ++i) { currentFloorCombo.addItem("" + i); } currentFloorCombo.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent event) { } }); add(currentFloorCombo); People.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { } }); } }
ControlPanel类主要功能是显示外部按钮,即乘客在电梯外的某一楼层选择“上”或者“下”,以及对乘客数量和载重进行判断,避免电梯超重。
此类中的方法都是为组件添加监听功能,就不再赘述,比较简单。
SubPanel类——一部电梯的显示和运行状况的控制
一部电梯的属性有许多:楼层请求数组FloorStop;电梯当前所处楼层CurrentFloor;电梯是否正在运行goFlag;电梯是否开启runFlag;电梯运行状态(上或下)CurrentState。
还有显示电梯需要的组件:楼层显示组件disButton;电梯内按钮operationButtons;电梯目前楼层数dispCurrentFloorLabel。
下面介绍SubPanel类的几个核心方法。
SubPanel(String str) { strName = str; thread = new Thread(this); thread.setDaemon(true); //标记为守护线程 Border b = BorderFactory.createEtchedBorder(); Border titled = BorderFactory.createTitledBorder(b, str); setBorder(titled); setLayout(new BorderLayout()); JPanel panelControl = new JPanel(); CurrentFloor = 1; CurrentState = 0; runFlag = false; goFlag = false; final JButton startButton = new JButton("start"); final JButton stopButton = new JButton("stop"); stopButton.setEnabled(false); // 启动电梯按钮的监听器类 ActionListener startL = new ActionListener() { public void actionPerformed(ActionEvent e) { runFlag = true; startButton.setEnabled(false); stopButton.setEnabled(true); for(int i=0;i<operatorButtons.length;++i) { operatorButtons[i].setEnabled(true); } } }; startButton.addActionListener(startL); panelControl.add(startButton); // 关闭电梯按钮的监听器类 ActionListener stopL = new ActionListener() { public void actionPerformed(ActionEvent e) { if (goFlag == false) { runFlag = false; startButton.setEnabled(true); stopButton.setEnabled(false); for(int i=0;i<operatorButtons.length;++i) { operatorButtons[i].setEnabled(false); } } } }; stopButton.addActionListener(stopL); panelControl.add(stopButton); add(panelControl, BorderLayout.SOUTH); JPanel panelCtrlInElevator = new JPanel(); panelCtrlInElevator.setLayout(new BorderLayout()); dispCurrentFloorLabel = new JLabel(" " + "1"); panelCtrlInElevator.add(dispCurrentFloorLabel, BorderLayout.NORTH); // 电梯内的按钮添加 JPanel panelButtonsInEvelator = new JPanel(); panelButtonsInEvelator.setLayout(new GridLayout(20, 2)); for (int i = 0; i < 4; ++i) { panelButtonsInEvelator.add(new JLabel("")); } operatorButtons = new JButton[21]; // 代表 21 层的21 个按钮 for (int i = 0; i < 21; ++i) { operatorButtons[i] = new JButton("" + i); operatorButtons[i].setEnabled(false); panelButtonsInEvelator.add(operatorButtons[i]); addAction(operatorButtons[i]); } panelCtrlInElevator.add(panelButtonsInEvelator, BorderLayout.CENTER); add(panelCtrlInElevator, BorderLayout.CENTER); // 模拟楼层 JPanel panelElevator = new JPanel(); panelElevator.setLayout(new GridLayout(21, 1)); dispButton = new JButton[21]; for (int i = 0; i < 21; ++i) { dispButton[i] = new JButton(" "); dispButton[i].setBackground(Color.white); // 初始化的颜色是白色 dispButton[i].setEnabled(true); panelElevator.add(dispButton[i]); } dispButton[19].setBackground(Color.blue); // 一楼在初始化时 颜色是红色,表示电梯在一楼 add(panelElevator, BorderLayout.WEST); thread.start(); }
SubPanel的构造方法,看起来很庞大,基本上都是在设置显示界面,包括电梯开启关闭按钮,楼层显示的初始化,电梯内按钮的初始化,电梯当前楼层的初始化。
// 当电梯在向上时,只接受高于当前楼层的停靠请求,当电梯在向下时,只接受低于当前楼层的停靠请求 private boolean isStop(int[] FloorStop,int num,int CurrentFloor,int CurrentState) { if (CurrentState == 1) { if (num > CurrentFloor) { FloorStop[num] = 1; return true; } } else if (CurrentState == 2) { if (num < CurrentFloor) { FloorStop[num] = 1; return true; } } else { FloorStop[num] = 1; return true; } return false; }
isStop方法用于选择性的接收楼层停靠请求,因为电梯必须首先完成一个方向的所有请求才可以接收反方向的请求,否则就会出现混乱,此方法用于进行判断,极其重要。
// 线程运行的函数 public void run() { while (true) { try { synchronized (thread) { if (!runFlag || !goFlag) { thread.wait(); } } if (isTheSameFloor(FloorStop)) { updateDisp(CurrentFloor); FloorStop[CurrentFloor] = 0; Thread.sleep(500); arrivalDisp(CurrentFloor); dispButton[20 - CurrentFloor].setBackground(Color.green); Thread.sleep(2500); dispButton[20 - CurrentFloor].setBackground(Color.blue); } else if (isUP(FloorStop)) { CurrentState = 1; while (isStillUP(FloorStop, CurrentFloor)) { CurrentFloor++; updateDisp(CurrentFloor); if (isStoped(FloorStop, CurrentFloor)) { Thread.sleep(1000); dispButton[20 - CurrentFloor].setBackground(Color.green); Thread.sleep(1000); FloorStop[CurrentFloor] = 0; arrivalDisp(CurrentFloor); Thread.sleep(1000); } dispButton[20 - CurrentFloor].setBackground(Color.blue); Thread.sleep(1000); } } else { // 在该电梯向下运行时的处理 CurrentState = 2; while (isStillDOWN(FloorStop, CurrentFloor)) { CurrentFloor--; updateDisp(CurrentFloor); if (isStoped(FloorStop, CurrentFloor)) { Thread.sleep(1000); dispButton[20 - CurrentFloor].setBackground(Color.green); FloorStop[CurrentFloor] = 0; arrivalDisp(CurrentFloor); Thread.sleep(1000); } dispButton[20 - CurrentFloor].setBackground(Color.blue); Thread.sleep(1000); } } goFlag = false; CurrentState = 0; } catch (InterruptedException e) { } } } }
线程运行方法run():在SubPanel中声明了许多的方法,需要通过run方法来调用,故此方法是使电梯真正运行的主方法。
首先判断请求楼层是否与电梯停靠楼层相同;如果不是,判断电梯是否要上升;如果不上升,则电梯必要下降。
二、程序测试
1.打开程序
初始界面如图所示,外部按钮在最下方,UP和DOWN是灰色的,因为没有键入人数和载重,人数不能多于10,载重不能多于800。四部电梯的start按钮用来启动电梯。
准备就绪,开始选择楼层,选择UP或者DOWN,本程序的四部电梯可以同时运作,所以支持用户不断的发送请求。
电梯到达请求楼层后会开门,开门时代表电梯的蓝色按钮会变成绿色,同时电梯内部的按键也会变为绿色,乘客进入电梯后,可以点击要达到的楼层对应的按钮,电梯就会继续运行。
在测试中,我们发现了以下的问题,由于时间比较紧张,有些还没有来得及改进:
(1)这个版本是经过多次修改迭代的最终效果,在之前的版本中,出现了电梯所处楼层与当前实际楼层不匹配的情况,还有电梯无法下楼,电梯内部按钮颜色不会恢复原样等问题,都已经改进。
(2)底部的UP DOWN按钮的位置不是特别明显,这是用户在外部主要使用的按钮,使用频率比较高,需要改进布局,增强外部按钮的显示效果。
(3)底部控制人数与载重的文本框按钮,在第一次打开程序的时候,必须输入符合范围内的数字才可以激活UP DOWN按钮,但是第二次再输入超过范围的数值时,不点击按钮仍然可以使用UP DOWN按钮,这个BUG需要后续用其他的方法来改进。
三、程序评价
本次结对编程中我负责写代码,葛轩负责审查代码和测试,由于是第一次做这种带有界面的程序,所以之前在CSDN论坛中看到了一些大牛们写好的界面,就仿照他们的写出了这个界面,从葛轩第一次测试开始,发现了这个程序只有电梯界面很有易读性,底部的外部按钮不是特别的显眼,而且第一次使用需要我的提示才能正常使用,所以程序界面对用户的友好度还有待进一步提升。
在编写这个程序的过程中遇到了非常多的麻烦和问题,目前做出来这样的效果,还没有满足老师要求的1号、4号电梯分别为单双层,每一部电梯规定的载客量和称重量不同的要求,仅满足了最基本的电梯调度,可能这会让我们的结对编程成绩降低,但是由于时间紧迫,目前还没有达到全部的要求,不过我们正在努力完善,不久之后一定会有完美的最终版提交出来。
程序还有很多地方需要润色,比如整体页面的布局,楼层显示,外部按钮的易用性,包括是否加入声效来提高程序的综合效果,我们会陆续改进的。
四、感想
经过这次的结对编程,收获了特别特别多,首先是合作方面,我和葛轩日常几乎没什么合作,这应该算是第一次,合作的过程很顺利,主要的代码是我来编写,他负责代码审查和测试,在代码审查时发现了我许多的问题,比如对变量和方法的命名,注释的方式,代码的规范等等,现在写好的代码,虽然也可能有些地方还是不够好看,不够易读,但是这么大的代码量,还是第一次按照这样的标准来完成,获益匪浅。
评价一下搭档葛轩吧,虽然在代码编写时没有出很多的力,但是在审查和测试时非常认真,提出了以用户视角看到的问题,这对后期的程序改进有非常大的意义,这次的任务虽然比较困难,但是我们还算是尽力做到了目前想要的效果,合作过程也很愉快。
对于程序本身,这次的开发过程让我得到了很大的历练,代码规范得到了很大的改进,更重要的是在编程过程中学到的知识:多线程技术、监听类使用、可视化界面的编写、面向对象设计编程思想的提升等等,只有在这种实战中才能得到最大的进步,在刚刚开始接到这个题目时,我完全没有头绪如何来调度4部电梯,起初我尝试使用C语言来编写,在CMD窗口中显示结果,但是我只能完成1部电梯的调度,无法将4部电梯联动起来进行分析,后来在CSDN论坛上看到了大牛的作品,用可视化界面非常清楚的展示出来5部电梯的调度情况,这才使我决定用JAVA来写这个程序。在这个程序中,使用了大概6、7个Panel面板,十多个组件来完成最基础的界面制作,在编写监听类的过程中遇到了很多的问题,比如如何使电梯内部按钮响应电梯运行,如何使外部按钮响应4部电梯的运行调度,想了很久,也借鉴了牛人的思想,才一点点的完成。在SubPanel类中编写一部电梯过程中,要考虑到电梯上行和下行,考虑到电梯同一时刻只能单向运行,考虑如何让4部电梯同时运行等等问题,故SubPanel类是整个程序最最核心的部分,也是最难编写的部分,这部分大约花费了编写所有代码一半的时间,在编写好了SubPanel所有方法之后,紧接着面临的还有一个特别困难的事情,那就是如何使用多线程。在编写本程序之前,我还没有学习完多线程技术,所以就抓紧时间补充了多线程的知识,来一点点的完成run方法的内容,因为要运行调度四部电梯,必须要通过run方法来一一实现。在经过这次结对编程后,使我的JAVA编程能力进步了许多。
说到JAVA,目前我正在自学的编程语言,通过学习和结对编程,让我对这门语言有了一些思考和感想,JAVA要比C更丰富,面向对象的语言可以更好地针对一个需求来编程,同时JAVA比C++更易学,除了单继承,JAVA有非常多的类方法可供直接使用,比如在本次编程中常用的监听器类ActionListener,多线程run方法,面板Panel的添加,布局等等方法,都是直接调用awt,swing等这些包中的方法拿来使用,和C++相比省了不少的事。最值得一提的就是JAVA的API帮助文档,它就像一个词典,编程中随时可供我来查询需要使用的方法和理解不了解的方法,特别特别地实用。我会继续努力的学习JAVA开发,也尽量在之后的团队项目中使用JAVA来做,这确实是一门非常好用的编程语言。
最后说一说自己对编程的感想吧。在校我在广播台用汉语和所有人沟通,编程语言也是一门语言,它和汉语有很多很多共通的地方,虽然它更加注重语法的严谨和逻辑准确,但是它也能完成我们语言无法完成的事情,它可以创造程序,每次当我编写的代码可以编译成功运行起来的时候,心中就会有极大的成就感,这是我们日常沟通无法带来的一种感受,虽然有时会感觉编程很枯燥,但是当认真的沉浸在编程当中时,还是觉得这是一件很酷的事情。在这里再次感谢同伴葛轩的帮助,感谢老师布置这次作业让我们都得到了很好的锻炼和进步,我会继续加油的。