以一个简单的项目来学习面向对象编程(设计模式和多线程)
来源:互联网 发布:淘宝上怎么卖二手东西 编辑:程序博客网 时间:2024/05/29 11:44
下面的项目是两年前学校老师布置的一个小项目,当时自己用了一种很笨拙的方式实现了,现在用面向对象的思想和多线程重构这个项目。
问题描述:
西宝高速仿真模拟
西安市到宝鸡市之间是我省主要的高速公路客运路线之一,经过简化后的客运路线端点、中途停靠点和里程如下图所示(括号里是简称,里程的单位是公里):
限定条件
(1) 从XN始发至BJ的客车和从BJ始发至XN的客车均有两种车型:沃尔沃(限定乘客人数为40人);依维柯(限定乘客人数为21人)。沃尔沃的速度为2公里/分钟,依维柯的速度为1.4公里/分钟。
(2) 起始状态时,XN拥有沃尔沃和依维柯客车分别为XNW和XNY辆,BJ拥有沃尔沃和依维柯客车分别为BJW和BJY辆。
(3) 从XN至BJ和从BJ至XN的沃尔沃,均为上午8:30开始,每小时一班,最后一班为下午5:30;从XN至BJ和从BJ至XN的依维柯,均为上午8:00开始,每20分钟一班,最后一班为下午6:00。
(4) 从XN至BJ的客车到达BJ后,即成为从BJ至XN的客车,排在当时BJ同类车型的队尾,再按(3)确定发车时间;从BJ至XN的客车到达XN后的规则相同。
(5) 只考虑途中只有乘客下车、没有乘客上车的情况。
(6) 有乘客下车时,不论方向与车型,停车时间统一为2分钟。
(7) 乘坐从XN至BJ客车的乘客,其下车点为XY、XP、WG、CP、GZ和BJ的可能性分别为P_XBXY、P_XBXP、P_XBWG、P_XBCP、P_XBGZ和P_XBBJ。这些可能性之和为1;乘坐从BJ至XN客车的乘客,其下车点为GZ、CP、WG、XP、XY和XN的可能性分别为P_BXGZ、P_BXCP、P_BXWG、P_BXXP、P_BXXY和P_BXXN。这些可能性之和为1。需仿真的活动
(1) 从上午7:30开始到下午5:59为止,每分钟分别在XN和BJ随机产生去往BJ和XN方向的新到达的乘客。每分钟达到的人数范围为0~PN人。
(2) 按照限定条件(7)的规定,随机产生新到达的乘客的目的地。
(3) 乘客按到达的先后顺序上最近一辆(依照限定条件(3)的规定)始发的客车,若该车客满则等候下一辆始发的客车。
(4) 若客车到达中途停靠站时有乘客在此下车,按限定条件(5)和(6)处理,否则不停车继续行驶。
我们逐步分析最关键的点:
我们先仅仅模拟一辆客车从西安到宝鸡的过程,中途遇到的中间站停车2分,没有乘客参与,仅仅是让这辆客车从西安跑到宝鸡。
这个简单的问题,直观的解决方案是:一个大循环,每次循环时间更新一次,在循环内更新客车的位置,判断客车时候到达中间站或终点站。这种解决方式思想简单,但是可扩展性差,若有新种类的客车,则我们需要重新改写主逻辑。
我们用面向对象的思维来分析这个简单的仿真模拟过程,实际上就是客车启动、行驶、中途停车、结束。这几个状态间的转化。可以用状态模式来解决这个问题。
思路:
客车类接收外界传入的时间,其初始时调用启动状态指针,并把自己作为参数传入,状态类根据外界条件(时间)和规则(客车时刻表),来判断出下个状态是什么(并更新客车类中保存的状态码)完成状态转换。
这样,客车只是一直调用其当前状态码对应的状态指针来运行逻辑,(状态类对象指针的函数悄悄地改变了客车类中的当前状态码,这样,在客车不知不觉地过程中,完成了状态的转换)
class Vehicle { public: Vehicle() { _brand = "Volvo" ; _identifier = 1 ; _speed = 2 ; _driveDirect = FORWARD ; _curStateNo = 0 ; _curPos = 0 ; } int Init(const Time& curTime) ; //run int Running(const Time& curTime) ; //根据当前时间,返回vehicle当前状态:起点start、路上running、中间站停车midStop、终点endStop int GetVehicleState(const Time& curTime) ; private: std::string _brand ; //Vehicle品牌(名字) int _identifier ; //Vehicle的编号(不同品牌分别编号) double _speed ; //车速(单位为:公里/分钟) int _passengerNumLimit ; //载客量 int _curStateNo ; //当前Vehicle所处状态码 DirectKind _driveDirect ; //当前Vehicle的行驶方向 int _curPos ; //当前位置(离始发站的距离) //每个Vehicle都有一张状态码和状态对象映射表,我们在Vehicle初始化的时候创建所有状态对象 std::map<int, VehicleState*> _vehicleStateMap ; //Vehicle运行时间表(每一站的到达时间和发车时间) std::vector<std::pair<Time, std::string> > _vehicleSchedule ; //改变当前状态 VehicleState* ChangeState(int destStateNo) ; //计算运行时刻表 int CalcVehicleSchedule(const Time& startTime, const DirectKind& driveDirect) ; friend class VehicleState ; } ;
客车对外的接口只有running();
而running所做的工作只是调用当前客车状态指针的process函数,并把自己和当前时间作为参数传入。
把主要的逻辑和处理交给客车状态对象去做。
int Vehicle::Running(const Time& curTime) { int ret ; ret = _vehicleStateMap[_curStateNo]->Process(this, curTime); if (ret == -1) return -1 ; return 0 ; }
//客车状态类 //交通工具接口(抽象类) class VehicleState { public: VehicleState() {} virtual int Process(Vehicle* pVehicle, const Time& curTime) = 0 ; protected: int ChangeState(Vehicle* pVehicle , int destStateNo); } ; //启动状态 class StartState : public VehicleState { public: int Process(Vehicle* pVehicle, const Time& curTime) ; } ; //行驶状态 class RunningState : public VehicleState { public: int Process(Vehicle* pVehicle, const Time& curTime) ; } ; //中途停车状态 class MidStopState : public VehicleState { public: int Process(Vehicle* pVehicle, const Time& curTime) ; } ; //到站停车状态 class EndStopState : public VehicleState { public: int Process(Vehicle* pVehicle, const Time& curTime) ; } ;
在状态类的process函数中,所做的工作是:1、处理当前状态下的事情。2、根据逻辑改变客车的当前状态(所以,状态类是客车类的友元)
int RunningState::Process(Vehicle* pVehicle, const Time& curTime) { std::cout << "Run\n" ; //在当前运行状态下,我们仅仅代表性地输出Run。 //先判断当前情况下能否行车(是否到站,根据时间判断:初始发车时 车会获得一个发车时间和和乘客信息,此时计算运行时刻表,每次启动的时候都要计算) Time nextTime = curTime ; nextTime.AddTime(1) ; int nextVehicleState = 0 ; nextVehicleState = pVehicle->GetVehicleState(nextTime) ; //转换到下一个状态(根据时间判断是否:中途停车、终点停车、在路上) if (nextVehicleState == -1) { return -1 ; } if (nextVehicleState == MIDSTOP) { ChangeState(pVehicle,MIDSTOP) ; } else if (nextVehicleState == ENDSTOP) { ChangeState(pVehicle,ENDSTOP) ; } return 0 ; }
我们把主要的逻辑写在状态类中,且状态的转化也是在状态类中完成的,客车类并不知道。
这样,在外部循环中,我们只需要调用客车的running函数且把时间传入即可,其中的运行和状态转化会自动进行。
状态模式
使用状态模式前,客户端外界需要介入改变状态,而状态改变的实现是琐碎或复杂的。
使用状态模式后,客户端外界可以直接使用事件Event实现,根本不必关心该事件导致如何状态变化,这些是由状态机等内部实现。
这是一种Event-condition-State,状态模式封装了condition-State部分。
每个状态形成一个子类,每个状态只关心它的下一个可能状态,从而无形中形成了状态转换的规则。如果新的状态加入,只涉及它的前一个状态修改和定义。
状态转换有几个方法实现:一个在每个状态实现next(),指定下一个状态(本文中就是使用这种方法);还有一种方法,设定一个StateOwner,在StateOwner设定stateEnter状态进入和stateExit状态退出行为。
状态从一个方面说明了流程,流程是随时间而改变,状态是截取流程某个时间片。
关于状态机的一个极度确切的描述是它是一个有向图形,由一组节点和一组相应的转移函数组成。状态机通过响应一系列事件而“运行”。每个事件都在属于“当前” 节点的转移函数的控制范围内,其中函数的范围是节点的一个子集。函数返回“下一个”(也许是同一个)节点。这些节点中至少有一个必须是终态。当到达终态, 状态机停止。
使用情景
State模式在实际使用中比较多,适合”状态的切换”.因为我们经常会使用If elseif else 进行状态切换, 如果针对状态的这样判断切换反复出现,我们就要联想到是否可以采取State模式了.
不只是根据状态,也有根据属性.如果某个对象的属性不同,对象的行为就不一样,这点在数据库系统中出现频率比较高,我们经常会在一个数据表的尾部,加上property属性含义的字段,用以标识记录中一些特殊性质的记录,这种属性的改变(切换)又是随时可能发生的,就有可能要使用State.
【注意:若是根据不同的条件有不同的处理,这种if-else不必用状态模式,直接用表驱动即可,用查表的方式设计更合理】
在实际使用,类似开关一样的状态切换是很多的,但有时并不是那么明显,取决于你的经验和对系统的理解深度.
这里要阐述的是”开关切换状态” 和” 一般的状态判断”是有一些区别的, ” 一般的状态判断”也是有 if..elseif结构,例如:
if (which==1) state="hello"; else if (which==2) state="hi"; else if (which==3) state="bye";
这是一个 ” 一般的状态判断”,state值的不同是根据which变量来决定的,which和state没有关系.
如果改成:
if (state.euqals("bye")) state="hello"; else if (state.euqals("hello")) state="hi"; else if (state.euqals("hi")) state="bye";
这就是 “开关切换状态”,是将state的状态从”hello”切换到”hi”,再切换到”“bye”;在切换到”hello”,好象一个旋转开关,这种状态改变就可以使用State模式了.
如果单纯有上面一种将”hello”–>”hi”–>”bye”–>”hello”这一个方向切换,也不一定需要使用State模式,因为State模式会建立很多子类,复杂化,但是如果又发生另外一个行为:将上面的切换方向反过来切换,或者需要任意切换,就需要State了.
多线程
刚才我们解决了一个核心问题,让客车动起来。现在我们要实现的是同时让多辆客车行驶起来。
我们可以用串行的方式来模拟这个过程:用同一时刻时间值来遍历所有的客车,激发客车的运行,模拟出在某时刻多辆客车运行的效果。
我们用多线程的方式来仿真这一过程,每一辆客车的运行由一个线程负责,在某时刻客车线程同时运行。
本项目中,使用的是Unix下的线程同步机制——条件变量,关于条件变量
条件变量(cond)
当我们遇到期待的条件尚未准备好时,我们应该怎么做?我们可以一次次的循环判断条件是否成立,每次给互斥锁解锁又上锁。这称为轮询(polling),是一种对CPU时间的浪费。
我们也许可以睡眠很短的一段时间,但是不知道该睡眠多久。
我们所需的是另一种类型的同步,它允许一个线程(或进程)睡眠到发生某个时间为止。
互斥量用于上锁,条件变量则用于等待。则两种不同类型的同步都是需要的。
条件变量是与互斥量一起使用的,因为条件本身是由互斥量保护的,线程在改变条件状态前必须首先锁住互斥量。
API
int pthread_cond_wait(pthread_cond_t* cond, pthread_mutex_t mutex) ;
使用pthread_cond_wait等待条件变为真,传递给pthread_cond_wait的互斥量对条件进行保护,调用者把锁住的互斥量传给函数。函数把调用线程放到等待条件的线程列表上,然后对互斥量解锁。pthread_cond_wait返回时,互斥量再次被锁住。示范代码:
pthread_mutex_lock(&var.mutex) ;while (条件为假) {pthread_cond_wait(&var.cond, &var.mutex) ;}修改条件pthread_mutex_unlock(&var.mutex) ;
通知线程条件已满足:
int pthread_cond_signal (pthread_cond_t* cond) ;
//唤醒等待条件的某个线程
int pthread_cond_broadcast (pthread_cond_t* cond) ;
//唤醒等待该条件的所有线程
【代码示例】
struct{ pthread_cond_t cond ; pthread_mutex_t mutex ; int continueRun ;} oneready = { PTHREAD_COND_INITIALIZER , PTHREAD_MUTEX_INITIALIZER, 0} ;struct{ pthread_cond_t cond ; pthread_mutex_t mutex ; int pthreadNum ;} allready = { PTHREAD_COND_INITIALIZER , PTHREAD_MUTEX_INITIALIZER, 0} ;Time g_curTime ;int g_curBusNum = 0 ;pthread_mutex_t mutexTime = PTHREAD_MUTEX_INITIALIZER ; pthread_mutex_t mutexBusNum = PTHREAD_MUTEX_INITIALIZER ;
//主线程for (int i=0; i<130; ++i) { //130只模拟130分钟,此是为了示范而写 startStation.Run(g_curTime) ;//会根据时间表来生成客车线程 //等待所有线程完成一轮工作(若当前无线程则跳过) pthread_mutex_lock(&allready.mutex) ; while(allready.pthreadNum != -g_curBusNum) { //若所有的线程都销毁了,则本线程不能继续阻塞等待 pthread_mutex_lock(&mutexBusNum) ; bool allEnded = (g_curBusNum == 0) ; pthread_mutex_unlock(&mutexBusNum) ; if (allEnded) break ; pthread_cond_wait(&allready.cond, &allready.mutex) ; } allready.pthreadNum = 0 ; pthread_mutex_unlock(&allready.mutex) ; //时间增加1 pthread_mutex_lock(&mutexTime) ; g_curTime.AddTime(1) ; pthread_mutex_unlock(&mutexTime) ; //通知所有线程继续 if (g_curBusNum > 0) { pthread_mutex_lock(&oneready.mutex) ; oneready.continueRun = 1 ; pthread_mutex_unlock(&oneready.mutex) ; pthread_cond_broadcast(&oneready.cond) ; } }
//客车线程void* busrun(void* busArgv){ while (1) { //做自己的事情 Vehicle* pBusArgv = (Vehicle*)busArgv ; pthread_mutex_lock(&mutexTime) ; g_curTime.Show(std::cout) ; pthread_mutex_unlock(&mutexTime) ; int retState = 0 ; retState = pBusArgv->Running(g_curTime) ; //若自己是最后一个完成的,则通知主控制线程 pthread_mutex_lock(&allready.mutex) ; allready.pthreadNum-- ; if (allready.pthreadNum == -g_curBusNum) { if (retState == -1) //bus跑完全程,回收 { pthread_mutex_lock(&mutexBusNum) ; g_curBusNum-- ; pthread_mutex_unlock(&mutexBusNum) ; } pthread_cond_signal(&allready.cond) ; } pthread_mutex_unlock(&allready.mutex) ; //bus跑完全程,此线程结束 if (retState == -1) break; //等待可以继续运行的信号 pthread_mutex_lock(&oneready.mutex) ; while(oneready.continueRun == 0) { pthread_cond_wait(&oneready.cond, &oneready.mutex) ; } oneready.continueRun = 0 ; pthread_mutex_unlock(&oneready.mutex) ; } return NULL ;}
startStation.Run(g_curTime) ;//根据当前时间判断是否到了发车时间,若到了发车时间,则生成一个客车线程。
至于乘客上下车,车站对客车的调度,实现不难,有兴趣的朋友可以自己用C++实现全部功能。
- 以一个简单的项目来学习面向对象编程(设计模式和多线程)
- 面向对象来做一个迷宫游戏(努深刻学习面向对象技术以及面向对象的设计模式)
- 面向对象编程和设计模式的七大原则理解
- 面向对象编程设计模式--简单工厂模式讲解(历史上最简单明白的例子)
- 面向对象编程设计模式--简单工厂模式讲解(历史上最简单明白的例子)
- 学习面向对象编程的简单途径
- 学习面向对象编程的简单途径
- 推荐一个值得尊敬老外Al-Farooque Shubho的两篇文章---对于学习面向对象和设计模式很好帮助
- 面向对象和设计模式的随想
- 面向对象的基础和设计模式
- 面向对象设计模式-简单工厂模式
- 面向对象和设计模式
- java学习笔记:面向对象编程之工具类的创建与单例设计模式
- 一个基于设计模式的面向对象的框架
- VB面向对象编程的一个简单的演示程序
- 调侃面向对象编程的23种设计模式
- 面向对象编程的23种设计模式
- 【设计模式笔记】面向对象编程的理解
- Your build settings specify a provisioning profile with the UUID, no provisioning profile was found
- 堆排序
- Java 8:不要再用循环了
- C基础面试题
- Linux svn迁移备份的三种方法
- 以一个简单的项目来学习面向对象编程(设计模式和多线程)
- 工厂模式
- It is indirectly referenced from required .class files。的解决方法
- 得到AndroidManifest.xml中activity配置的meta-data的值
- iOS8本地通知异常
- 队列的链接存储结构——链队列 图解和代码实现
- (4.2.2.2)【android开源工具】【Android UI设计与开发】第17期:滑动菜单栏(二)开源项目SlidingMenu的示例
- state框架的学习
- NFS服务器安装配置实现Ubuntu 12.04与ARM开发板文件共享