PairWork-电梯调度程序结对编程
结对编程人员:050/184
1 结对编程
1.1 结对编程的优缺点
优点:
● 与单独开发相比,结对能够使人们在压力之下保持更好的状态。结对编程鼓励双方保持代码的高质量,即使在出现了让人不得不飞快地编写代码的压力时仍然如此。
● 它能够改善代码质量。代码的可读性和可理解性都倾向于上升至团队中最优秀的程序员的水平
● 它能够缩短进度时间表。结对往往能够更快地编写代码,代码的错误也更少。这样一来,项目组在项目后期花费在修正缺陷的时间会更少。
缺点:
● 对于有不同习惯的编程人员,可以在一起工作会产生麻烦,甚至矛盾。
● 两个人在一起工作可能会出现工作精力不能集中的情况。程序员可能会交谈一些与工作无关的事情,反而分散注意力,导致效率比单人更为低下。
● 可能让某些人有滥竽充数的机会。
1.2 结对伙伴的优缺点
并肩编程的好伙伴~~!
*我*
优点是:偶尔比较机智,肯于思考,对算法的设计比较有想法;
缺点是:约好的时间经常会迟到……,有时候粗心,有时候想得太多……
*我的小伙伴*
优点是:比较务实,做事不拖沓,对于编程任务的参与比较积极;
缺点是:有点小小的选择恐惧症,细节再注意一点,耐心还要多一点。
总之合作还是很愉快的!两人可以相互提醒,感觉不错,多了一个视角。但是我有时自己想着就陷入自己了……半天回不过神。。。
2 Information Hiding, interface design, loose coupling
2.1 Information Hiding
信息隐藏指在设计和确定模块时,使得一个模块内包含的特定信息(过程或数据),对于不需要这些信息的其他模块来说,是不可访问的。
信息隐藏是结构化设计与面向对象设计的基础。在结构化中函数的概念和面向对象的封装思想都来源于信息隐藏。软件业对这个原则的认同也是最近十年的事情。
David Parnas在1972年最早提出信息隐藏的观点。他在论文"On the Criteria To Be Used in Decomposing Systems into Modules"中指出:代码模块应该采用定义良好的接口来封装,这些模块的内部结构应该是程序员的私有财产,外部是不可见的。
(论文链接:http://www.cs.umd.edu/class/spring2003/cmsc838p/Design/criteria.pdf)
Fred Brooks在《人月神话》的20周年纪念版中承认了当时自己对Parnas的批评是错误的。他说道:“我确信信息隐藏--现在常常内建于面向对象的编程中--是唯一提高设计水平的途径”。
以下列举了一些信息隐藏原则的应用:
1. 多层设计中的层与层之间加入接口层;
2. 所有类与类之间都通过接口类访问;
3. 类的所有数据成员都是private,所有访问都是通过访问函数实现的。
2.2 interface design
每一个大的系统都是有许多模块系统组成的,系统的开发是一个很大的工程,开发起来得难度也是比较大。因此任何一个有一定规模系统,通常会把系统做一定分解降低分析设计开发的难度,模块划分是一个比较常见的方式,而模块与模块之间则是通过接口设计将它们整合在一起的。
实践中,极有可能出现两种状况:接口维护失控或者过严而死板(而影响开发)。接口失控是因为接口的维护太过随意,因为A模块的需要就轻易在B模块中添加一个接口(方法),导致该接口(方法)非独立性(基本上只给模块A的这个功能点使用),或者是接口的控制过严,导致或者工作效率不高,或者接口的易用性不好。
一种可行的实践是:不轻易为模块设计对外提供的接口(方法),除非是通过重构得来的;模块对外提供两种类:一个是需要外部模块实现的接口(接口设计从本模块需要出发,当然每个接口尽管是为某个功能点服务,但也要注意其在模块内通用性),另一个是其它模块要求本模块实现的接口的实现类。即:A模块拥有一些需要B模块实现的接口(A模块对B模块的要求),而B模块中也有要求A模块实现的接口,因而A有这些接口的实现类。这种实践方式的好处在于:模块的接口就多了一层隔离降低了耦合,把接口的通用性和接口的适应性分离,又明确了模块的边界,使得接口在日后的优化和调整有了缓冲。
接口设计的关键是能够将系统的每一个模块能够很好的整合在一起,而且能够让系统能够更好的运行。模块接口设计也是实现系统功能实现整体化的手段,而且是有益于系统拆分、整合等手段所必备的。
接口设计也有一些可查的原则,依据这些原则,我们能将程序完成得更加规范。
2.3 loose coupling
在过去常用的程序架构中,多数应用程序之间直接相互通信。当应用程序需要修改或淘汰时,这种依赖便成为一个实际问题。任何修改都可能会按其自身的方式更新每条唯一的通信线路。因此,这种变更可能代价高昂。这种情况被称为应用程序间的紧耦合,也逐渐成为让一些企业头疼的问题。
另一方面,SOA(面向服务的体系结构) 将松耦合作为成功的企业级应用程序集成的一个主要原则。与紧耦合相反,松耦合是:
限制请求者应用程序代码和提供者应用程序代码的相互了解。如果耦合的服务任何方面有所变化,那么,请求者或提供者的应用程序代码(更可能是两者同时)必须改变。如果任何一方(请求者、提供者或中介基础架构)对解耦的服务任何方面作出改变,那么其它几方不必随之改变。
松耦合系统通常是基于消息的系统,此时客户端和远程服务并不知道对方是如何实现的。客户端和服务之间的通讯由消息的架构支配。只要消息符合协商的架构,则客户端或服务的实现就可以根据需要进行更改,而不必担心会破坏对方。松耦合通讯机制提供了紧耦合机制所没有的许多优点,并且它们有助于降低客户端和远程服务之间的依赖性。但是,紧耦合性通常可以提供性能好处,便于在客户端和服务之间进行更为紧密的集成(这在存在安全性和事务处理要求时,可能是必需的)。
2.4 总结
信息隐藏、接口设计、松耦合都是面向对象编程的重要原则。在这次的程序设计中,我们也尽可能的规范变量或方法的属性,不让私有变量或方法泄漏,保证良好的封装性。
对原始代码中一些不严谨的细节我们也做了修改,比如一些无故公用的方法。
3 Design by Contract, Code Contract
契约式设计或者Design by Contract (DbC)是一种设计计算机软件的方法。这种方法要求软件设计者为软件组件定义正式的,精确的并且可验证的接口,这样,为传统的抽象数据类型又增加了先验条件、后验条件和不变式。这种方法和商业契约的情况有点类似。所谓契约,也就是合约,规定两个交互物件上的权利和责任。雇佣合同规定你的工作时数和你必须遵守的行为规则,作为公司则付你薪水,双双履行义务,双双受益。DbC的核心思想是对软件系统中的元素之间相互合作以及“责任”与“义务”的比喻。
DbC六大原则:
原则1 区分命令和查询。查询返回一个结果,但不改变对象的可见性质。命令改变对象的状态,但不返回结果。(应当是不一定返回结果)
原则2 将基本查询同派生查询分开。派生查询可以用基本查询来定义。
原则3 针对每个派生查询,设定一个后验条件,使用一个或多个基本查询的结果来定义它。这样我们只要知道基本查询的值,也就能知道派生查询的值。
原则4 对于每个命令都撰写一个后验条件,规定每个基本查询的值。结合“用基本查询定义派生查询”的原则,我们现在已经能够知道每个命令的全部可视效果。
原则5 对于每个查询和命令,采用一个合适的先验条件。先验条件限定了客户调用查询和命令的时机。
原则6 撰写不变式来定义对象的恒定特性。类是某种抽象的体现,应当将注意力集中在最重要的属性上,以帮助读者建立关于类抽象的正确概念模型。
DbC对于软件工程是一个极大的理论改革,对于C/S模式造成了极大的影响和冲击。对于C/S模式,我们看待两个模块的地位是不平等的,我们往往要求server非常强大,可以处理一切可能的异常,而对client不闻不问,造成了client代码的低劣。
而在DbC中,使用者和被调用者地位平等,双方必须彼此履行义务,才可以行驶权利。调用者必须提供正确的参数,被调用者必须保证正确的结果和调用者要求的不变性。双方都有必须履行的义务,也有使用的权利,这样就保证了双方代码的质量,提高了软件工程的效率和质量。
缺
点是对于程序语言有一定的要求,契约式编程需要一种机制来验证契约的成立与否。而断言显然是最好的选择,但是并不是所有的程序语言都有断言机制。那么强行
使用语言进行模仿就势必造成代码的冗余和不可读性的提高。比如.NET4.0以前就没有assert的概念,在4.0后全面引入了契约式编程的概念,使得
契约式编程的可用性大大提高了。此外,契约式编程并未被标准化,因此项目之间的定义和修改各不一样,给代码造成很大混乱,这正是很少在实际中看到契约式编
程应用的原因。
在我们的编程中,函数间的调用基本运用了契约式编程的思想,要求传入的参数必须满足特定的要求。
4 Unit test
运行测试的结果:
代码覆盖率:
代码覆盖率只有78.67%,原因是在单元测试中需要自己编写出所有可能的取值,才能保证覆盖。出于时间关系,在编写时类似的语句就没有再进行取值覆盖的过程。
5 UML图
6 算法详解
6.1 算法关键
总的来说是模拟现实中电梯的调度。
程序中有一个总调度器NaiveScheduler,它包含了一个请求队列_PassengerQueue以及供它调度的电梯列表_Elevators。在初始化时,将4个电梯的所有信息都加载到电梯列表里;乘客每次产生一个方向请求,就将该请求添加至总调度器的请求队列_PassengerQueue中。
对于4个电梯而言,每个电梯都有各自的调度器。我们为电梯类SenElevator增加了一个请求列表。每次进去一个乘客,便会产生一个目的地请求,该请求被添加至这个电梯的调度器的请求列表_allReq里。
在每一个tick,都对总调度器的请求队列进行遍历,把请求添加到最佳电梯里。具体的做法是每次循环都取队头指令,对于当前请求,依次遍历电梯列表,寻找最佳电梯bestElev。最佳电梯的主要判断依据是电梯的当前楼层CurrentFloor与请求的发出楼层DirectionReqSource距离最近。当然还必须满足乘客限制、电梯容量以及可达到楼层等要求。遍历完电梯列表后,如果能够找到最佳电梯,则将当前请求添加至最佳电梯的请求列表_allReq里;否则将当前请求移至请求队列_PassengerQueue的队尾。
此外,每一个电梯都实时更新target。具体的做法是每一个tick都遍历请求列表_allReq,对每一个请求的类型加以判断:如果是方向请求,则目标为发出请求的楼层DirectionReqSource;如果是目的地请求,则目标为乘客想到达的楼层DestinationReqDest。找出距离电梯的当前楼层最近的目标,即为电梯的target.
6.2 独到之处
由每一层都停靠改为有需求才停靠,节省了大量的时间。
另外,实时地更新每一个电梯的请求列表(具体的思想是让下一目标距离当前楼层最近)保证了可以顺带地带上顺路的乘客,也对效率有一定的提高。
后来进行优化时,我们想到可以用一个函数CheckRushHour()实现对与上班高峰和下班高峰的判断。具体的做法是统计一段时间内向上的请求和向下的请求的比值,如果比值超过我们设定的阀值RushHourThresholdValue,则认为当前是上/下班高峰。
如果是上班高峰,我们让所有闲置的电梯都停到它们的最低层LowestFloor;如果是下班高峰,则让所有闲置的电梯停到它们的最高层HighestFloor。