以一个简单的项目来学习面向对象编程(设计模式和多线程)

下面的项目是两年前学校老师布置的一个小项目,当时自己用了一种很笨拙的方式实现了,现在用面向对象的思想和多线程重构这个项目。

问题描述:

西宝高速仿真模拟

西安市到宝鸡市之间是我省主要的高速公路客运路线之一,经过简化后的客运路线端点、中途停靠点和里程如下图所示(括号里是简称,里程的单位是公里):

  • 限定条件

    (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++实现全部功能。

时间: 2024-10-25 05:57:22

以一个简单的项目来学习面向对象编程(设计模式和多线程)的相关文章

c# 自己制作一个简单的项目倒计时器

using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; namespace date { public partial class Form1 : Form { public Form1() { InitializeCompo

一个简单的项目流程

一个简单的项目流程 一.需求分析 二.设计 技术选型 数据库设计 三.开发 环境搭建 编码 四.测试 五.部署运维 原文地址:https://www.cnblogs.com/zhuobo/p/10806758.html

Django入门第一步(安装和创建一个简单的项目)

目录 Django入门第一步(安装和创建一个简单的项目) 一. Django项目目录结构 二.注意事项 三.Django安装 3.1.安装命令 3.2.验证django是否安装成功 3.3.使用方法 Django入门第一步(安装和创建一个简单的项目) 在使用Django框架开发web应用程序时,开发阶段同样依赖wsgiref模块来实现Server的功能,我们使用Django框架是为了快速地开发application. 如果使用的是我们自定义的框架来开发web应用,需要事先生成框架包含的一系列基础

少走弯路去学习面向对象编程

少走弯路去学习面向对象编程 如何学习面向对象编程 在学习面向对象的过程中,我自己也走了很多的弯路.一般来讲,接触面向对象是做为编程语言的一部分.那时候认为在程序中写一个 Class 关键字就是面向对象,写Class A: Class B就是面向对象的继承.实际从编程语言的角度来理解是无法掌握面向对象的精髓的,在这里我想以我的经验来告诉大家我的一些方法. 面向对象是一种思想理论,要远远高于编程语言.不深入理解面向对象理论就无法做到编写比较高级的软件,而且往往这些软件不能有效应对变化,不能复用,不能

学习面向对象编程OOP 第一天

面向对象编程 Object Oriented Programming 一.什么是面向对象编程OOP 1.计算机编程架构; 2.计算机程序是由一个能够起到子程序作用的单元或者对象组合而成.也就是说由多个程序单元可以拼凑成一个完整的功能程序; 3.三个目标:重用性.灵活性和扩展性; 4.每个单独的对象或者单元都可以实现数据的接收.处理和发送; 5.在实际的项目开发中,都会使用达到OOP去声明类,而且在项目里面只用对象和类. 详细参考网址(根据原文学习的) http://www.cnblogs.com

从一些简单代码实例彻底理解面向对象编程思想|OOP本质是什么?

从Rob Pike 的 Google+上的一个推看到了一篇叫<Understanding Object Oriented Programming>的文章,我先把这篇文章简述一下,然后再说说老牌黑客Rob Pike的评论. 先看这篇教程是怎么来讲述OOP的.它先给了下面这个问题,这个问题需要输出一段关于操作系统的文字:假设Unix很不错,Windows很差. 这个把下面这段代码描述成是Hacker Solution.(这帮人觉得下面这叫黑客?我估计这帮人真是没看过C语言的代码) 1 2 3 4

iOS从零开始学习socket编程——高并发多线程服务器

在上一篇文章<iOS从零开始学习socket编程--HTTP1.0服务器端>中我们已经简单的接触了OC搭建的HTTP服务器. (地址http://blog.csdn.net/abc649395594/article/details/45131373) 出于用户体验和鲁棒性考虑,这里把这个HTTP服务器改进成多线程的. 首先,AnsycSocket这个类是基于OC的Runloop实现的,Runloop实现了方法的异步调用但并不支持多线程. 在这里首先简单区分一下多线程和方法异步调用的区别.他们都

面向对象编程——设计模式之一

一.面向对象 面向对象,就是将一些属性和行为封装成对象,对于使用者来说不用关心对象的内部具体实现,只管调用它公开的属性方法就行了.同一类型的对象抽象出来就是类.类是对象的模板. 面向对象编程,共三步: 1.设计系统的类和接口: 2.设计类和接口的方法和属性: 3.建立类类和接口之间的关系(继承.实现.依赖.关联(聚合.组合关系)): 类和接口之间的关系请参考:http://www.cnblogs.com/liuling/archive/2013/05/03/classrelation.html

spring security+mybatis+springMVC构建一个简单的项目

1.引用 spring security ,这是一种基于spring AOP和Servlet的过滤安全框架.它提供全面的安全性解决方案,同时在web请求级和方法的调用级处理身份确认和授权.在spring framework基础上,spring security充分利用了依赖注入(DI,Dependency Injection)和AOP技术. 下面就让我们用一个小的晓得项目来出初步了解Spring Security 的强大功能吧. 2.项目实战    1)项目的技术架构:maven+spring