AppleScript とデザインパターン (10)

今回は、『オブジェクト指向における再利用のためのデザインパターン』の中から State パターンを取り上げます。以前に Strategy パターンを取り扱ったことがありますが、State パターンは Strategy パターンと同じクラス図、同じ実装方法になるデザインパターンです。

状態に応じて処理を変更する...例えば、iTunes の再生/一時停止のボタン。これは、一つのボタンですが、再生中に押すと一時停止になり、一時停止中に押すと再生になります。一つのボタンで複数の状態が表現されています。このようなボタンをプログラムで書くとなると...まずは、以下のようなコードを思いつくと思います。

property playing : false

if playing then
    stop()
    set playing to false
else
    play()
    set playing to true
end if

条件分岐による状態の変更ですね。再生と一時停止だけなのでそれほど複雑になるわけではないですが、時間によって異なる挨拶を返すようなプログラムでは、「おはよう」、「こんにちは」、「こんばんは」の 3 つが必要になります。返す挨拶が増えると分岐も増えます。

State パターンでは状態をオブジェクトで表現し、状態オブジェクトを交換することで異なった処理を行います。具体的には if 文で分岐しているそれぞれの処理を状態オブジェクトととして分割します。

script Morning
    on showMessage()
        return "おはよう"
    end showMessage
end script

script Daytime
    on showMessage()
        return "こんにちは"
    end showMessage
end script

script Night
    on showMessage()
        return "こんばんは"
    end showMessage
end script

朝、昼、晩の挨拶をオブジェクトにします。これらを交換することで、それぞれの状態に対応します。

Script Editor で開く

script Greeting
    property currentState : Morning

    on setState(theObject)
        set currentState of me to theObject
    end setState

    on showMessage()
        currentState's showMessage()
    end showMessage
end script

on run
    tell Greeting
        showMessage()
        --> "おはよう"
        setState(Daytime)
        showMessage()
        --> "こんにちは"
        setState(Night)
        showMessage()
        --> "こんばんは"
    end tell
end run

このような感じです。...あれ?サンプルのコードが変わっただけで Strategy パターンと同じでは...?

全くその通りで...どうしましょう。えーっと『Strategy パターンはアルゴリズムの交換を目的とし、State パターンは状態を動的に変更したいというときに使うもので、Strategy パターンとは概念が違います』。...え、違いってそれだけ?

これで、State パターンが終わったら何とも釈然としない。ちゃんと、Strategy パターンとの際を明確にしましょう。まず、State パターンは、状態を表すオブジェクトが存在します。これ(ら)を State オブジェクトといいます。そして、State オブジェクトを保持し、現在の状態を表すとともに状態の動作を呼ぶ出す役目を持つ Context オブジェクトが存在します。利用者はこの Context オブジェクトにアクセスします。

上記の挨拶でいえば、Morning、Daytime、Night が State オブジェクトにあたり、Greeting が Context にあたります。利用者(run ハンドラ)は、Greeting の showMessage() を呼び出します。Greeting は、自分が保持している State オブジェクトによって適切な挨拶を返します。

State パターンは、このように状態によって振る舞いが変化し、状態の遷移(挨拶は時間が経つと変わる)を表現するときに利用するといいデザインパターンです。

iTunes は playpause という命令を持っています。再生中のときは一時停止、一時停止のときは再生になります。この命令を模してみます。再生中の状態と一時停止中の状態。まず、この 2 つの状態オブジェクトを作ります。

Script Editor で開く

on Playing()
    script
        on action(theObject)
            tell application "iTunes" to pause
            tell theObject to setState(pausedObject)
        end action
    end script
end Playing

on Paused()
    script
        on action(theObject)
            tell application "iTunes" to play
            tell theObject to setState(playingObject)
        end action
    end script
end Paused

それぞれ、action() ハンドラを持っていて、Playing の時は iTunes で一時停止を行います。Paused のときは、再生を行います。そして、次の状態として他方を登録するようにします。

状態を保持する Context オブジェクトの役割を担う Player というオブジェクトを追加します。

Script Editor で開く

on Player(stateObject)
    script
        property currentState : stateObject

        on setState(theObject)
            set currentState of me to theObject
        end setState

        on playpause()
            tell currentState of me to action(me)
        end playpause
    end script
end Player

状態を登録する setState() というハンドラと playpause() というハンドラがあります。playpause() ハンドラでは登録されている状態の action() ハンドラを自分を引数にして呼び出します。

これらを利用する側は以下のようなコードを記述します。

Script Editor で開く

property playingObject : missing value
property pausedObject : missing value
property playerObject : missing value

on run
    initialize()
    playerObject's playpause()
end run

on reopen
    playerObject's playpause()
end reopen

on idle
    tell application "iTunes"
        set theState to player state
        if theState is paused or theState is stopped then
            playerObject's setState(pausedObject)
        else if theState is playing then
            playerObject's setState(playingObject)
        end if
    end tell

    return 1
end idle

on initialize()
    set playingObject to Playing()
    set pausedObject to Paused()

    tell application "iTunes"
        set theState to player state
        if theState is paused or theState is stopped then
            set playerObject to my Player(pausedObject)
        else if theState is playing then
            set playerObject to my Player(playingObject)
        else
            set playerObject to my Player(pausedObject)
        end if
    end tell
end initialize

最初に iTunes の現在の状態を調べて Player オブジェクトを作っています。あとは、Player オブジェクトの playpause() ハンドラを呼び出せば、iTunes の playpause と同じ動作をします。idle ハンドラでは iTunes の状態を調べてオブジェクトを再登録しなおしています。

しかし、関係のないところでつまずきました。iTunes の player state 属性って Script Editor 上では as で文字列に変換できるのですが、アプリケーションで保存したら変換できないのですね。おかげで if 文のどこにも引っかかりませんでした。今まで文字列に変換して使っていて大丈夫だから気がつかなかった...。まったく...なんで、エラーになるのかなとさんざん悩みました。

このサンプルでは状態を状態自身が次の状態として変更していますが、状態はどこで変更しても構いません。実際、idle ハンドラの中で変更しています。さて、このサンプルで Strategy パターンとの違いを分かっていただけたでしょうか?あまり自信がありませんが...。

0 件のコメント :

コメントを投稿