65.9K
CodeProject 正在变化。 阅读更多。
Home

面向对象的状态逻辑实现

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (14投票s)

2004年7月20日

6分钟阅读

viewsIcon

92106

downloadIcon

791

本文解决了将面向对象和基于自动机的编程技术相结合的问题。

摘要

本文解决了将面向对象和基于自动机的编程技术相结合的问题。主要有两个问题

  1. “如何将自动机集成到面向对象的系统中?”  
  2. “如何以面向对象的方式实现自动机?”

面向对象和基于自动机的编程技术相结合的问题在文献中经常被分析[1-4]。本文仅对该问题进行简要介绍。

引言

传统的面向对象系统中,对象通过它们的public接口相互通信。每个接口都是对象与其客户端之间的契约。接口公开一个或多个方法。方法可以被视为带有参数和返回值的事件(消息)。

系统经常包含一个或多个基于状态的对象。这些对象的行为取决于它们的状态,其中状态可以表示为某种标量值。表示这种行为的一种方式是有限状态机。

基于状态的对象可以采用以下三层结构实现

  • 传统的面向对象接口
  • 将方法转换为事件的中间层
  • 底层的基于自动机的逻辑

所提出的方法符合主要的面向对象范例原则。

  • 封装:基于状态的行为由环境隐藏。
  • 多态:如果存在许多具有不同行为但具有共同接口的基于状态的对象,则用户可以以统一的方式与之交互。
  • 继承:可以使用普通的继承机制来扩展基于状态对象的行为。

面向对象接口

基于状态对象的接口没有任何不方便之处。这些接口可以包含许多方法。方法可以接受许多参数,并可以具有返回值。某些方法可以标记为常量。

例如,下面展示了用于控制文件访问权限的接口

struct IFileAccess {
  virtual void Open( string const& _mode ) = 0;
  virtual void Close() = 0;
  virtual bool CanRead() const = 0;
  virtual bool CanWrite() const = 0;
};

中间层

这一层是一个常规。它只是一个接口实现,其中每个方法调用都会被转换为一个事件对象,该对象会被传播到底层逻辑层,然后,处理完毕后,返回值和输出参数会返回给调用者。

此类应如图所示,派生自接口和通用实现设施

图 1. 中间层继承模型

首先,应为每个方法定义一个特定的事件类型。这些类型中的每一种都应派生自StateBased::Event类型。这些类型的结构没有更多限制。这些类型可以声明在FileAccessBase类内部。

class FileAccessBase : public virtual IFileAccess,
                       protected virtual StateBased {
protected:
  struct EOpen : public Event {
    EOpen( string const& _mode ) : mode( _mode ) {}

    string const& GetMode() const {
      return mode;
    }
  private:      
    EOpen& operator=( EOpen const& );
    string const mode;
  };

  struct EClose : public Event {};

  struct BoolEvent : public Event {
    BoolEvent() : result( false ) {}

    bool GetResult() {
      return result;
    }

    bool SetResult( bool _result ) {
      result = _result;
      return true;
    }
  private:
    bool result;
  };

  struct ECanRead : public BoolEvent {};
  struct ECanWrite : public BoolEvent {};
/*...*/
};

其次,应实现接口类中的所有方法。这些实现具有相似的结构:在堆栈上创建一个相应的事件对象,并将其传递给StateBased::Execute()方法。

class FileAccessBase : public virtual IFileAccess,
                       protected virtual StateBased {
  /*...*/
public:
  virtual void Open( string const& _mode ) {
    EOpen event( _mode );
    Execute( event );
  }

  virtual void Close() {
    EClose event;
    Execute( event );
  }

  virtual bool CanRead() const {
    ECanRead event;
    Execute( event );
    return event.GetResult();
  }

  virtual bool CanWrite() const {
    ECanWrite event;
    Execute( event );
    return event.GetResult();
  }
};

基于自动机的逻辑

有一种易于人类阅读的方式来表示自动机的逻辑——状态转换图。此方法用于 Statecharts [5]、SyncCharts [6]、SWITCH-technology [7] 等。状态转换图中的每个自动机状态绘制为圆角矩形。状态之间的每个转换绘制为箭头。箭头以<condition>/[<action>]格式标记。括号表示操作部分是可选的。

下面展示了上述示例的一种可能的状态转换图。此逻辑允许我们拥有两种文件打开模式——readingwriting

图 2. FileAccess 类的状态转换图

实现自动机主要有两种方法

  • 使用动态函数表
  • 使用带有嵌套if条件的switch运算符(此方法用于 SWITCH-technology)

本文提供了一种新的方法。该方法基于内置的虚方法机制。自动机状态空间映射到虚方法集。这些方法称为状态方法。所有状态方法都具有相同的签名。每个自动机状态对应一个唯一的状态方法,反之亦然。基于状态对象的实例将其当前状态保存为对当前状态方法的引用。有一个特殊的状态方法称为main,它被视为初始状态。

在这些假设下,图 2 中的自动机可以轻松实现如下

class FileAccess : public FileAccessBase {
protected:
  virtual bool main( Event& _event ) {
    if ( EOpen* e = EventCast< EOpen >( _event ) ) {
      if ( e->GetMode() == "r" )
        return SetState( *this, &FileAccess::reading );
      else if ( e->GetMode() == "w" )
        return SetState( *this, &FileAccess::writing );
    }
    return false;
  }

  virtual bool reading( Event& _event ) {
    if ( ECanRead* e = EventCast< ECanRead >( _event ) )
      return e->SetResult( true );
    else if ( ECanWrite* e = EventCast< ECanWrite >( _event ) )
      return e->SetResult( false );
    else if ( EClose* e = EventCast< EClose >( _event ) )
      return SetState( *this, &FileAccess::main );
    return false;
  }

  virtual bool writing( Event& _event ) {
    if ( ECanRead* e = EventCast< ECanRead >( _event ) )
      return e->SetResult( false );
    else if ( ECanWrite* e = EventCast< ECanWrite >( _event ) )
      return e->SetResult( true );
    else if ( EClose* e = EventCast< EClose >( _event ) )
      return SetState( *this, &FileAccess::main );
    return false;
  }
};

每个状态方法应返回true表示已完成转换,否则返回false。假定如果方法调用没有完成任何转换,则会抛出StateBase::UnexpectedOperation异常。

通过继承扩展逻辑

该方法的主要优点是可以使用继承。特定状态下对象的行为可以被更改或扩展。可以在自动机中添加新状态。

例如,我们可以通过添加混合读/写模式来扩展我们的FileAccess对象。相应的自动机如下所示

图 3. RWFileAccess 类的状态转换图

状态main上方的星号表示该状态已扩展,即main到其他状态的所有转换都保存在新自动机中。文件访问控制的扩展版本代码如下所示

class RWFileAccess : public FileAccess {
protected:
  virtual bool main( Event& _event ) {
    if ( FileAccess::main( _event ) )
      return true;
    if ( EOpen* e = EventCast< EOpen >( _event ) )
      if ( e->GetMode() == "rw" )
        return SetState( *this, &RWFileAccess::readwriting );
    return false;
  }

  virtual bool readwriting( Event& _event ) {
    if ( ECanRead* e = EventCast< ECanRead >( _event ) )
      return e->SetResult( true );
    else if ( ECanWrite* e = EventCast< ECanWrite >( _event ) )
      return e->SetResult( true );
    else if ( EClose* e = EventCast< EClose >( _event ) )
      return SetState( *this, &RWFileAccess::main );
    return false;
  }
};

此外,还可以使用多重继承。可以将几个自动机的逻辑合并在一起。此选项有两个主要要求

  • 接口和StateBased类必须是虚继承的
  • 初始状态方法main应该被覆盖以避免歧义

例如,假设我们有一个AppendFileAccess类,其状态转换图如下

图 4. AppendFileAccess 类的状态转换图

此类代码如下所示

class AppendFileAccess : public FileAccessBase {
protected:
  virtual bool main( Event& _event ) {
    if ( EOpen* e = EventCast< EOpen >( _event ) )
      if ( e->GetMode() == "a" )
        return SetState( *this, &AppendFileAccess::appending );
    return false;
  }

  virtual bool appending( Event& _event ) {
    if ( ECanRead* e = EventCast< ECanRead >( _event ) )
      return e->SetResult( false );
    else if ( ECanWrite* e = EventCast< ECanWrite >( _event ) )
      return e->SetResult( true );
    else if ( EClose* e = EventCast< EClose >( _event ) )
      return SetState( *this, &AppendFileAccess::main );
    return false;
  }
};

接下来,我们可以将AppendFileAccessRWFileAccess合并到一个基于状态的对象中,该对象可以工作在四种模式下——readingwritingreadwritingappending。相应的自动机如下所示

图 5. RWAFileAccess 类的状态转换图

这个类的代码非常简洁

class RWAFileAccess : public RWFileAccess, public AppendFileAccess {
protected:
  virtual bool main( Event& _event ) {
    if ( RWFileAccess::main( _event ) )
      return true;
    if ( AppendFileAccess::main( _event ) )
      return true;
    return false;
  }
};

后端设施

有一个名为StateBased的后端类。所有基于状态的类都必须派生自它。StateBased类提供以下功能

  • 所有事件对象的基础类
  • 供中间层使用的Execute()方法的const和非const版本
  • 供用于更改对象状态的SetState()方法
  • 供用于确定事件对象特定类型的EventCast()模板方法

维护不变性

不幸的是,对象不变性的自动控制被破坏了。在构造一个基于状态的对象实例期间,会记住对其的非const引用。即使您调用一个const接口方法——仍然使用非const引用来调用state方法。

但是,StateBased类在运行时自动控制对象state的不变性。如果调用了const接口方法,并且当前状态方法尝试调用SetState()方法,则会抛出StateBased::ReadOnlyViolation异常。

但是,如果类中存在用户数据,则必须手动维护不变性。如果请求对数据的非const访问,并且StateBased::IsImmutable()true,则应抛出ReadOnlyViolation

结论

所提出的方法在以下条件下非常适用

  • 有许多具有相同接口但行为不同的对象
  • 可以为这些对象引入逻辑层次结构
  • 对象中除了状态外的数据很少(没有数据是理想情况)

参考文献

  1. Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides Design Patterns - Elements of Reusable Object-Oriented Software. Addison-Wesley, 1995
  2. Kevlin Henney State Government, C/C++ Users Journal C++ Experts Forum, June 2002
  3. Ted Faison Object-Oriented State Machines, Software Development Magazine, Sept, 1993
  4. Paul Adamczyk The Anthology of the Finite State Machine Design Patterns, The 10th Conference on Pattern Languages of Programs 2003
  5. Harel D. Statecharts: A visual formalism for complex systems. Sci. Comput. Program., vol. 8, June 1987. pp. 231-274
  6. Andre C. Representation and Analysis of Reactive Behaviors: A Synchronous Approach. CESA96, Lille, France, IEEE-SMC, July 1996
  7. Shalyto A.A. Switch-technology. Algorithmization and programming of tasks of logical control. SPb.: Nauka (Science), 1998

许可证

本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。

作者可能使用的许可证列表可以在此处找到。

© . All rights reserved.