设计模式–状态
通常,计算机软件基于称为状态的条件进行操作。 这些状态传统上是使用switch语句实现的。 开关的情况代表了各种状态。 以下示例可用于读取光盘文件
int state = 1;
switch (state)
{
case 1:
//open the file
state = 2;
break;
case 2;
//read a record from the file
if end of file
{
state = 4;
break;
}
state =3;
break;
......case 3:
........ //displays the record
state = 2;
break;
case 4:
//close the file
state = 0;
break;
}
在上面的开关中,用户设置初始状态以打开文件。
打开文件后,状态变量将更改为读取文件的状态。
读取状态检查文件结尾,如果未到达文件结尾,则将状态更改为显示状态。
显示状态显示记录,然后将状态设置回读取状态,以读取下一条记录的文件。
在读取状态下,如果已到达文件末尾,则状态将更改为关闭文件。
关闭文件后,状态将更改为无状态,并且退出开关。
请注意,所需要做的就是每个案例(状态)必须知道以下状态。
这是用于从光盘读取文本文件的上述开关。 假设光盘文件包含:
这是
我最容易的事情
见过
该代码将是:
int main()
{
fstream input;
int state = 1;
char buffer[256];
while (state)
{
switch (state)
{
case 1:
//open the file
input.open("c:\\scratch\\instructor\\input.txt", ios::in);
state = 2;
break;
case 2:
//read a record from the file
input.getline(buffer, 256);
if (input.eof() == true)
{
state = 4;
break;
}
state = 3;\
break;
case 3:
//displays the record
cout << buffer << endl;
state = 2;
break;
case 4:
//close the file
input.close();
state = 0;
break;
} //end of switch
} //end of while
}
用户设置初始状态,然后编写一个循环,只要存在状态(switch语句中的情况),该循环就会继续。
每个案例都执行其动作,然后将状态更改为后继状态。
自己尝试此代码。 编译并运行它,直到您看到案例如何协同工作。
局限性切换方法(一种转到编程方式)的局限性在于,如果需要添加或删除任何状态,则需要更改切换。 此外,开关的用户需要了解状态。 也就是说,没有上下文。 一切都无需封装就可以完成。
对于大量已安装的软件,更改开关将导致需要重新安装用户群。 也就是说,由变化引起的涟漪具有海啸的比例。
面向对象的方法–第1部分切换真正发生的是,处理根据状态而变化。 在面向对象的世界中,行为的改变意味着对象的类型有所不同。 在这种情况下,需要的是一个看起来会根据其内部状态更改其类型的对象。
该对象称为
上下文对象 ,它包含状态以及执行操作的请求。 用户仅创建上下文对象并传递服务请求。为了说明这一点,下面的示例将以面向对象的方式实现该开关。
int main()
{
FileRead context;
context.Request("c:\\scratch\\instructor\\input.txt");
context.Process();
} //end of main
首先,创建上下文对象,然后提交服务请求。
在这种情况下,请求是要读取的光盘文件的名称。
然后,要求上下文对象处理请求。
结果将是屏幕上显示光盘记录。
这里没有关于状态的任何信息。 用户摆脱了这种负担。
上下文类是:
class FileRead
{
private:
State* CurrentState;
fstream TheFile;
string request;
public:
FileRead();
~FileRead();
void Request(string name);
void Process();
void ChangeState(State* theNewState);
fstream& GetFileStream();
};
上下文对象包含其内部状态为CurrentState。
当上下文对象处理请求时,正是该成员需要更改类型。
由于这是一个文件读取上下文对象,因此它还包含文件流和用于将信息传递给CurrentState的请求。
该请求的含义将根据CurrentState的类型而有所不同。
有一种接收服务请求的方法和一种处理该请求的方法。有一种通过更改CurrentState指向的对象类型来更改内部状态的方法。
最后,有一种方法可以返回对文件流的引用。
这些方法的实现可能如下:
FileRead::FileRead() : CurrentState(0)
{
}
void FileRead::Process()
{
ChangeState(new OpenState);
string token;
while (CurrentState)
{
CurrentState->DoProcess(this->request, *this, token);
}
}
void FileRead::ChangeState(State* theNewState)
{
delete CurrentState;
CurrentState = theNewState;
}
fstream& FileRead::GetFileStream()
{
return this->TheFile;
}
void FileRead::Request(string name)
{
this->request = name;
}
FileRead::~FileRead()
{
delete CurrentState;
}
实际状态是一个以State为基类的多态层次结构。
State类具有一个虚拟的析构函数,可以将其用作多态基类。
唯一的方法是接收请求,上下文对象和令牌的处理方法。
class State
{
public:
virtual ~State(); //a signal that it's OK to derive from this class.
//the derived object's process
virtual void DoProcess(string& request, FileRead& context, string& token) = 0;
};
State::~State()
{
//nothing to do
}
这些是各种状态的派生类。
DoProcess方法是State基类DoProcess方法的重写。
覆盖在派生类中是私有的,强制调用来自状态指针或引用。
这些导出状态不能直接使用。
在处理请求时,可以根据需要使用令牌参数来在状态之间传递信息。
class OpenState :public State
{
private:
virtual void DoProcess(string& request, FileRead& context, string& token);
};
class ReadState :public State
{
private:
virtual void DoProcess(string& request, FileRead& context, string& token);
};
class DisplayState :public State
{
private:
virtual void DoProcess(string& request, FileRead& context, string& token);
};
class ExitState :public State
{
private:
virtual void DoProcess(string& request, FileRead& context, string& token);
};
这些是各种状态的DoProcess实现。
您可以查看请求根据状态如何变化。
OpenState将请求视为要打开的文件名。
ReadState将其视为返回已读记录的一种方式。
void OpenState::DoProcess(string& request, FileRead& context, string& token)
{
context.GetFileStream().open(request.c_str(), ios::in);
context.ChangeState(new ReadState);
}
void ReadState::DoProcess(string& request, FileRead& context, string& token)
{
char buffer[256];
context.GetFileStream().getline(buffer, 256);
if (context.GetFileStream().eof() == true)
{
context.ChangeState(new ExitState);
return;
}
request = buffer;
token = buffer;
context.ChangeState(new DisplayState);
}
void DisplayState::DoProcess(string& request, FileRead& context, string& token)
{
if (token.size())
{
cout << token << endl;
}
context.ChangeState(new ReadState);
}
void ExitState::DoProcess(string& request, FileRead& context, string& token)
{
context.GetFileStream().close();
request.erase();
token.erase();
context.ChangeState(0);
}
您应该能够汇编这些代码示例,并能够读取和显示具有256个字节或更少记录的任何文本文件。
稍加改进,便可以删除256个字节的限制。 然后可以添加状态以写入文件。 显示屏可能会被移除。 最终产品将是一个可以读写任何文件的对象。
面向对象的方法–第2部分需求经常发生变化。 上面的示例显示从文件读取的记录,每行一条记录。 新要求要求每个记录的每个单词都显示在单独的行上。
为了满足此要求,需要添加一些新状态。 此外,现有状态可能具有新的后续状态。
为了将记录处理成单词,添加了两个状态。 一种是WhiteSpace状态,删除前导空白字符。 第二个单词是WordState,它将单词从记录中分解出来并存储在令牌中。
这些新类如下所示:
class WhiteSpaceState :public State
{
private:
virtual void DoProcess(string& request, FileRead& context, string& token);
};
class WordState :public State
{
private:
virtual void DoProcess(string& request, FileRead& context, string& token);
};
DoProcess方法如下所示:
void WhiteSpaceState::DoProcess(string& request, FileRead& context, string& token)
{
while (isspace(request[0]) )
{
request.erase(0,1);
}
if (request.size())
{
context.ChangeState(new WordState);
}
else
{
context.ChangeState(new ReadState);
return;
}
}
void WordState::DoProcess(string& request, FileRead& context, string& token)
{
token.erase();
while (request.size() && !isspace(request[0]))
{
token +=request[0];
request.erase(0, 1);
}
//This is the successor state
context.ChangeState (new DisplayState);
}
WhiteSpaceState :: DoProcess确定是否存在请求。
如果不是,则状态更改为ReadState。
否则,将从请求中删除前导空格字符,并将状态更改为WordState。
WordState :: DoProcess会将单词放入令牌中。 它只是逐个字符地移动,直到遇到空白字符为止。 将单词放入令牌中后,状态将更改为DisplayState,以便可以在屏幕上显示单词。
为了支持这些新状态,一些现有状态现在具有新的后续状态。 下面显示的是需要更改的DoProcess方法。 ReadState :: DoProcess现在具有WhiteSpaceState后继程序,因此该记录(请求)将被处理为单词(令牌)。 DisplayState :: DoProcess也具有WhiteSpaceState的后继状态以获取下一个标记。
下面显示的是第一个示例中的方法,其中旧状态被注释掉,新状态被添加。
void ReadState::DoProcess(string& request, FileRead& context, string& token)
{
char buffer[256];
context.GetFileStream().getline(buffer, 256);
if (context.GetFileStream().eof() == true)
{
context.ChangeState(new ExitState);
return;
}
request = buffer;
token = buffer;
context.ChangeState(new WhiteSpaceState);
//context.ChangeState(new DisplayState);
}
void DisplayState::DoProcess(string& request, FileRead& context, string& token)
{
if (token.size())
{
cout << token << endl;
}
context.ChangeState(new WhiteSpaceState);
//context.ChangeState(new ReadState);
}
请注意,添加这些状态后,FileRead类完全没有变化。
这意味着main()也没有变化。
除此之外,现有状态的唯一变化是确定新的后续状态。
新的分词功能是通过在原始应用程序中仅更改两行代码来实现的。
单词突破要求的其余部分是用新代码实现的。
通过将各种状态的详细信息隐藏在派生类中,可以实现高度的封装。 这减少了由变化引起的波纹。
使用手柄由于指针语法是众所周知的,因此本文中的示例使用指针。 但是,建议在实际使用的应用程序中使用。 您应该参考“ C / C ++文章”部分中有关“句柄”的文章。
更多的信息请参阅Erich Fromm等人的《设计模式》一书,Addison-Wesley,1994年。
本文仅显示国家模式的概念基础,而没有显示使用此模式的动机和后果。
版权所有2007 Buchmiller Technical Associates美国北华盛顿本德
From: https://bytes.com/topic/c/insights/660060-design-patterns-state