什么是对象的状态?
大部分的对象都有内部状态,对象对于讯息的反应都是根据其状态来决定的,例如:
- 我会接电话,可是当电话铃声响起时我去不去接电话则必须看我当时忙不忙而定
- 电视机通常有 power/channel(+-)/volume(+-)/setup 几个按键,但是 channel(+-)/volume(+-)/setup
按键通常需要在 power on 的状况下才有作用, channel(+-)/volume(+-) 按键一般情况下是调整频道和音量的,但是在
setup 按键被按下后 (进入设定模式),通常又都有另外的功能。
- 利用 iostream 函式库做程序输入输出的动作时,透过一个 ifstream 的对象可以读取档案内的资料流,可是这个对象必须要代表一个已经开启而且还没有读到
EOF 的档案才可以
- stack 对象可以让我们 push 数据进去,可是 stack 满了的话就没办法 push 数据进去了,可以让我们 pop
数据出来,可是 stack 空了的话就没办法 pop 资料出来了
注意:
- 只有功能很单纯的对象是没有状态的,这种对象不管在任何时候接到讯息的反应都是一模一样的,有内部状态的对象其接收讯息的时序是很重要的
- 由对象的外部是没办法很容易地看出对象的状态来的,除非透过对象定义的特殊界面,或是长时间地观察对象的表现。
对象界面的使用方法是其内部状态的外在表现:
公用成员函式与其预设的呼叫顺序都是对象界面 (interface) 的一部份,其中 "函式呼叫的顺序"
就代表了对象内部状态的存在。使用一个对象时必须透过这一组界面、遵循其使用规则才可以正确地得到此对象所提供的功能。实作一个对象的时候,必须保证
"不管其它对象如何使用此组界面,对象都需要正确、合理地运作,不能够随便就当掉而有不合理的反应"
例如:
class MyClass
{
public:
void open();
void connect();
void read();
void write();
void disconnect();
void close();
private:
...
};
使用规则:对象必须在没有开启的状况下呼叫 open() 才有作用, 必须要已经 open() 过后才能够呼叫 connect(),必须
要是 connected 的状况下才能做 read/write/disconnect, 必须要是 disconnected 且有
open 过才能呼叫 close()
正确用法: |
|
错误用法: |
MyClass obj;
obj.open();
obj.connect();
obj.read();
obj.disconnect();
obj.close();
|
|
MyClass obj;
obj.open();
obj.read();
|
直觉地实作对象的状态:
在需要记录状态的地方以布尔变量来记录
void
open()
{
if (!m_fOpen)
{
m_fOpen
= true;
do_open();
}
}
|
|
void
close()
{
if ((m_fOpen)&&(!m_fConnected))
{
m_fOpen
= false;
do_close();
}
} |
void
connect()
{
if ((m_fOpen)&&(!m_fConnected))
{
m_fConnected
= true;
do_connect();
}
} |
|
void
disconnect()
{
if (m_fConnected)
{
m_fConnected
= false;
do_disconnect();
}
} |
void
read()
{
if (m_fConnected)
do_read();
} |
|
void
write()
{
if (m_fConnected)
do_write();
} |
上面程序的考虑周全了吗? 两个 flag 共有 4 种状况,可是每一个讯息处理函式中似乎都只考虑到部份的状况,其它的状况呢?
以状态图来全盘描述对象的动态反应:
状态图最大的好处在于 "完整地描述对象所有可能发生的状况",注意下图中必须画出所有的状态,同时每一个状态下
outgoing branch 需要包含所有可能出现的事件 (讯息)
首先 m_fOpen 及 m_fConnected 两个 flag 可以定义出 4 种状况,其中有意义的有三种如下表,表中并指定每一种状况一个状态的名称:
m_fOpen |
m_fConnected |
|
对应状态 |
false |
false |
|
Closed |
true |
false |
|
Opened |
true |
true |
|
Connected |
第四种状态 m_fOpen==false, m_fConnected==true 是不可能发生的状态,不予考虑。
在下面的状态图中我们考虑三种状态以及六种输入讯息 (事件),这三种状态是 Closed, Opened, 及 Connected,六种讯息是
open, connect, read, write, disconnect, close
实作状态图表示的对象:
用单一的状态变量来实作状态图中对象的所有状态
制作讯息 open 处理函式 open() 的方法如下:
在状态图中找到所有的 open 讯息 (我们知道 OO 中对象与对象的沟通是藉由事件传递来完成的),针对每一个不同的状态来制作其响应动作,只有标注为
NOP 的状态才能略过不予实作
void open()
{
if
(m_state == Closed)
{
do_open();
m_state
= Opened;
}
}
|
|
void close()
{
if
(m_state == Opened)
{
do_close();
m_state
= Closed;
}
} |
void connect()
{
if (m_state == Opened)
{
do_connect();
m_state
= Connected;
}
} |
|
void disconnect()
{
if
(m_state == Connected)
{
do_disconnect();
m_state
= Opened;
}
} |
void read()
{
if
(m_state == Connected)
do_read();
} |
|
void write()
{
if
(m_state == Connected)
do_write();
} |
注意:
假设更改一点点系统的要求:此对象允许在联机的状况下直接处理 close 讯息的话,状态图修改如下图左所示, close()
处理函式可以改写为下图右:
|
void close()
{
if
(m_state == Opened)
{
do_close();
m_state
= Closed;
}
else
if (m_state == Connected)
{
do_disconnect();
do_close();
m_state = Closed;
}
} |
结论:
在制作每一个类别的时候,如果能够清楚的分析条列此类别对象的
- 所有能够接受的讯息,
- 对象所有的状态,
- 对象在不同状态下对各种讯息的反应
在任何一种状态下应该要考虑所有可能收到的讯息,才不会在某些没有考虑到的操作状况下产生不合适的反应。
类别关系图及对象关系图是设计程序者与使用此应用程序的人心中共同的静态应用模型 (conceptual model),必需反映实际世界中真实的系统运作模型,每一个不同的对象的状态图则是该对象的完整动态模型,这两种图可以说完整地刻划了整个对象系统,一般同一个程序计划中不同的设计人员在讨论时并不直接交换程序代码,而是共同讨论这些图片,这些图片也可以和提出系统需求的使用者来沟通,同时也是系统标准文件中最重要的一部份。
基本上每一个类别是一个模块,和外界其它的对象之间只透过定义好的界面来沟通,在制作这个类别中的事件处理函式时不应该假设其它对象会以某种特定的方法来使用此对象,这也就是为什么在每一个状态下我们都需要考虑所有可能发生的讯息的原因,在很多情况下,对象不会去处理一些不该出现的讯息,也就是状态图中标示
NOP 的意义,但是这些不该出现的讯息,常常也设计成导致一些警告的讯息来提醒其它的对象他们的使用方式有误,例如上例中:
这样子设计出来的对象的独立性很高,可以提供给任意其它的对象来组合系统,各种可能出现的状况都已经考虑到了,不会有什么意外的状况发生。
|