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

前回、Observer パターンをアプリケーションオブジェクトを使って試してみました。

なんで、初回が Observer パターンなのか。『PHP Hacks』で最初に取り上げられていたからというだけで、他に理由はありません。しかし...iPod ゲームはいけませんね。駄目です。とてもではないですが、暇つぶしで終わらせることなんてできません。全部買ってしまいそうな勢いです。

それはさておき...。前回は極力難しいことを考えずに Observer パターンを適用しました。デザインパターンの本や Web サイトを見ていると UML 図やパターンの概念的なコードがあります。スクリプトオブジェクトで概念的なコードを書いて一応 AppleScript でもデザインパターンを適用できるよ、という感じで進めていこうと思っていたのですが、概念的なコードってあくまで理解するためのものなのでどういうふうに使えばいいかがよく分からない。で、前回のようなスクリプトを書いたのですが...UML 図はともかく、一応動作を理解できるようにデザインパターンの概念的なスクリプトも書いておきます。

監視されるオブジェクトは、誰が監視しているかを知っています。変更があったときに通知を送るためにです。監視するオブジェクトを追加、削除できるような機能を持っています。監視オブジェクトを複数保持することができるようになっていて、それらに一括で通知を送る機能も持っています。

監視している方も監視対象を知っています。監視する方では必ず update() ハンドラを実装していました。監視されているオブジェクトは監視者の update() ハンドラを呼び出す、という取り決めの上に関係が成り立っているからです。

これらをスクリプトオブジェクトで表現すると以下のようになります。

Script Editor で開く

-- 監視オブジェクト
on Observer()
    script Observer
        -- 通知を受け取るハンドラ
        on update()
            log "update"
        end update
    end script
end Observer

-- 監視対象オブジェクト
on Observable()
    script Observable
        -- 監視オブジェクトのリスト
        property observers : {}

        -- 監視オブジェクトを加える
        on addObserver(observerObject)
            set end of observers to observerObject
        end addObserver

        -- 監視オブジェクトを削除する
        on deleteObserver(observerObject)
            repeat with thisObject in observers
                if (contents of thisObject) is observerObject then
                    set contents of thisObject to missing value
                    exit repeat
                end if
            end repeat

            set observers to scripts of observers
        end deleteObserver

        -- 通知を送る
        on notifyObservers()
            repeat with thisObject in observers
                update() of thisObject
            end repeat
        end notifyObservers
    end script
end Observable

監視する方、される方、どちらも最低限の機能だけを実装しています。Observer パターンは、これらのオブジェクトを拡張することで実現します。

Observer オブジェクトは、update() ハンドラを持ってます。これは実装側で実際の処理を記述します(インターフェースのつもり)。Observable オブジェクトは、監視者を追加、削除するハンドラと通知を送るハンドラを持っています。こちらも実際の処理はこのオブジェクトを継承したオブジェクトで実装し、自身が持つべきデータ(属性)とそれらのデータを取得、設定するハンドラを追加します。

蛇足なのですが、deleteObserver() ハンドラの最後の部分で

set observers to scripts of observers

としています。これは、スクリプトオブジェクトの利用を考えているのでこのようにしていますが、監視者が他のオブジェクト(アプリケーション等)の場合は、このままでは動かないので適切に変更する必要があります(他にも問題はあるのですが)。

実際に利用するには以下のように監視する方、される方、どちらも目的に合った実装を行います。

Script Editor で開く

-- 監視者オブジェクトの実装
on ConcreteObserver1()
    script ConcreteObserver1
        property state : missing value
        property observableObject : missing value

        on setObject(theObject)
            set observableObject of me to theObject
        end setObject

        on getObject()
            return observableObject of me
        end getObject

        on update()
            set state of me to observableObject's getState()
            log "Observer 1 update."
        end update
    end script
end ConcreteObserver1

on ConcreteObserver2()
    script ConcreteObserver2
        property state : missing value
        property observableObject : missing value

        on setObject(theObject)
            set observableObject of me to theObject
        end setObject

        on getObject()
            return observableObject of me
        end getObject

        on update()
            set state of me to observableObject's getState()
            log "Observer 2 update."
        end update
    end script
end ConcreteObserver2

-- 監視対象オブジェクトの実装
on ConcreteObservable()
    script ConcreteObservable
        property parent : Observable()
        property state : missing value

        on getState()
            return state of me
        end getState

        on setState(theState)
            set state of me to theState
        end setState
    end script
end ConcreteObservable

-- クライアント
on run
    -- 監視対象の作成
    set subjectObject to ConcreteObservable()
    -- 監視者の作成
    set observerObject1 to ConcreteObserver1()
    set observerObject2 to ConcreteObserver2()
    -- 監視対象を監視者に登録
    tell observerObject1 to setObject(subjectObject)
    tell observerObject2 to setObject(subjectObject)

    tell subjectObject
        -- 監視者を登録
        addObserver(observerObject1)
        addObserver(observerObject2)
        -- 状態の変化
        setState(true)
        -- 通知を送る
        notifyObservers()
        -- 監視者を削除
        deleteObserver(observerObject1)
        log "Delete Observer 1"
        -- 状態の変化
        setState(true)
        -- 通知を送る
        notifyObservers()
    end tell
end run

-- 結果
(*Observer 1 update.*)
(*Observer 2 update.*)
(*Delete Observer 1*)
(*Observer 2 update.*)

Script Editor のイベントログで確認してみてください。監視オブジェクトは、update() ハンドラを実装しています。ここで処理を行ってもいいですし、他のスクリプトオブジェクトやアプリケーションに処理を渡してもいいです。ここでは監視対象を属性で保持していますが、これは update() ハンドラの引数で受け取るようにしても構いません(その方が監視オブジェクトはそのままで、他でも再利用しやすくなるかもしれません)。update() ハンドラさえあれば実装はどのようなものでも構いません。

監視対象は自分の属性を操作するハンドラを持っています。監視する方はこのハンドラを使って状態を調べます。監視オブジェクトの追加や削除、通知を送るハンドラは親で定義されているので、監視対象が実装する必要があるのは自身の属性とハンドラです。

基本的にこのような形になり、目的や状況に応じて Observer パターンの考え方を適用しつつスクリプトを記述します(デザインパターンはプログラム設計の指針のようなものなので必ずしもこう書かなくてはならないというものではありません)。

使い道はいろいろあると思いますが...AppleScript は、一度に一つの動作しか実行できないので通知を送り、監視オブジェクトが処理を終了するまで監視対象の処理が待ち状態になります。AppleScript Studio でなら、ボタンを押した、ビューが更新されたなどのイベントがあったときに他のオブジェクトに通知を送るような設計が可能です。が、先にも書いたように通知先で処理が滞るようなら Observer パターンの適用は考えた方がいいと思います。

AppleScript Studio ではなく、前回のようなアプリケーション形式のスクリプトなら ignoring application responses を使って処理を待たずに先に進むことができます。いろいろなことを考えるとデザインパターンは、アプリケーション形式のスクリプトをオブジェクトに見立てて(見立ててって AppleScript ではオブジェクトなのですが)それらの関係にデザインパターンを用いる方が使い勝手はいいかもしれません。

前回の iTunes の曲が変わったときになんらかの動作を行うというスクリプトを以上を踏まえて変更すると以下のようになります。

Script Editor で開く

-- 監視オブジェクトのリスト
property observers : {}
-- iTunes の現在の曲
property curTrack : missing value

-- 監視オブジェクトを加える
on addObserver(observerObject)
    set end of observers to observerObject
end addObserver

-- 監視オブジェクトを削除する
on deleteObserver(observerObject)
    repeat with thisObject in observers
        if (contents of thisObject) is observerObject then
            set contents of thisObject to missing value
            exit repeat
        end if
    end repeat

    set observers to strings of observers
end deleteObserver

-- 通知を送る
on notifyObservers(observableObject)
    repeat with thisObject in observers
        ignoring application responses
            tell application thisObject to update(observableObject)
        end ignoring
    end repeat
end notifyObservers

-- 属性の取得
on getState()
    return curTrack of me
end getState

-- 属性の設定
on setState(thisTrack)
    set curTrack of me to thisTrack
end setState

on idle
    tell application "System Events"
        set bool to (name of processes whose visible is true) contains "iTunes"
    end tell

    if bool then
        tell application "iTunes"
            if player state is playing then
                set tmp to current track
            else
                set tmp to curTrack
            end if
        end tell

        if tmp is not curTrack then
            my setState(tmp) -- 状態の変更
            my notifyObservers(me) -- 通知を送る
        end if
    end if

    return 1
end idle

これを iTunesChecker という名前にして終了しないアプリケーションで保存します。監視者を追加するハンドラと通知を送るハンドラは変更しています。この部分は後で説明します。起動しても、まだ監視者がいないので曲が変わってもなんの反応もしません。

次に監視オブジェクトを作ります。以下は、Monzai を使った曲名の読み上げスクリプトです。

Script Editor で開く

on update(theObject)
    set theTrack to getState() of theObject
    if theTrack is missing value then return

    tell application "iTunes"
        set theArtist to artist of theTrack
        set theTitle to name of theTrack

        set msg to (theArtist & "の" & theTitle & "です") as Unicode text
    end tell

    tell application "monzai"
        speak controller 1 moji msg
    end tell
end update

update() ハンドラだけを持っています。これをアプリケーション形式で保存します。もう一つ、ダイアログで曲名を表示する監視オブジェクトを作ります。

Script Editor で開く

on update(theObject)
    set theTrack to getState() of theObject
    if theTrack is missing value then return

    tell application "iTunes"
        set theArtist to artist of theTrack
        set theTitle to name of theTrack

        set msg to (quote & theTitle & quote & return & return & theArtist) as Unicode text
    end tell

    tell application (path to frontmost application as Unicode text)
        activate
        display dialog msg buttons {"OK"} default button 1 giving up after 2 with icon 1 with title "Track Info"
    end tell
end update

こちらもアプリケーションで保存します。

最後にクライアントとなるべきスクリプトを作成します。監視対象と監視オブジェクトの操作はここで行います。

Script Editor で開く

on run
    tell application (path to frontmost application as Unicode text)
        activate
        display dialog "Add or Remove?" buttons {"Cancel", "Remove", "Add"} default button 3
        set theButton to button returned of result
    end tell

    set theApp to choose file of type {"APPL"} without invisibles
    set theInfo to info for theApp
    if (file creator of theInfo) is "aplt" then
        if theButton is "Add" then
            tell application "iTunesChecker" to addObserver(theApp as string)
        else
            tell application "iTunesChecker" to deleteObserver(theApp as string)
        end if
    end if
end run

実行し、先ほど保存した Monzai での読み上げアプリケーションか、ダイアログを表示するスクリプトを選択します。追加するとそのアプリケーションに通知が送られます。ここでは、監視オブジェクトのファイルパスを監視対象に渡しています。パスではなく、アプリケーションを渡せばいいのでは?と思います。先に『監視者を追加するハンドラと通知を送るハンドラは変更しています。この部分は後で説明します』と書きましたが、アプリケーションやスクリプトオブジェクトを監視対象に登録するのは問題があるのです。

なにが問題かといいますと...例えば、アプリケーションを登録するようにしておきます。

addObserver(application "Log")

アプリケーションを登録してから iTunesChecker を終了します。終了後もスクリプトオブジェクトの属性は保持されます。次回起動時も登録されたままなので面倒がありませんが、削除ができないのです。

deleteObserver(application "Log")

これが実行できない。なぜかというと、最初に登録したアプリケーションオブジェクトと削除しようとしているアプリケーションオブジェクトが別のものと判断されるからです。削除するには同じオブジェクトでないといけないのですが、iTunesChecker に渡して終了した時点で iTunesChecker で別のオブジェクトとして保存されるのです(同じスクリプトオブジェクト、同じアプリケーションオブジェクトと判定できるルーティンを組めばいいのですが、そうなると余分な処理に時間がかかる)。

AppleScript のスクリプトオブジェクトは、スクリプトの中に一つしか存在できません。copy 命令や load script 命令で同じスクリプトオブジェクトを複製、生成したとしてもオリジナルとは異なるオブジェクトになります。オブジェクトの独自性が保証されているのですが、同じものを渡すことができないような状況では困ることがあります。

だから、アプリケーションのパスを渡しているのです。これなら、同じ文字列なら同じ文字列と判定されるので。が、そのためにアプリケーションの場所を移動できないという制限が加わります(alias でも構わないのですが)。

あちらを立てればこちらが立たず...AppleScript ではこれが限界かな...?

デザインパターンを用いると必然的にコード量が増えるのですが、ここまでする必要があるのか?という疑問があると思います。もちろん、作るスクリプトによりけりなのですが、上記のスクリプトを一つにまとめてみることを考えるとメリットとデメリットが分かると思います。

iTunes で曲が変わったときに曲名を使う人の好みによりダイアログか声で知らせる。...面倒そうに思えます。

Observer パターンを使った方は、機能を追加したいときにその部分だけを作ればいいようになっています。iTunesChecker もクライアントも修正する必要がありません。update() ハンドラを持ったスクリプトアプリケーションを作るだけです。いい面、悪い面ありますが、こういう AppleScript もいいかもしれません。

0 件のコメント :

コメントを投稿