随便浏览中观摩了《联想利泰的一道做出来就给月薪7K的面试题--交通灯管理系统》一文,该文设计实现了一个交通灯管理系统,模拟了道路、灯和控制器三个对象的行为。这种设计在思路上比较直观,可能大多数面向对象的设计者第一时间都会想到这种设计。虽然这种设计已经很好的满足于需求说明书了,但是细细想来,还是觉得有些可以改进的地方,而这种改进是从根本上对设计思路的改进!
我是开车新手,但是以我的观察,十字路口的交通灯控制方式大概有两种:对向式控制,和单边轮转式控制。对向式控制就是原文中需求说明书提到的控制方式,每次开放对向直行或左转的匝道,有横向直行、横向左转、纵向直行、纵向左转四个状态。单边轮转式控制是每次同时开放一条道路的直行和左转匝道,按四个路口分成也四个状态。
那么从实际情况来看(抛开原文的需求说明书),我们知道,需求往往是变化的。一个路口可能原先是一种控制方式,到高峰期要转换为另外一种控制方式,甚至转变为手动控制。我就见过有交警站在十字路口边上手动拧控制器的按钮来按照实际车流来控制交通灯(一般是路口拥堵的时候)。
如果从面向对象的角度来看,我们完全可以把控制方式抽象出来,实际需要变更控制方式的时候,就启用不同的控制对象。但是这种方式有两个缺点:1、某一个控制对象的代码已经占很大篇幅了,变更控制对象意味着要实现多个控制对象,工作量跟重写整个控制系统没什么区别,因为交通灯系统的数据很简单,更换代码基本上就等同于更换系统。2、重载总是有数量的,切换为手动控制不能等同于去调用某个特殊的控制对象,因为手动控制是随意的,而不能局限于系统上已有的控制方式。
所以从这方面来看,在一个实际的交通灯系统设计中,一味的对象划分和封装可能捞不到多少好处,因为可供你划分和封装数据实在是太少了,没什么划分和封装的价值。在此引一个笑话:
男子喊道:“服务员,过来一下!” 服务员:“您好,什么事?” 男子怒问:“我20块钱一碗的牛肉面,怎么才一块牛肉?” 服务员:“先生,那您希望有几块?” 男子想了想说:“怎么也得五六块牛肉吧。。” 服务员冲厨房喊道:“出来个师傅,帮这位顾客把这块牛肉切一下!”
PS:引入这个笑话并没有讽刺原文的意思,原文只是一种贴合那个简单需求的设计而已,而且那个设计也很简明易懂。我把交通灯需求扩展了很多,原设计肯定是不合适了。这里只是说明交通灯的可供划分的数据实在是太少了。
那么如何完成这个被我扩展了的需求呢?也许,从本质上可以看得更明白些。交通灯系统的本质是什么?是状态机!上面已经讲到了,通用的那两种控制方式都是4种状态轮转。从这个角度来看,其实控制器工作反倒是相对固定的:1、将某些灯变红;2、将某些灯变绿;3、(最重要的是)保证路口的红绿灯组合不违反交通逻辑,或者说,不能让同行的车道相交。
因此我们首先可以构造出一个交通逻辑出来,也就是说,我们要建立一张表,记录车流与车流之间的冲突关系:
E S W N 代表东南西北(道路),L F R 分别代表 左转,直行 和右转(方向)。右转与所有其他车流都不冲突,所以不列出。
仔细观察这张表会发现规律:任意不冲突的两个车流要么道路相同,要么方向相同。假设我们选择一个车流A为绿灯,那么同时还可以选择另外一个车流为绿灯,并保证所有同行车流不冲突,那么另外一个车流要么道路与A相同,要么方向与A相同。前者就是前面说的单边轮转式,后者就是对向式。
那么控制器可以这样设计:每次控制器选择一个车流为绿灯,同时选择一个自动选择方式(道路 or 方向),由此组合出一个绿灯组合,其他灯变为红灯。
controllMode = "Direction"; // 对向式控制 routeListRoad = {"EL", "SL", "WL", "NL"}; routeListDirection = {"EL", "EF", "SL", "SF"}; function controller(){ route = "EL"; while (true){ route = chooseNextRoute(getControllMode(), route); nonCross = chooseNonCross(getControllMode(), route); setRedExcept(nonCross); setGreen(nonCross); } } function getControllMode(){ return controllMode; } function chooseNextRoute(mode, route){ // 写得很笨,各位有兴趣自己改进 i = 0; routeList = eval("routeList"+mode); while(i<routeList.length()){ if (routeList[i++] == route) break; } return routeList[i % routeList.length()]; } function chooseNonCross(mode, route){ nonCross = new Array(); // 略,各位有兴趣自己写 return nonCross; }
这基本上算是面向过程设计,各位有兴趣可以包装成对象。
这种设计兼顾了两种主流的自动控制方式,而且可以改造为手动。并且手动控制的接口将会很直观,交警只需要设定好控制模式(controlMode),然后选择任意车道即可。
最后总结一下:认清事物的本质很重要,往往事半功倍。