17Th
Developer Camp

【G6】ライトニングトーク

StateパターンをDelphiで実装する

東洋テクニカルシステム株式会社
システム開発部
福士 光

1

17Th
Developer Camp

1

基本知識

2

状態とは？
• 状態(State)とは？
– 同じ入力に対しても状態が異なれば振る舞いも異なる。
同じ入力に対しても状態が異なれば振る舞いも異なる• 同じ話を同じ人にしても、そのときの気分(=状態)で反応が異なる、みたいなこと。

3

状態の遷移とは？
• 状態の遷移とは– 外部からの入力や内部的な動作によって状態が変化して外部からの入力や内部的な動作によ て状態が変化してゆくこと。
– 状態とその遷移を考えるときは、一定の間そこに留まるも状態とその遷移を考えるときは、 定の間そ に留まるものだけを『状態』として考える。
• 一瞬だけその状態に属し、何かをし終わったら別の状態に遷移する といったものは基本的に状態として扱わない
遷移する、といったものは基本的に状態として扱わない。

4

デザインパターンとは？
• パターン(パターンランゲージ)とは？
– もともとは建築家のクリストファー
もともとは建築家のクリストファ アレグザンダー
アレグザンダ(Christopher Alexander) が建築物をデザインするときの手法として考案した。
– ソフトウェアに対してパターンという考え方を適用した。
– 問題とその解決方法、そして適用した結果やトレード
オフ そしてそれらの組み合わせに名前をつけたもの
オフ、そしてそれらの組み合わせに名前をつけたもの。

• デザインパターンとは？
– デザイン(設計)における問題を解決して適切な構造をコードに与えるための手法。

5

GoFのデザインパターンとは？
• GoF(Gang of Four)のデザインパターンとは？
– オブジェクト指向の設計に対してデザインパターンを
オブジ クト指向の設計に対してデザインパタ ンを適用。
– 設計上の課題とその典型的な解決方法(コーディング)に設
課題 そ 典
解決方法(
ィ)
名前をつけたもの。
– 3つのカテゴリー、23のパターンに分類している。
– あくまで一つの考え方です(絶対的なものとして捉えないあくま
考 方 す 絶 的なも捉 なほうがいいでしょう)。
– GoF本(参考文献(1))の著者であるErich Gamma
Gamma、Richard Helm、Ralph Johnson、John M. Vlissidesの4人をGang of Fourと呼んでいます。
6

Stateパターンとは？
• Stateパターンとは？
– GoFの『振る舞いに関する』パターンの一つ。
GoFの『振る舞いに関する』パタ ンの つ– オブジェクトが内部状態に従って振る舞いを変えるような状況状況に適用する。
用す 。
– 詳細はGoF本(と付属のCD-ROM)をお読みください。
– Delphiではクラス参照型の変数とその仮想関数が有効に機能す機能するので、C++のように状態毎のインスタンスを
う 状態毎
タ
を
生成しない実装が可能(状態の持つ内部情報はコンテキスト側に保持する)。
ンテキ ト側に保持する)。

7

Stateパターンを使わない場合• if文やcase文で実装(発生事象と状態で二重になる？)。
• あるいは関数テーブルで実装。
あるいは関数テ ブルで実装• Stateパターンの実装は関数テーブルをDelphiのVMT(仮想メソッドテーブル)に任せたもの(仮想メソッドテブル)に任せたもの、と考えることがと考えることができるかも。

8

17Th
Developer Camp2

サンプルについて9サンプルについて• 今回のサンプルは…
TCP/IPで接続して郵便番号を問い合わせると該当する住所を返してくれるサーバがあり、このサーバとの通信を行う という要求を想定する。
行う、という要求を想定する。
応答には時間が掛かるかもしれないし、エラー(応答が(不正だったりタイムアウトしたり)が発生するかもしれない。

10

サンプルについて• ポート番号は32100(適当)。
• 手抜きです。電文のデリミタはCR(0x0D)。
手抜きです 電文のデリミタはCR(0 0D)• 要求電文
00

01

02

03

04

05

06

07

08

09

0A

0B

0C

0D

0E

0F

10

0

0

0

0

SP

G

E

T

SP

#

#

#

#

#

#

#

CR

…
CR

シーケンス番号

郵便番号(7桁)

要求• 回答電文
00

01

02

03

04

05

06

07

08

09

…

…

…

…

…

…

0

0

0

0

SP

O

K

SP

SP

X

…

…

…

…

…

…

シ ケンス番号
シーケンス番号

住所(Shift JIS)
住所(Shift_JIS)

応答

00

01

02

03

04

05

06

07

08

09

…

…

…

…

…

…

0

0

0

0

SP

N

G

SP

SP

X

…

…

…

…

…

…

シ ケ
シーケンス番号
番号

応答

…
CR

メ セ ジ
メッセージ
11

17Th
Developer Camp

3

分析

12

分析
• 状態遷移図

理解をまとめる程度、適当でよい

アイドル
Idle
dl

Standby

スタンバイ
Disconnected

Request

接続待ち

切断待ち

Connected

回答待ち

RecvAnswer
Notify13

17Th
Developer Camp

4

設計

14

設計
• 状態マトリクス– 縦方向に状態(→状態クラスとして実装)を、横方向に
縦方向に状態( 状態クラスとして実装)を 横方向に事象(→メソッドとして実装)を配置– グループ化された状態も考慮しておくとよい
状
考慮
No

状態

Entry

Exit

Go IdleGo StandBy

Request addr

Connected

Receive answer

Disconnected

Error

0

Idle

N/A

N/A

無視

→1

無視

無視

無視

無視

無視

1

Stand by

タイマ停止

N/A

→0

無視

→2

無視

無視

無視

無視

(Connected)

タイマ開始

N/A

切断
→0

無視

無視

無視

無視

→1

エラー通知
→1

2

Wait to connect

タイマ開始
接続

N/A

切断
→0

無視

無視

要求送信
→3

無視

→1

エラー通知
→1

3

Wait answer

タイマ開始

N/A

切断
→0

無視

無視

無視

回答通知
→4

→1

エラー通知
→1

4

Wait to disconect

タイマ開始
切断

N/A

切断
→0

無視

無視

無視

無視

→1

エラー通知
→1

15

17Th
Developer Camp

5

実装

16

実装
• 画面
– 都合によりDelphi 2007です– ソースコードはダウンロードできるようにする予定です((テスト用サーバも)用)

17実装 (状態クラスの宣言/1){ TCommStatus : Status class (abstract) }TCommStatus = class(TObject)publicclass function GetDescription: String; virtual;class procedure EntryState(DM: TDataModuleComm); virtual;class procedure ExitState(DM: TDataModuleComm); virtual;class procedure GoIdle(DM: TDataModuleComm); virtual;class procedure GoStandby(DM: TDataModuleComm); virtual;class procedure RequestAddr(DM: TDataModuleComm; const APostalCode: String); virtual;class procedure Connected(DM: TDataModuleComm; Socket: TCustomWinSocket); virtual;class procedure ReceiveAnswer(DM: TDataModuleComm; Socket: TCustomWinSocket;
const RecvData: String); virtual;class procedure Disconnected(DM: TDataModuleComm; Socket: TCustomWinSocket); virtual;class procedure SocketError(DM: TDataModuleComm; Socket: TCustomWinSocket;
ErrorEvent: TErrorEvent; var ErrorCode: Integer); virtual;class procedure ReceiveError(DM: TDataModuleComm; Socket: TCustomWinSocket;
const Data: String); virtual;class pprocedure TimeoutError(DM:(TDataModuleComm);); virtual;
;
end;

18

実装 (状態クラスの宣言/2)
type{ TCommStatusIdle : Idle }TCommStatusIdle = class(TCommStatus)
publicclass function GetDescription: String; override;class procedure GoStandby(DM: TDataModuleComm); override;end;{ TCommStatusStandby : Standby }TCommStatusStandby = class(TCommStatus)
publicclass function GetDescription: String; override;class procedure EntryState(DM: TDataModuleComm); override;class procedure GoIdle(DM: TDataModuleComm); override;class procedure RequestAddr(DM: TDataModuleComm; const APostalCode: String); override;end;{ TCommStatusConnected : Connencted (abstract)() }TCommStatusConnected = class(TCommStatus)
publicclass procedure EntryState(DM: TDataModuleComm); override;class procedure GoIdle(DM: TDataModuleComm); override;class procedure Disconnected(DM: TDataModuleComm; Socket: TCustomWinSocket); override;class procedure SocketError(DM: TDataModuleComm; Socket: TCustomWinSocket;
E
ErrorEvent:Et TErrorEvent;TEEt var ErrorCode:E
C dInteger);I t) override;idclass procedure ReceiveError(DM: TDataModuleComm; Socket: TCustomWinSocket;
const Data: String); override;class procedure TimeoutError(DM: TDataModuleComm); override;
end;

19

実装 (状態クラスの宣言/3)
type{ TCommStatusWaitToConnect : Wait to connenct }TCommStatusWaitToConnect = class(TCommStatusConnected)
publicclass function GetDescription: String; override;class procedure EntryState(DM: TDataModuleComm); override;class procedure Connected(DM: TDataModuleComm; Socket: TCustomWinSocket); override;end;{ TCommStatusWaitToAnswer : Wait answer }TCommStatusWaitAnswer = class(TCommStatusConnected)
publicclass function GetDescription: String; override;class procedure ReceiveAnswer(DM: TDataModuleComm; Socket: TCustomWinSocket;
const RecvData: String); override;end;{ TCommStatusWaitToDisconnect :Wait to disconnect }TCommStatusWaitToDisconnect = class(TCommStatusConnected)
publicclass function GetDescription: String; override;class procedure EntryState(DM: TDataModuleComm); override;
end;

20

実装 (状態クラスの定義/1){ TCommStatusIdle }class function TCommStatusIdle.GetDescription: String;
begin
Result := 'アイドル状態';
end;class procedure TCommStatusIdle.GoStandby(DM: TDataModuleComm);
begin
DM.CommStatus := TCommStatusStandby;
end;{ TCommStatusStandby }class function TCommStatusStandby.GetDescription: String;
begin
Result := 'スタンバイ状態';
end;class pprocedure TCommStatusStandby.EntryState(DM:yy(TDataModuleComm););
begin
DM.DoStopTimer;
end;class procedure TCommStatusStandby.GoIdle(DM: TDataModuleComm);
begin
DM C
DM.CommStatus
St t:= TCommStatusIdle;TC
St t Idl
end;class procedure TCommStatusStandby.RequestAddr(DM: TDataModuleComm; const APostalCode: String);
begin
DM.FPostalCode := APostalCode;DM.CommStatus ::= TCommStatusWaitToConnect;
end;

21

実装 (状態クラスの定義/2){ TCommStatusConnected }class procedure TCommStatusConnected.EntryState(DM: TDataModuleComm);
begin
DM.DoStartTimer;
end;class procedure TCommStatusConnected.GoIdle(DM: TDataModuleComm);
begin
DM.DoDisconnect;
DM.CommStatus := TCommStatusIdle;
end;class procedure TCommStatusConnected.Disconnected(DM: TDataModuleComm; Socket: TCustomWinSocket);
begin
DM.CommStatus := TCommStatusStandby;
end;class pprocedure TCommStatusConnected.SocketError(DM:(TDataModuleComm;; Socket: TCustomWinSocket;;
ErrorEvent: TErrorEvent; var ErrorCode: Integer);
begin
DM.NotifySocketError(Socket,ErrorEvent,ErrorCode);
ErrorCode := 0;
DM.DoDisconnect;
DM.CommStatus := TCommStatusStandby;
end;dclass procedure TCommStatusConnected.ReceiveError(DM: TDataModuleComm; Socket: TCustomWinSocket;
const Data: String);
begin
DM.NotifyReceiveError(Socket,Data);
DM.DoDisconnect;
DM.CommStatus := TCommStatusStandby;end;22実装 (状態クラスの定義/3)
class procedure TCommStatusConnected.TimeoutError(DM: TDataModuleComm);
begin
DM.NotifyTimeoutError;
DM.DoDisconnect;
DM.CommStatus := TCommStatusStandby;
end;{ TCommStatusWaitToConnect }class function TCommStatusWaitToConnect.GetDescription: String;
begin
Result := '接続待機状態';
end;class procedure TCommStatusWaitToConnect.EntryState(DM: TDataModuleComm);
begin
inherited;
DM.DoConnect;end;;class procedure TCommStatusWaitToConnect.Connected(DM: TDataModuleComm; Socket: TCustomWinSocket);
begin
DM.DoSendRequest(Socket);
DM.CommStatus := TCommStatusWaitAnswer;
end;

23

実装 (状態クラスの定義/4){ TCommStatusWaitAnswer }class function TCommStatusWaitAnswer.GetDescription: String;
begin
Result := '回答電文受信待ち状態';
end;class procedure TCommStatusWaitAnswer.ReceiveAnswer(DM: TDataModuleComm; Socket: TCustomWinSocket; const
RecvData: String);
begin
DM.NotifyReceiveData(Socket,RecvData);
DM.CommStatus := TCommStatusWaitToDisconnect;end;{ TCommStatusWaitToDisconnect }class function TCommStatusWaitToDisconnect.GetDescription: String;
begin
Result := '切断待機状態';
end;;class procedure TCommStatusWaitToDisconnect.EntryState(DM: TDataModuleComm);
begin
inherited;
DM.DoDisconnect;
end;

24

実装 (データモジュールの宣言/1)
type
TCommStatus = class;TCommStatusClass = class of TCommStatus;
TNotifyReceiveEvent = procedure (Sender: TObject; const RecvData: String) of object;
TNotifyMessageEvent = procedure (Sender: TObject; const Msg: String) of object;
TDataModuleComm = class(TDataModule)
ClientSocket: TClientSocket;
TimerTimeout: TTimer;
procedure DataModuleCreate(Sender: TObject);
procedure DataModuleDestroy(Sender: TObject);
procedure ClientSocketConnect(Sender: TObject; Socket: TCustomWinSocket);procedure ClientSocketDisconnect(Sender: TObject; Socket: TCustomWinSocket);procedure ClientSocketRead(Sender: TObject; Socket: TCustomWinSocket);procedure ClientSocketError(Sender: TObject; Socket: TCustomWinSocket; ErrorEvent: TErrorEvent;var ErrorCode: Integer);procedure TimerTimeoutTimer(Sender:p(
TObject);j);privateFCommStatus: TCommStatusClass;
FRecvBuf: String;
FSeqNo: String;
FPostalCode: String;
FOnStatusChanged: TNotifyEvent;
FO R
FOnReceive:i
TNotifyReceiveEvent;
TN tif R
i Et
FOnError: TNotifyMessageEvent;
procedure SetCommStatus(const Value: TCommStatusClass);(to be continued...)25

実装 (データモジュールの宣言/2)
protected
procedure DoConnect;
procedure DoDisconnect;
procedure DoSendRequest(Socket: TCustomWinSocket);procedure DoStartTimer;
procedure DoStopTimer;
procedure NotifyReceiveData(Socket: TCustomWinSocket; const RecvData: String);
procedure NotifySocketError(Socket: TCustomWinSocket; ErrorEvent: TErrorEvent; ErrorCode: Integer);procedure NotifyReceiveError(Socket: TCustomWinSocket; const Data: String);
procedure NotifyTimeoutError;
procedure DoStatusChanged; virtual;
procedure DoReceive(const RecvData: String); virtual;
procedure DoError(const Msg: String); virtual;publicprocedure SetServerParams(const IPAddress: String; const Port: String);
procedure GoIdle;
procedure GoStandby;
procedure SendRequest(constpq(
APostalCode: String);
g);property CommStatus: TCommStatusClass read FCommStatus write SetCommStatus;
property OnStatusChanged: TNotifyEvent read FOnStatusChanged write FOnStatusChanged;
property OnReceive: TNotifyReceiveEvent read FOnReceive write FOnReceive;property OnError: TNotifyMessageEvent read FOnError write FOnError;
end;

26

実装 (データモジュールの定義/1)
procedure TDataModuleComm.ClientSocketRead(Sender: TObject; Socket: TCustomWinSocket);var
Recv: String;
Position: Integer;
RecvData: String;
SeqNo: String;Response: String;
begin
while True do
begin
Recv := Socket.ReceiveText;
if Recv = '' then
begin
Exit;
end;
FRecvBuf := FRecvBuf + Recv;
while FRecvBuf <> '' do
begin
Position := AnsiPos(#13,FRecvBuf);
if Position <= 0 then
begin
Break;
end;
R
RecvData
D t := Copy(FRecvBuf,1,PositionC(FR
B f 1 P iti- 1);
1)
Delete(FRecvBuf,1,Position);
SeqNo := Copy(RecvData,1,4);
Delete(RecvData,1,5);(to be continued...)27

実装 (データモジュールの定義/2)
if SeqNo = FSeqNo thenbegin
Response := Copy(RecvData,1,2);
Delete(RecvData,1,5);
if Response = 'OK' thenbegin
CommStatus.ReceiveAnswer(Self,Socket,RecvData);
end
else
begin
CommStatus.ReceiveError(Self,Socket,RecvData);
end;
end;
end;
end;
end;

28

実装 (フォーム)
procedure TFormClient.Button1Click(Sender: TObject);
begin
FDataModuleComm.GoStandby;
end;
procedure TFormClient.Button2Click(Sender: TObject);
begin
FDataModuleComm.GoIdle;
end;procedure TFormClient.Button3Click(Sender: TObject);
begin
FDataModuleComm.SetServerParams(Edit1.Text,Edit2.Text);
FDataModuleComm.SendRequest(Edit3.Text);
end;
procedure TFormClient.CommStatusCahnged(Sender: TObject);beging
Edit5.Text := FDataModuleComm.CommStatus.GetDescription;
end;
procedure TFormClient.CommReceive(Sender: TObject; const RecvData: String);
begin
Edit4.Text := RecvData;
Edit6 T t := '';Edit6.Text''
end;
procedure TFormClient.CommError(Sender: TObject; const Msg: String);
begin
Edit6.Text := Msg;
Edit4.Text ::= '';
;
end;

29

17Th
Developer Camp

デモンストレーション

30

17Th
Developer Camp

6

参考文献

31

参考文献 (1)
• オブジェクト指向における再利用のためのデザインパタ ン(改訂版)
デザインパターン(改訂版)– Erich Gamma、Richard Helm、Ralph Johnson、John M. Vlissides著– 本位田 真一、吉田 和樹監訳
– ソフトバンククリエイティブ– ISBN4-7973-1112-6 (ISBN978-4797311129)– 5,040円• http://www.sbcr.jp/products/4797311126.html
http://www sbcr jp/products/4797311126 html
• http://www.amazon.co.jp/dp/4797311126

32参考文献 (2)• Head Firstデザインパターン– Eric Freeman
Freeman、Elisabeth
Elisabeth Freeman
Freeman、Kathy
Kathy Sierra
Sierra、Bert Bates著–木木下 哲
哲也、有限会社、有限会社 福龍興業訳– 佐藤 直生監訳– ソフトバンククリエイティブ– ISBN4-87311-249-4– 4,830円• http://www.oreilly.co.jp/books/4873112494/
htt //
ill
j /b k /4873112494/• http://www.amazon.co.jp/dp/4873112494

33

17Th
Developer Camp

Q&A

34

想定される質問とその回答 (1)••サンプルプロジェクトを開こうとするとエラーになるんですけど。
→設計時パッケージ“CodeGear
設計時 ッケ ジ
Socket Components”をIDEに
pを
インストールする必要があります。IDEのメインメニューから
コンポーネント→パッケージのインストールでDelphiのインストール
先のbinディレクトリにある“dclsocketsXXX.bpl”(Delphi 2007
なら“dclsockets100.bpl”)を追加してから、サンプルプロジェクトを開きなおしてください。
Delphi 2007以外ではサンプルプロジェクトは動作しないんですか？
→Delphi 2007およびそれ以前のバージョンであれば基本的に動作するはずです。Delphi 2009以降ではString = UnicodeStringとなっている関係から、そのままではコンパイルすら通りません(申し訳ない)。TClientSocket/TServerSocketとインタフェース
している部分をAnsiStringとすることで動くのではないかと思いますが…。
35

想定される質問とその回答 (2)•LTでの説明がはしょりすぎでよくわからなかったんですけど。
→すいませんすいませんすいません
す ませんす ませんす ません
とりあえずこの資料とサンプルコード、できれば参考文献のHeadFirstデザインパターンをよくお読みください。その上での不明点は公式 ォ ラ公式フォーラムかDelphi‐mlで質問していただければと思います。
p
質問
けれ
思 ます。
– Embarcadero Discussion Forums: Delphi
https://forums.embarcadero.com/forum.jspa?forumID=14– Delphi｜freeml
http://www.freeml.com/delphi‐users

36想定される質問とその回答 (3)•サンプルプログラムの使い方を教えてください。
→まずテスト用サーバ(TestSvr.exe)を起動します。待機ポートを
まずテ ト用サ()を起動します。待機ポ トを確認してOpenボタンをクリックするとサーバソケットがlisten状態になります。Closeボタンでlisten状態が解除されます。
• Delayには電文を受信してから回答を送信するまでの時間をy
電文を受信
ら回答を送信するま
時間を
msec単位で指定します。また“シーケンス番号を置き換える”
チェックボックスをチェックオンにすると回答電文に含まれるシーケンス番号が受信した要求電文と一致しなくなります。
• 実装は手抜きされているので、“1500045”と“1020072”以外の郵便番号はエラーになります。

37

想定される質問とその回答 (4)→次にクライアント(TestClient.exe)を起動します。起動直後は次 クライアント()を起動します。起動直後はアイドル状態になっていますので、Standbyボタンをクリックしてスタンバイ状態に移行します。ここでサーバのIPアドレス、ポート
番号を確認して問い合わせたい郵便番号(7桁)を入力し、問い合わせボタンをクリックするとサーバへの問い合わせのシーケンスが発動します。正常に回答を受信したときは住所が表示されます。またエラーのときはエラーメッセージが表示されます。なおクライアント側の通信タイムアウトはクライアント側の通信タイムアウトは5000msec固定です。
固定です

38

想定される質問とその回答 (5)•••どのくらい複雑なケースならStateパターンを適用するべきでしょうか？
→もちろんケースバイケースですが、Idleステート、Standbyステートを
もちろんケ
イケ
ですが、
テ ト、
y テ トを含め4状態以上ある場合は適用することを考慮していいのでは？というのが個人的な意見です。
Idleステートの存在意義がわかりません。
テ
存在意義 わ りま。
→プログラム起動直後やプログラムの環境設定を行っているとき、あるいはプログラムを終了しようとしているときなど、一連のシ ケ
シーケンスが発動してほしくないときに状態をIdleステートにして
発動
くな
状態を
テ
おく、という使い方を想定しています。
エラー発生時にリトライしないとかずいぶん手抜きじゃないですか？
→Stateパタ ンを考える上での本質ではないので外しました。
→Stateパターンを考える上での本質ではないので外しました。
もちろん現実のプロジェクトに適用するときはエラー発生時のリトライや、必要ならその際のウェイトなどもシーケンスに組み込む
形で設計、
、 実装
実装されるべきだと思います。
39

想定される質問とその回答 (6)•ところでIDEに標準でインストールされていないTClientSocket/
TServerSocketコンポーネントを使うのはいかがなものかと。
→TClientSocket/ TServerSocketコンポーネントを使用するのは確かにあまりお勧めできません(SMP環境下で不具合があるとかいう話を聞いたことがあります)。本当はIndyにするべきなんでしょう
けれども、Indyはブロッキング動作が基本なのでUIとはスレッドを分ける必要があるとか、非ブロッキング動作のWinSockをIndyでわざわざブロッキング動作にしているものをまた非ブロッキング
動作にして使うのはどうなんだろう？とか いろいろあ てこういう動作にして使うのはどうなんだろう？とか、いろいろあってこういうサンプルになっています。有償でもいいので非ブロッキングで使用できるお勧めのSocket通信コンポーネントはありませんか？

40

17Th
Developer Camp

ありがとうございました

41