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

「なんで iTunes で曲が変わったときに教えてくれないんだ?」

「iPod がマウントされたときに自動的にテキストを転送したいんだけど...」

「外付けの HDD をマウントしたときに勝手にバックアップしてくれよ」

「HTML を編集・保存したときにブラウザで勝手に再読み込みして欲しいんだけど」

「新着メールが来たら教えて欲しい」

...人間というのは結構わがままな生き物ですね。iTunes といえば、新しい iTunes 7。track クラスに動画関連の属性が追加されていますね。それ以外で大きな変更は見当たらず...つまり、今までのスクリプトはほとんどそのままで使えるということです。多分。

いや、iTunes 7 のことではなくて。先に上げたような要望を満たすには、idle ハンドラを使えばいいでしょう。例えば、iTunes で次の曲になったときに動作するようなスクリプトなら、以下のような感じ。

Script Editor で開く

property currentTrack : ""

on idle
    tell application "iTunes" to set tmp to name of current track

    if tmp is not currentTrack then
        tell application (path to frontmost application as Unicode text)
            activate
            display dialog tmp buttons {"OK"} default button 1 giving up after 2 with icon 1
        end tell

        set currentTrack to tmp
    end if

    return 1
end idle

これで曲が変わったときだけ教えてれますね。Dock に表示されて鬱陶しいというなら、スクリプトをアプリケーションバンドル形式で保存してプロパティリストをごにょごにょ...とすればいいでしょう。

AppleScript には「あるオブジェクトの状態が更新されたらそれを通知する」という機能がありません。まぁ、スクリプト言語なんだからそこまで必要ありませんが。あることが起きたかどうかを監視したいというとき、上記のようなスクリプトを作り、idle ハンドラで目的を達成するというのが基本的な方法ではないでしょうか。

と、ここまでくればなんのデザインパターンを取り上げようとしているのか分かるのではないでしょうか。あるオブジェクトの状態を監視し、更新されたことを通知してもらう Observer パターンです。

ほとんどの場合、上記のようなスクリプトで問題はなく、わざわざデザインパターンを用いることもないのですが...「AppleScript でこういうこともできるんだ」というぐらいに捉えていただければさいわいです。話のネタってやつです。

Observer パターンを簡単に説明。監視するオブジェクトと監視されるオブジェクトがあります。先のスクリプトを監視側と監視される側に分けてみましょう。監視されるオブジェクトは iTunes で曲が変わったかどうかを調べ、それを自分の属性(currentTrack 属性)として持ちます。

監視するオブジェクトは、監視対象のオブジェクトの状態を監視しています。ここでは、iTunes の曲が変わり、属性が更新されたかどうかです。

曲が変わり、currentTrack 属性が更新された時、監視されているオブジェクトは監視しているオブジェクトに更新があったことだけを伝えます。監視しているオブジェクトは通知を受け取るとなんらかの処理(ここではダイアログを表示しています)を実行します。

監視されているオブジェクトはデータを持っています。監視するオブジェクトはなんらかの処理を行います。Observer パターンでは、データと処理が分離され、お互いのことを詳しく知る必要がありません。このため、処理の追加と既存のコードの修正が容易になります。

先のスクリプトを監視と監視される側に分離してみます。まず、監視するスクリプト。

Script Editor で開く

on update(theObject)
    set theDate to getState() of theObject
    tell application (path to frontmost application as Unicode text)
        activate
        display dialog theDate buttons {"OK"} default button 1 giving up after 2 with icon 1
    end tell
end update

このスクリプトを Observer という名前でアプリケーション形式で保存します。update() ハンドラが監視しているオブジェクトの通知を受け取るハンドラで、監視しているオブジェクトを引数にとります。そして、監視しているオブジェクトの getState() ハンドラを使ってオブジェクトの状態を取り出します。

監視されるオブジェクトは、以下のようになります。

Script Editor で開く

property currentTrack : ""
property observerObject : application "Observer"

on getState()
    return currentTrack of me
end getState

on idle
    tell application "iTunes" to set tmp to name of current track

    if tmp is not currentTrack then
        set currentTrack to tmp
        ignoring application responses
            tell observerObject to update(a reference to me)
        end ignoring
    end if

    return 1
end idle

このスクリプトを Observable という名前で「実行後、自動的に終了しない」にチェックを入れ、アプリケーション形式で保存します。監視されるオブジェクトでは、監視しているオブジェクトに通知を送るために監視するオブジェクトを属性として持っています。また、監視しているオブジェクトが状態を取得できるように getState() ハンドラを定義しています。

このスクリプトを起動します。iTunes の曲が変わると Observer として保存したスクリプトアプリケーションに自分の参照を引数にして通知を送ります。Observer は通知を受け取ると Observable の状態を取得し、ダイアログを表示します。

...おお。頭の中だけで考えていたことだけど、ちゃんと動きますね。

このようにすることでなにが嬉しいか。そこが問題です。監視される方のオブジェクトは複数から監視されることがあります。今のサンプルでは監視側は一つです。複数の監視オブジェクトを保持するように変更しましょう。まず、監視するオブジェクトを作ります。

Script Editor で開く

on update(theObject)
    beep
end update

なんのことはなく、ビープするだけです。これを Beeper としてアプリケーション形式で保存します。観察されるオブジェクトは以下のように変更します。

Script Editor で開く

property currentTrack : ""
property observerObject : {application "Observer", application "Beeper"}

on getState()
    return currentTrack of me
end getState

on idle
    tell application "iTunes" to set tmp to name of current track

    if tmp is not currentTrack then
        set currentTrack to tmp
        repeat with thisObserver in observerObject
            ignoring application responses
                tell thisObserver to update(a reference to me)
            end ignoring
        end repeat
    end if

    return 1
end idle

複数の監視オブジェクトを持つように属性を変更し、繰り返しで通知を送っているだけです。監視側の処理を待たずに先に進むようにしているのでそれほ処理に手間取ることはないと思います。監視側も監視しているオブジェクトのことを気にすることなく勝手に動作しているので監視されているオブジェクトの idle ハンドラに影響が及ぶことはありません。これは、監視するオブジェクトを増やしているのですが、同時に機能を追加していることにもなります。

iTunes の状態を監視しているオブジェクトが監視されているオブジェクトで、そいつを監視するオブジェクトがいるというなんとも奇妙な関係ですが、iTunes で再生している曲が変わったときに教えてくれるようにハンドラを追加することができないので仕方がないです。本当は、iTunes にハンドラを追加する方がスマートな感じなのですが。しかも、観察されているオブジェクトが常時起動していて監視しているオブジェクトが変更時に呼び出されるだけですし。

今のままではダイアログが表示されるのでキーを打っているときに鬱陶しいのですが、Monzai や say コマンドを利用して発話させるようにすればもう少し使い勝手もあがるでしょう。

Script Editor で開く

on update(theObject)
    set theDate to getState() of theObject
    tell application "monzai"
        speak controller 1 moji theDate
    end tell
end update

さて。Observer は以上のような感じなのですが...思ったより長くなったので続きは、また今度。

0 件のコメント :

コメントを投稿