一、十字路口交通灯管理系统
(一)背景
这是某家公司的面试题,要求三天时间完成。
(二)需求分析:
1,需求:
①异步随机生成按照各个线路行驶的车辆:
车辆的行驶方向有:直行,左转,右转,掉头(新增需求)
直行:南<-->北;东<-->西。
左转:南-->西;北-->东;西-->北;东-->南。前两个为一对,后两个为一对。
右转:南-->东;北-->西;西-->南;东-->北。前两个为一对,后两个为一对。
掉头:南-->南,北-->北,东-->东,西-->西
②信号灯只考虑红绿灯,绿灯的下一个灯是红灯,红灯的下一个灯是绿灯,周而复始
③直行、左转受信号灯控制,右转、掉头不受信号灯控制(为了便于统一控制,其信号灯始终为绿灯),为了考虑不造成交通拥堵,掉头也要受信号灯的控制。
④南北车辆与东西车辆交替放行。同方向车辆先放行直行车辆再放行左转车辆
⑤每辆车通过路口时间为1秒
⑥随机生成车辆时间及红绿灯交换时间自定,可以设置;
线路图如下所示:
2.面向对象设计分析
分析需求”①异步随机生成按照各个线路行驶的车辆”可知,要用到的对象有车,路段,而且路段是有向的,即包含了直行:S2N(南到北),N2S,W2E,E2W;左转:S2W,N2E,W2N,E2S;右转:S2E,E2N,N2W,W2S,掉头:S2S,N2N,W2W,E2E。已上路段共计16条。除了这16条以外,不能再有其他值了,故可以考虑设计一个代表路段的类Road,在产生该路段的对象的实例时,要传一个代表路段方向的字段,故构造方法可以设计成Road(String direction),总共要产生16个Road的实例对象,并且在产生该对象的时候要在一个随机的时间内随机增加新的车辆,增加到一个集合List<String>中保存。所以构造方法内要有这么一段代码:
//模拟车辆不断随机上路的过程 //JDK1.5以后线程池定义新技术: ExecutorService pool = Executors.newSingleThreadExecutor(); pool.execute(new Runnable(){//匿名内部类 public void run(){ for(int i=1;i<1000;i++){ try { //在1到10秒中间随机的增加该路段上的车辆 Thread.sleep((new Random().nextInt(10) + 1) * 1000); } catch (InterruptedException e) { e.printStackTrace(); } vechicles.add(Road.this.name + "_" + i); } } }); 再分析需求“⑤每辆车通过路口时间为1秒”可知,及该路段上的车辆在绿灯期间要每个1秒钟移走一辆,所以在构造方法中还是实现每隔一秒钟启动定时器检查当前的灯是否为绿灯,如果是则将路段上的第一辆车移走,及vechiclesvechicles.remove(0),其中的0代表第一辆车,代码如下: //每隔1秒检查对应的灯是否为绿,是则放行一辆车,JDK1.5以后定时器的新技术 ScheduledExecutorService timer = Executors.newScheduledThreadPool(1000); timer.scheduleAtFixedRate( new Runnable(){ public void run(){ if(vechicles.size()>0){ boolean lighted = Lamp.valueOf(Road.this.name).isLighted(); if(lighted){ System.out.println(vechicles.remove(0) + " is traversing !"); } } } }, 1, 1, TimeUnit.SECONDS);
分析需求”②信号灯只考虑红绿灯,绿灯的下一个灯是红灯,红灯的下一个灯是绿灯”,可知,要用到的对象有红绿两种灯(enum Lamp),灯控制系统(LampController )。
由前面的分析可知,每个方向上都有自己的信号灯,故共有16个信号灯,可分别用枚举直行:S2N(南到北),N2S,W2E,E2W;左转:S2W,N2E,W2N,E2S;右转:S2E,E2N,N2W,W2S,掉头:S2S,N2N,W2W,E2E来表示,而且每一个方向都有自己当前的灯的状态以及自己灯的反向灯,而且要每隔一段时间切换一次状态(绿-->红-->),所以要用同样的枚举记住当前的灯的状态和自己反向的灯(除了右转和掉头),并且每一个灯也有它在状态切换时的下一个灯(除了右转和掉头),所以还要有一个切换灯的状态的方法void light()和Lamp blackOut()方法,每一个方向上的灯的下一个分别是(除了右转和掉头):S2N-->S2W-->E2W-->E2S-->S2N,这样就形成了一个个的周期。那为什么信号灯自己提供这样的这样设计方式呢?一个重要的经验是:谁有数据,谁就提供对外操作这些数据的方法。由于前面说的这些状态和属性都是信号灯自己有的,所以就由它来提供对外操作这些数据的方法。信号灯枚举Lamp类完整代码如下:
package com.isoftstone.interview.traffic; /** * 每个Lamp元素代表一个方向上的灯,总共有16个方向,所有总共有12个Lamp元素。 * 有如下一些方向上的灯,每两个形成一组,一组灯同时变绿或变红,所以, * 程序代码只需要控制每组灯中的一个灯即可: * s2n,n2s * s2w,n2e * e2w,w2e * e2s,w2n * s2e,n2w * e2n,w2s * s2s,n2n,e2e,w2w * 上面最后两行的灯是虚拟的,由于从南向东和从西向北、以及它们的对应方向不受红绿灯的控制, * 所以,可以假想它们总是绿灯。 * */ /**/ public enum Lamp { /*每个枚举元素各表示一个方向的控制灯*/ S2N("N2S","S2W",false),S2W("N2E","E2W",false),E2W("W2E","E2S",false),E2S("W2N","S2N",false), /*下面元素表示与上面的元素的相反方向的灯,它们的“相反方向灯”和“下一个灯”应忽略不计!*/ N2S(null,null,false),N2E(null,null,false),W2E(null,null,false),W2N(null,null,false), /*由南向东和由西向北等右拐弯的灯不受红绿灯的控制,所以,可以假想它们总是绿灯*/ S2E(null,null,true),E2N(null,null,true),N2W(null,null,true),W2S(null,null,true); /*掉头的灯不受红绿灯的控制*/ S2S(null,null,true),N2N(null,null,true),E2E(null,null,true),W2W(null,null,true) /*** *枚举也是一种特殊类,它也有自己的构造方法,只不过这个构造方法是私有的 *定义枚举的时候其实就是调用它的私有的构造方法实例化本类对象 *当枚举中只有一个成员时,这其实就是单例设计模式 */ private Lamp(String opposite,String next,boolean lighted){ this.opposite = opposite; this.next = next; this.lighted = lighted; } /*当前灯是否为绿*/ private boolean lighted; /*与当前灯同时为绿的对应方向*/ private String opposite; /*当前灯变红时下一个变绿的灯*/ private String next; public boolean isLighted(){ return lighted; } /** * 某个灯变绿时,它对应方向的灯也要变绿 */ public void light(){ this.lighted = true; if(opposite != null){ Lamp.valueOf(opposite).light(); } System.out.println(name() + " lamp is green,下面总共应该有6个方向能看到汽车穿过!"); } /** * 某个灯变红时,对应方向的灯也要变红,并且下一个方向的灯要变绿 * @return 下一个要变绿的灯 */ public Lamp blackOut(){ this.lighted = false; if(opposite != null){ Lamp.valueOf(opposite).blackOut(); } Lamp nextLamp= null; if(next != null){ nextLamp = Lamp.valueOf(next); System.out.println("绿灯从" + name() + "-------->切换为" + next); nextLamp.light(); } return nextLamp; } }
现在路(Road)和信号灯(enum Lamp)都有了,但是它们还不能自我进行控制,所以还需要控制信号灯的控制器,即灯控制系统(LampController ),灯控制系统要做的职责是每隔一定时间启动定时器将当前绿灯变为红灯,并让下一个方向的灯变绿,这里的一定时间可以取为10秒等。完整代码如下:
package com.isoftstone.interview.traffic; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class LampController { private Lamp currentLamp; public LampController(){ //刚开始让由南向北的灯变绿; currentLamp = Lamp.S2N; } //启动信号灯控制器 public void start(){ currentLamp.light(); /*每隔10秒将当前绿灯变为红灯,并让下一个方向的灯变绿*/ ScheduledExecutorService timer = Executors.newScheduledThreadPool(1); timer.scheduleAtFixedRate( new Runnable(){ public void run(){ System.out.println("来啊"); currentLamp = currentLamp.blackOut(); } }, 10, 10, TimeUnit.SECONDS); } }
通过已上的分析后,关于交通灯控制的准备工作已经结束,现在就差总的控制中心了,即main()方法
(三)启动控制系统
这里所说的控制系统即main()方法,实例化16个路段,代码如下:
package com.isoftstone.interview.traffic; public class MainClass { /** * @param args */ public static void main(String[] args) { /*产生16个方向的路线*/ String [] directions = new String[]{ "S2N","S2W","E2W","E2S","N2S","N2E","W2E","W2N","S2E","E2N","N2W","W2S", "S2S","N2N","E2E","W2W" }; for(int i=0;i<directions.length;i++){ new Road(directions[i]); } /*启动交通灯系统*/ new LampController().start(); } }
(四)总结
通过以上的需求分析和代码实现后,感觉收获不小,这个过程中又让我复习了关于面向对象的高级编程设计和枚举以及List集合、单例设计模式等等。其中面向对象设计中最经典的一句话就是,谁拥有数据,谁就对外提供操作数据的方法,而枚举更是让我更上一层楼的感觉,原来枚举就是一个特殊的类,它里面的成员的取值都是确定的,而且就是枚举本类,它可以提供私有的构造方法来定义枚举的时候做一些特殊的处理,如信号灯枚举就是利用私有构造方法既保证了值的确定性,又可以根据不同的参数产生16个方向上的信号灯,而且右转和掉头的信号灯还是常绿的,枚举更特殊的时候是,当它只有一个成员时,它其实就是一种单例设计模式。除了这些以外我还学到了jdk1.5以后的两种新技术那就是线程池的定义和计时器的定义和使用方法,其中线程池的定义和使用是:
//模拟车辆不断随机上路的过程 //线程池的新定义,定义单线程执行器 ExecutorService pool = Executors.newSingleThreadExecutor(); //线程池的使用 pool.execute(new Runnable(){//匿名对象 public void run(){ for(int i=1;i<1000;i++){ try { //在1到10秒中间随机的增加该路段上的车辆 Thread.sleep((new Random().nextInt(10) + 1) * 1000); } catch (InterruptedException e) { e.printStackTrace(); } vechicles.add(Road.this.name + "_" + i); } } });
定时器的定义和使用是:
/*每隔10秒将当前绿灯变为红灯,并让下一个方向的灯变绿*/ //定时器的新定义和使用方法, scheduleAtFixedRate()需要传四个参数,第一个参数是线程对象,第2个参数是多久后开始启动定时器,第3个参数是每隔多久执行一次,第4个参数是2、3参数的单位 ScheduledExecutorService timer = Executors.newScheduledThreadPool(1); timer.scheduleAtFixedRate( new Runnable(){ public void run(){ System.out.println("来啊"); currentLamp = currentLamp.blackOut(); } }, 10, 10, TimeUnit.SECONDS);