State (ステート) パターン

状態をクラスとして表現するデザインパターンです。
Memento パターンではインスタンスで状態を表現していましたが、
このパターンではクラスとして状態を表現します。


サンプルでは、入力された文字列(アカウント)に対し、
ログインした状態・既にログイン済みの状態・入力したアカウントが不正な状態をクラスとして表現しています。

このカテゴリーの先頭へ このページの先頭へ

■クラス図

ここで用いるサンプルのクラス図です。
準備中...

このカテゴリーの先頭へ このページの先頭へ

■サンプルの説明

サンプルの見た目1
TextBox にユーザー ID を入力し、LoginCheck ボタンをクリックすると、アカウント状態の取得を行います。
入力されたアカウントのログイン状態に応じ、メッセージが表示されます。
このサンプルの仕様では、データベースに登録されているアカウントはログイン可能、
登録されていても、既にログインされている場合はログイン不可能である、とします。
サンプルのプロジェクトダウンロード

このカテゴリーの先頭へ このページの先頭へ

■コード

アプリケーションのエントリポイント StartUpForm です。

StartUpForm.vb
Public Class StartUpForm
  Inherits System.Windows.Forms.Form

#Region " Windows フォーム デザイナで生成されたコード "

  Public Sub New()
    MyBase.New()

    ' この呼び出しは Windows フォーム デザイナで必要です。
    InitializeComponent()

    ' InitializeComponent() 呼び出しの後に初期化を追加します。
    AddHandler Application.ThreadException, AddressOf CommonExceptionAndHandler.ExceptionHandler.MyHandler
  End Sub

  ' Form は、コンポーネント一覧に後処理を実行するために dispose をオーバーライドします。
  Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
    If disposing Then
      If Not (components Is Nothing) Then
        components.Dispose()
      End If
    End If
    MyBase.Dispose(disposing)
  End Sub

  ' Windows フォーム デザイナで必要です。
  Private components As System.ComponentModel.IContainer

  ' メモ : 以下のプロシージャは、Windows フォーム デザイナで必要です。
  'Windows フォーム デザイナを使って変更してください。  
  ' コード エディタを使って変更しないでください。
  Friend WithEvents TextBox1 As System.Windows.Forms.TextBox
  Friend WithEvents Button1 As System.Windows.Forms.Button
  <System.Diagnostics.DebuggerStepThrough()> Private Sub InitializeComponent()
    Me.TextBox1 = New System.Windows.Forms.TextBox
    Me.Button1 = New System.Windows.Forms.Button
    Me.SuspendLayout()
    '
    'TextBox1
    '
    Me.TextBox1.Location = New System.Drawing.Point(8, 8)
    Me.TextBox1.Name = "TextBox1"
    Me.TextBox1.TabIndex = 0
    Me.TextBox1.Text = "TextBox1"
    '
    'Button1
    '
    Me.Button1.Location = New System.Drawing.Point(136, 8)
    Me.Button1.Name = "Button1"
    Me.Button1.TabIndex = 1
    Me.Button1.Text = "Button1"
    '
    'StartUpForm
    '
    Me.AutoScaleBaseSize = New System.Drawing.Size(5, 12)
    Me.ClientSize = New System.Drawing.Size(292, 266)
    Me.Controls.Add(Me.Button1)
    Me.Controls.Add(Me.TextBox1)
    Me.Name = "StartUpForm"
    Me.Text = "Form1"
    Me.ResumeLayout(False)

  End Sub

#End Region

  '画面がロードされた時のイベント
  Private Sub StartUpForm_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Load
    Me.Text = "State"

    Me.TextBox1.Text = ""
    Me.Button1.Text = "LoginCheck"
  End Sub

  'LoginCheckボタン押下時の処理
  Private Sub Button1_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles Button1.Click
    Dim state As IState = LogoutState.GetInstance() '最初はログアウトの状態
    Dim myContext As IContext = New LoginContext(state)
    Dim message As String

    message = myContext.GetStateMessage(Me.TextBox1.Text)

    MessageBox.Show(message, _
            Me.Text, _
            MessageBoxButtons.OK, _
            MessageBoxIcon.Information)
  End Sub
End Class
このカテゴリーの先頭へ このページの先頭へ

現在の何かの状態を保持するインターフェースです。

IContext.vb
'現在の状態を保持するインターフェース
Public Interface IContext

  '状態を取得するメソッド定義
  Function GetStateMessage(ByVal checkString As String) As String
  '自身の状態を変更するメソッド定義
  Sub ChangeState(ByVal state As IState)

End Interface
このカテゴリーの先頭へ このページの先頭へ

ログインの状態を保持するクラスです。
IContext を実装しています。

LoginContext.vb
'ログインに関する状態を保持
Public Class LoginContext
  Implements IContext

  Private m_state As IState

  Public Sub New(ByVal state As IState)
    Me.m_state = state
  End Sub

  '状態を取得するメソッド実装
  Public Function GetStateMessage(ByVal checkString As String) As String Implements IContext.GetStateMessage

    If Not System.Text.RegularExpressions.Regex.Match(checkString, "^[a-zA-Z]+$").Success Then
      Throw New CommonExceptionAndHandler.MyException("不正な文字が入力されました")
    End If

    Dim mdbpath As String = System.IO.Path.Combine(Application.StartupPath, "State.mdb")
    Dim connection_string As String = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" + mdbpath + ";"
    Dim dt As DataTable

    Dim myDa As CommonDataAccess.AbstructDataAccess = New CommonDataAccess.OleDataAccess
    Try
      myDa.Open(connection_string)
      dt = myDa.DataSelect("SELECT account, login_status FROM users WHERE account='" + checkString + "';")
    Finally
      myDa.Close()
    End Try

    '状態チェック
    Me.m_state.CheckState(Me, checkString, dt)
    'ログイン可能かどうかのチェック結果メッセージを返却
    Return Me.m_state.GetMessage()

  End Function

  '自身の状態を変更するメソッド実装
  Public Sub ChangeState(ByVal state As IState) Implements IContext.ChangeState
    Me.m_state = state
  End Sub
End Class
このカテゴリーの先頭へ このページの先頭へ

何かの状態を表すインターフェースです。

IState.vb
'状態を表すインターフェース
Public Interface IState

  '状態チェック処理のメソッド定義
  Sub CheckState(ByVal context As IContext, ByVal account As String, ByVal dt As DataTable)
  '自身の状態を文字列表現で返却するのメソッド定義
  Function GetMessage() As String

End Interface
このカテゴリーの先頭へ このページの先頭へ

不正なアカウントが入力された状態を表すクラスです。 IState を実装しています。

InvalidAccount.vb
'無効なアカウントの状態
Public Class InvalidAccount
  Implements IState

  'Singleton
  Private Shared m_invalidAccount As InvalidAccount = New InvalidAccount

  Private Sub New()
  End Sub

  Public Shared Function GetInstance() As InvalidAccount
    Return m_invalidAccount
  End Function

  '状態チェック処理のメソッド実装
  Public Sub CheckState(ByVal context As IContext, ByVal account As String, ByVal dt As System.Data.DataTable) Implements IState.CheckState
    If dt.Rows.Count < 1 Then Exit Sub

    If dt.Select("account='" + account + "' AND login_status=0").Length = 1 Then
      '有効 かつ ログインしてない時
      context.ChangeState(LogoutState.GetInstance())
    Else
      '有効 かつ ログインしていた時
      context.ChangeState(LoginedState.GetInstance())
    End If
  End Sub

  '自身の状態を文字列表現で返却するのメソッド実装
  Public Function GetMessage() As String Implements IState.GetMessage
    Return "入力されたアカウントが正しくありません"
  End Function
End Class
このカテゴリーの先頭へ このページの先頭へ

既にログインされていたアカウントを入力された状態を表すクラスです。 IState を実装しています。

LoginedState.vb
'有効なアカウントだけど、すでにログインされていた状態
Public Class LoginedState
  Implements IState

  'Singleton
  Private Shared m_loginedState As LoginedState = New LoginedState

  Private Sub New()
  End Sub

  Public Shared Function GetInstance() As LoginedState
    Return m_loginedState
  End Function

  '状態チェック処理のメソッド実装
  Public Sub CheckState(ByVal context As IContext, ByVal account As String, ByVal dt As System.Data.DataTable) Implements IState.CheckState

    'accountが有効でない場合
    If dt.Rows.Count < 1 Then
      context.ChangeState(InvalidAccount.GetInstance())
    Else
      'account有効 かつ login_status=0の時
      If dt.Select("account='" + account + "' AND login_status=0").Length = 1 Then
        context.ChangeState(LogoutState.GetInstance())
      End If
    End If
  End Sub

  '自身の状態を文字列表現で返却するのメソッド実装
  Public Function GetMessage() As String Implements IState.GetMessage
    Return "入力されたアカウントは既にログインしています"
  End Function
End Class
このカテゴリーの先頭へ このページの先頭へ

既にログイン可能なアカウントを入力された状態を表すクラスです。 IState を実装しています。

LogoutState.vb
'有効なアカウント、ログイン可能な状態
Public Class LogoutState
  Implements IState

  'Singleton
  Private Shared m_logoutState As LogoutState = New LogoutState

  Private Sub New()
  End Sub

  Public Shared Function GetInstance() As LoginedState
    Return m_loginedState
  End Function

  '状態チェック処理のメソッド実装
  Public Sub CheckState(ByVal context As IContext, ByVal account As String, ByVal dt As System.Data.DataTable) Implements IState.CheckState
    'accountが有効でない場合
    If dt.Rows.Count < 1 Then
      context.ChangeState(InvalidAccount.GetInstance())
    Else
      'account有効 かつ login_status=1の時
      If dt.Select("account='" + account + "' AND login_status=1").Length = 1 Then
        context.ChangeState(LoginedState.GetInstance())
      End If
    End If
  End Sub

  '自身の状態を文字列表現で返却するのメソッド実装
  Public Function GetMessage() As String Implements IState.GetMessage
    Return "入力されたアカウントはログイン可能です"
  End Function
End Class
このカテゴリーの先頭へ このページの先頭へ

■ひとこと

状態の判定が多岐に渡る場合、このようにクラスとして表現しておく事で
すっきりと実装できる場合があります。
また、if や case 等の分岐で状態を判断していた場合よりも、
State パターンで実装しておくと、状態の追加で判定するのが柔軟にできる可能性があります。


また、サンプルでは GetMessage というメソッドで、文字列を返却するという単純な処理を行っていましたが、
状態に依存する処理をこのような形で分けて実装する事ができます。

このカテゴリーの先頭へ このページの先頭へ