クリップボード活用のススメ

クリップボードはアプリケーション間でデータのやり取りを行うときに非常に重宝します。

クリップボードの中には様々なデータが格納されます。AppleScript に対応していないアプリケーションでもコピー & ペーストは使えることがほとんどなので、

  1. 対象アプリケーションでデータをコピー
  2. AppleScript でデータを加工
  3. クリップボードに加工したデータを格納
  4. 対象アプリケーションでペースト

と、このようにクリップボードを介して AppleScript を利用することがあります。その昔、Script Editor が AppleScript に対応していなかったとき、よく使っていました。また、TextEdit は AppleScript に対応していますが、選択範囲を取得できないのでクリップボードに選択範囲をコピーし、クリップボード内のデータを加工し、TextEdit でペーストするということもよく行います。

使いようによっては便利なクリップボード。そのクリップボードの中身を調べるとなかなか興味深いものがあります。AppleScript でクリップボードを扱う命令は、StandardAdditions.osax が提供していて、以下の命令があります。

  • clipboard info(クリップボードの情報を取得)
  • the clipboard(クリップボードのデータを取り出す)
  • set the clipboard to(クリップボードにデータを格納する)

AppleScript は文字列をバイトで数えるのではなく、多バイト文字でも一文字は一文字として数えます。そうではなく、単純にバイト数を知りたいときにクリップボードは使えます。

Script Editor で開く

set theText to "文字列"
set the clipboard to (theText as text)
clipboard info
--> {{string, 6}}

string クラスで 6 バイトになります。どうしてリストの中にリストがあるのかというと、クリップボードは複数のデータを保管するからです。例えば、修飾された文字列をコピーしたなら、修飾された文字列のデータと修飾されていない文字列のデータ、また、ユニコード文字列のデータ、ユニコードでも UTF-8 や UTF-16 などの異なったエンコードの文字列データ...これらがクリップボードに一緒に保管されます。複数のデータが入っているので対応アプリケーションでペーストしたときに適切なデータが取り出されて貼り付けられます。

クリップボードは様々なデータが扱えます。それらのデータの中に目的のデータがあるかどうかを調べるには clipboard info 命令の for オプションを使います。for オプションの引数に目的のデータ型を指定します。

Script Editor で開く

clipboard info for string
--> {{string, 61}}

目的のデータがあれば、このような結果になります。ない場合は空のリストが返ってきます。

Script Editor で開く

on dataIsInClipboard for dataType
    return (clipboard info for dataType) is not {}
end dataIsInClipboard

このようなハンドラにしておけば、目的のデータが存在するかどうかを調べることができます。目的のデータを取り出すには、the clipboard 命令を使います。しかし、このままでは目的のデータを取り出すことはできないので、as を使って取り出します。

Script Editor で開く

set str to the clipboard as Unicode text
class of str
--> Unicode text

set str to the clipboard as string
class of str
--> string

set str to the clipboard as «class RTF »
class of str
--> «class RTF »

Script Editor 上で編集中のドキュメントの文字列をコピーして実行してみました。このように as で指定することでクリップボードに格納されている複数のデータから目的のものを取り出すことができます。これも、以下のようなハンドラにしておきます。

Script Editor で開く

on dataFromClipboard for dataType
    return the clipboard as dataType
end dataFromClipboard

さて。ここからが、応用。いろんなアプリケーションでいろんなデータをコピーして clipboard info 命令で返ってくる結果を見てみると、«» という括弧で囲まれたデータ型が返ってくることがあります。これらは、AppleScript で定義されていないデータ型です。以下、その一部。

  • «class ut16»
  • «class utf8»
  • «class RTF »
  • «class rtfd»
  • «class hfs »
  • «class furl»
  • «class urln»
  • «class PDF »
  • «class icns»
  • «class moov»

パッと見て、どんなデータか概ね想像がつくと思います。これらのデータ型は AppleScript で扱える(加工できる)ものもあり、そうでないものもあります。文字列ならそのまま扱える場合もあります。画像に関するデータ型もありますが、これらは as として型を変換することはできません。

ほとんどのデータ型は AppleScript で加工できないのですが、ファイルに書き出すことはできます。例えば、QuickTime Player で動画の一部分をコピーするとクリップボードには «class moov» というデータが格納されます。このデータを write 命令で書き出すと QuickTime の mov ファイルを作ることができます。

Script Editor で開く

on run
    set theFile to (path to desktop as Unicode text) & "tmp.mov"

    if (dataIsInClipboard for «class moov») then
        set theData to dataFromClipboard for «class moov»
        set errInfo to {|number|:0, |error message|:""}
        set errRef to a reference to errInfo
        if not (writeFile(theFile, theData, errRef)) then
            return errInfo
        end if
    end if
end run

on dataIsInClipboard for dataType
    return (clipboard info for dataType) is not {}
end dataIsInClipboard

on dataFromClipboard for dataType
    return the clipboard as dataType
end dataFromClipboard

on writeFile(theFile, theData, errRef)
    try
        set fh to open for access file theFile with write permission
        set eof fh to 0
        write theData to fh starting at eof
        close access fh
        return true
    on error errMsg number errNum
        try
            close access fh
        end try
        set contents of errRef to {|number|:errNum, |error message|:errMsg}
        log {errNum, errMsg}
        return false
    end try
end writeFile

AppleScript の write 命令で書き出したファイルは TextEdit で開くように関連づけられている(「iTunes のアートワークをファイルに書き出す」参照)ので、クリエータータイプとファイルタイプを変更する必要があります。

ちなみに、«class RTF » を拡張子 rtf のファイルに書き出すと RTF 書類として保存できます。iCal のイベントをコピーするとこのクラスが返ってきます。ファイルに書き出すことでイベントの一覧が簡単にファイルに出力できます。Mail のメッセージ(件名や差出人が表示されている部分)をコピーすると «class rtfd» が返ってきます。RTFD のデータなのですが、RTFD はパッケージ形式のファイルなのでそのまま書き出しても開くことはできないファイルになります。RTFD ファイルとして開けるようにするには面倒なので、省略。

このようにアプリケーションで選択した項目がどのようなデータ型でクリップボードに入っているかが分かると RTF やテキストファイル、その他のファイルとして保存することが可能です。

Preview で開いている PDF ファイルは選択範囲を指定してコピーすることができます。このとき、クリップボードには «class PDF » のデータが格納されます。これを write 命令で PDF ファイルで書き出せば、選択範囲をファイルに保存することができます。

Finder で項目をコピーします。すると、クリップボードにはファイルの名前やパスといった情報とともにアイコンデータ(«class icns»)も格納されます(複数項目のコピーではアイコンデータは格納されません)。もちろん、このデータもファイルに書き出すことでアイコンファイルとして保存できます。また、Preview.app で表示することも可能です。

Script Editor で開く

on run
    pictureFromClipboardAtPreview()
end run

on pictureFromClipboardAtPreview()
    if not (dataIsInClipboard for «class icns») then return

    tell application "Preview" to activate

    tell application "System Events"
        tell process "preview"
            try
                keystroke "n" using command down
            end try
        end tell
    end tell
end pictureFromClipboardAtPreview

on dataIsInClipboard for dataType
    return (clipboard info for dataType) is not {}
end dataIsInClipboard

Finder の項目をコピーしたとき、アイコンのデータと一緒に PICT データもクリップボードに格納されるのですが、こちらは 32 x 32 のサイズの画像が格納されるのでアイコンを抜き出すという目的では使えません。

このクリップボードの内容を Preview で表示するというのは iTunes のアートワークでも使えます。

Script Editor で開く

tell application "iTunes"
    activate
    set thisTrack to current track

    if (exists artworks of thisTrack) then
        repeat with thisArtwork in artworks of thisTrack
            set pictData to data of thisArtwork
            set the clipboard to pictData

            my pictureFromClipboardAtPreview()
        end repeat

        beep 2
    end if
end tell

on pictureFromClipboardAtPreview()
    if not (dataIsInClipboard for picture) then return

    tell application "Preview" to activate

    tell application "System Events"
        tell process "preview"
            try
                keystroke "n" using command down
            end try
        end tell
    end tell
end pictureFromClipboardAtPreview

on dataIsInClipboard for dataType
    return (clipboard info for dataType) is not {}
end dataIsInClipboard

アプリケーションが返す picture データをそのまま set the clipboard to 命令でクリップボードに格納するだけです。

read 命令でファイルを画像データとして書き出し、それを set the clipboard to 命令でクリップボードに格納し、アプリケーションでペーストするということもできます。

Script Editor で開く

set theData to read (choose file without invisibles) as TIFF picture
set the clipboard to theData

ただし、これは TIFF か PICT だけに限られます。アプリケーション間での画像データのやり取りは、どうもこれらのファイル形式に限られているようで(どんな形式の画像でもクリップボードの中では PICT、または、PICT と TIFF に変換されて保持される)。

では、最後。Finder では選択項目をコピーして別の場所にペーストすることで複製を作ることができます。このときクリップボードには、ファイルのパスが格納されます。«class hfs » と «class furl» がそうです。前者のパスは AppleScript で作ることはできませんが、«class furl» は以下のように作ることが可能です。

Script Editor で開く

set theFile to POSIX path of (choose file without invisibles)
set theFile to theFile as POSIX file
set the clipboard to theFile

選択したファイルを POSIX file に変換します。そして、クリップボードに格納します。この後 Finder でどこかの場所にペーストすれば、そのファイルが複製されます。

ただし、この方法では Finder のように複数の項目をコピーすることはできません。もし、複数の項目をコピーしたいなら、以下のような方法があります。

Script Editor で開く

set fileList to choose file with multiple selections allowed without invisibles
set tmp to {}
repeat with thisItem in fileList
    set end of tmp to POSIX path of thisItem
end repeat

tell application "Automator"
    set preferred type of pasteboard "general" to "file names"
    set contents of pasteboard "general" to tmp
end tell

いちいち Automator を起動するのが嫌というときは Xcode でダミーのスクリプタブルアプリケーションを作ってもいいでしょう。実際、ダミーのスクリプタブルアプリケーションって使い道が多々あって重宝します。最近、hetimaの日記 - Xcode の AppleScript で call method でも触れられていましたが、なんといっても call method が使えるようになります。

一応、ダミーのスクリプタブルアプリケーションの作り方を書いておくと、

  1. Xcode で AppleScript Application の新規プロジェクトを作る
  2. Info.plist を開き NSBGOnly = 1 を追加する
  3. ビルドする
  4. 完成

となります。バックグラウンドで動作するので邪魔になりません。で、先のコードを以下のように修正。

Script Editor で開く

set fileList to choose file with multiple selections allowed without invisibles
set tmp to {}
repeat with thisItem in fileList
    set end of tmp to POSIX path of thisItem
end repeat

tell application "Dummy"
    set preferred type of pasteboard "general" to "file names"
    set contents of pasteboard "general" to tmp
    quit
end tell

hetimaの日記 - Xcode の AppleScript で call method では、AppleScriptKit.framework をプロジェクトにリンクし、NSAppleScriptEnabled = YES を Info.plist に追加していますが、AppleScript Application はどちらも最初から設定されています。もちろん、その他のプロジェクトで作成するアプリケーションに AppleScript の機能を追加するというのであれば、hetima さんのように行う必要があります。このダミーのアプリケーションは非常に便利なので AppleScript 使いの少数の人間だけで共有されていたトップシークレット(嘘)だったのですが、hetima さんの記事によって広まってしまいますね。

Automator を使って楽をする?

いまいち盛り上がりに欠ける気がしますね、Automator。実際、Automator 関連の記事を書いたにもかかわらず、あまり使うことがありません。その目的、コンセプトを見ていると「なるほど、使えると便利なんだな」と思うのですが...。

「Automator を使ったら自動化ができて便利だよ」
「へー、そうなんだ」
「...」
「で、Automator って何ができるの?」

そう、Automator って何ができるか分からない。自動化って何?

「たとえば、『ブログに掲載する画像をリサイズする』ということをどうやっている?」
「Photoshop Elements で画像を開いて大きさを変更して、Web 用に保存してそれをブログにアップロードして...」
「結構、面倒だね」
「うん、面倒」
「そういう面倒な処理を自動でやってくれるのが Automator なんだ」
「へー。やってみて」
「画像リサイズのアクションを置いて、ファイルをアップロード...あれ?アクションがない?」
「できねーじゃん!」

できないことも多いのです。

「ここにたくさんのファイルがあるんだけど...このファイルの名前を変えたいの。でも、数が多いから手でやるのは面倒で...なんかいい方法ない?」
「ああ、こういうときこそ Automator」
「Automator でできるの?」
「もちろん」
「じゃ、お願い」
「『クリスマスパーティーaxcsw051224.jpg』『ハイキングxsdJKf061023.jpg』『ハイキングxsQecdL061023.jpg』『ハイキングweFvbSg061023.jpg』(以下続く)...何この奇妙奇天烈なファイルの名前?」
「うん、そのファイルの名前からアルファベット小文字だけを取り除きたいの」
「...」
「正規表現も使えないのかよ!」

(「シェルスクリプトを実行」、「AppleScript を実行」アクションを使えばできますが)使えません。

「Automator でできるっていつもいうけど、ほとんどできないじゃない!」 「いや、もっと普遍的で一般的な処理なら...」 「なによ!その一般的な処理って!私がしたいのは、私の携帯のカレンダーとアドレスを Mac の iCal とアドレスブックと同じものにしたいってことだけじゃない!」

そう。自動化したい処理って同じ処理でも微妙に要求が違います。使う人の数だけあるといってもいいでしょう。いまのところ Automator では(もしかしたら最も望まれていることかもしれない)微妙な要求に応えてくれることはありません。

「アクションの数が多くてどれがどれだか...」
「そういうときは、ほら、ウィンドウの上のところにあるテキストフィールドに検索したい言葉を入力すれば、アクションを探すことができるよ」
「そうなの?えーと、『画像』と」
「...」
「アクションがないわね」
「『画像』じゃなく『イメージ』で検索するんだよ...」
「なんで?一緒じゃない?私は、日本人よ!馬鹿にするな!」

Automator の検索は賢くありません。

「『Finder 項目に Spotlight コメントを追加』って便利ね。Spotlight と組み合わせれば、簡単にファイルが見つかっちゃう」
「(よかったと思いつつ)Automator も捨てたもんじゃないでしょ?」
「でも、コメントに何を書いたか忘れちゃうのよね。書いたコメントを一覧表示してくれるアクションってないの?」
「...」
「あまり、期待していなかったわ。じゃ、コメントを追加するのを勝手にやってくれるアクションってないの?コメントいちいち追加するのが面倒で...」
「...」
...頬を撫でる一陣の風。今日も冷え込みは厳しい。

ワークフローのご利用は計画的に。

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 パターンとの違いを分かっていただけたでしょうか?あまり自信がありませんが...。

iTunes のアートワークをファイルに書き出す

個人的には人気アプリケーションは、

  1. iTunes
  2. Safari
  3. Finder

だと思います。人気アプリケーションといっても AppleScript の話題で触れられることの多いアプリケーションです。AppleScript で検索するとよく見る話題が上記のアプリケーション関連。このサイトでは触れることのないアプリケーションともいえます。既に書かれているのにわざわざ書くこともないよな...と思って触れることがなかったのでした。同様に知っていても書かない話題は多数あります。例えば、AppleScript の日本語問題とか。

が、ちょっと考えを改めました。気づいたというか。これだけ書かれているということは、要求があるんだということに。そこで、少しは、軟派な話題でも取り扱ってみるか、と(エラソーに...)。

で、iTunes。iTunes 7 になってからアートワークを自動でダウンロードして取得できるようになりました。取得したアートワークは ~/Music/iTunes フォルダ(デフォルト)の中にある Album Artwork フォルダの階層深いところに保管されているようです。

このダウンロードされたアートワークをファイルに書き出すのが目的です。まずは、iTunes で現在再生中の曲のアートワークを取得してみます。

Script Editor で開く

tell application "iTunes"
    try
        set thisTrack to current track
    on error errMsg number errNum
        -- 再生中でないと current track は取得できないので
        return
    end try

    tell thisTrack
        if exists artworks then
            return artworks -- アートワークの取得
        end if
    end tell
end tell

最初のエラートラップは、ちゃんと調べるのが面倒だから仕掛けているだけでたいした意味はありません。現在再生中の曲を取得して、その曲のアートワークを調べています。一つの曲に複数のアートワークを設定できるので複数形です。アートワークが設定されているかどうかは exists で調べることができます。

AppleScript から調べている限り、ダウンロードされたアートワークか、自分で設定したアートワークかといったことはあまり気にする必要がないようです。どちらのアートワークでも data 属性を取得できるようですし。つまり、アートワークが設定されているかどうかを調べればいいだけで、ダウンロードされて保管されている拡張子 itc のファイルにアクセスする必要はないのです。もちろん、itc から画像を取得したいとなると別ですが、その場合は、こちらの記事を参照するといいと思います。

アートワークのファイルタイプが JPEG や PNG でも data 属性で取得できるのは picture、つまり、PICT 形式です。だから、ファイルタイプはなにかといったことも気にする必要がなく、単純に write 命令でファイルに書き出せばいいようです。

Script Editor で開く

tell application "iTunes"
    try
        set thisTrack to current track
    on error errMsg number errNum
        -- 再生中でないと current track は取得できないので
        return
    end try

    tell thisTrack
        if exists artworks then
            set thisArtist to artist
            set thisAlbum to album
            set desktopFolder to path to desktop as Unicode text
            repeat with i from 1 to count artworks
                set fileName to thisArtist & "_" & thisAlbum & "_" & (i as string)
                set theFile to (desktopFolder & fileName & ".pict") as Unicode text
                set theResult to my saveAsPICT(theFile, data of artwork i)
                if theResult then
                    my openWithPreview(theFile)
                end if
            end repeat
        end if
    end tell
end tell

on saveAsPICT(theFile, theData)
    try
        set fh to open for access file theFile with write permission
        set eof of fh to 512
        write theData to fh starting at 513
        close access fh
        return true
    on error errMsg number errNum
        try
            close access fh
        end try
        log {errNum, errMsg}
        return false
    end try
end saveAsPICT

on openWithPreview(theFile)
    tell application "Finder"
        set theFile to document file theFile
        set file type of theFile to "    "
        set creator type of theFile to "    "
    end tell
end openWithPreview

ただ、書き出せばいいといってもいくつか注意点があります。AppleScript の write 命令で書き出されたファイルはクリエータータイプが ttxt、ファイルタイプが TEXT になります。これをそのまま開こうとすると TextEdit で開こうとします。今回は画像ファイルを書き出すのですから TextEdit で開こうとしても開けません。そこで、ファイルタイプとクリエータータイプを変更(具体的には両方とも半角スペース 4 つ)し、プレビューで開くようにします。

もう一点。artwork クラスの data 属性は picture(PICT)を返します。これをそのままファイルに書き出しても開くことができないファイルになります。AppleScript の write 命令で picture を書き出すには eof を 512 バイトに設定し、513 バイト目から書き出す必要があります。

PICT で書き出すことができたら、後は煮て食おうが焼いて食おうがお気に召すままに。

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

AppleScript でもデザインパターンを使ってみようというこの企画。『オブジェクト指向における再利用のためのデザインパターン』の中から今回は、Strategy パターンを取り上げます。

Strategy パターンは、アルゴリズムを汎用化し、プログラムの実行中にアルゴリズムを交換可能にするために利用されます。目的によって処理方法を変更したい...こういう状況はよくあることと思います。例えば、プログラムの開発中。肝心のデータ処理の部分は完成していないのでダミーのデータを用いる。

property DEBUG : true

if DEBUG then
    dummy()
else if not DEBUG then
    processor()
end if

...(コードが続く)

こんな感じですね。または、体重や身長、年齢などの比較。これらの属性を持った人間オブジェクトを作ったとします。

Script Editor で開く

on Human(theName, theAge, theHeight, theWeight)
    script Human
        property _name : theName
        property _age : theAge
        property _height : theHeight
        property _weight : theWeight
    end script
end Human

どの属性も比較が行えますね。単純に以下のような比較を。

Script Editor で開く

on Human(theName, theAge, theHeight, theWeight)
    script Human
        property _name : theName
        property _age : theAge
        property _height : theHeight
        property _weight : theWeight
    end script
end Human

on run
    set nancy to Human("Nancy", 24, 154, 48)
    set bob to Human("Bob", 36, 172, 62)

    set theResult to choose from list {"Age", "Weight"} default items {"Age"}
    if theResult is false then return
    set theResult to theResult as Unicode text

    if theResult is "Age" then
        return ageCompare(nancy, bob)
    else if theResult is "Weight" then
        return weightCompare(nancy, bob)
    end if
end run

on ageCompare(human1, human2)
    if (human1's _age) is greater than (human2's _age) then
        return 1
    else if (human1's _age) is (human2's _age) then
        return 0
    else
        return -1
    end if
end ageCompare

on weightCompare(human1, human2)
    if (human1's _weight) is greater than (human2's _weight) then
        return 1
    else if (human1's _weight) is (human2's _weight) then
        return 0
    else
        return -1
    end if
end weightCompare

一見ごく当たり前のコードですが、if 文の部分をどうにかしたいと思うのではないでしょうか。身長や名前の比較も行おうとすると、if 文が長くなり、修正も煩雑になるからです。最初に提示したプログラム開発中のデバックでも同じことが言えます。

こういうときに Strategy パターンの適用を検討するといいかもしれません。Strategy パターンは実際の処理の部分(アルゴリズム)を切り分け、利用するときに置き換えるようにします。実際に適用してみます。

まず、アルゴリズムの部分を別のオブジェクトとして定義します。

Script Editor で開く

script WeightComparator
    on compare(human1, human2)
        if (human1's _weight) is greater than (human2's _weight) then
            return 1
        else if (human1's _weight) is (human2's _weight) then
            return 0
        else
            return -1
        end if
    end compare
end script

script AgeComparator
    on compare(human1, human2)
        if (human1's _age) is greater than (human2's _age) then
            return 1
        else if (human1's _age) is (human2's _age) then
            return 0
        else
            return -1
        end if
    end compare
end script

体重と年齢の比較ハンドラを compare() と同じ名前にしてスクリプトオブジェクト内で定義します。同じ名前にしておくのは呼び出すスクリプトオブジェクトが変わっても compare() を呼び出せば目的が達成できるようにするためです。AppleScript では、スクリプトオブジェクトの型やインターフェースの定義ができない代わりにこのようにしておきます。

そして、目的によって比較オブジェクトを交換可能にするためのスクリプトオブジェクトを追加します。

Script Editor で開く

on Comparator(theObject)
    script Comparator
        property comparatorObject : theObject

        on setComparator(theObject)
            set comparatorObject of me to theObject
        end setComparator

        on compare(human1, human2)
            tell comparatorObject of me to return compare(human1, human2)
        end compare
    end script
end Comparator

比較オブジェクトを受け取り、比較オブジェクトを自身の属性に設定します。実際の比較は compare() ハンドラが行います。compare() ハンドラでは属性に設定されている比較オブジェクトの compare() ハンドラを呼び出します(委譲ってやつですね)。以下のようにして利用します。

Script Editor で開く

on run
    set nancy to Human("Nancy", 24, 154, 48)
    set bob to Human("Bob", 36, 172, 62)

    -- 年齢で比較
    set theObject to Comparator(AgeComparator)
    tell theObject
        compare(bob, nancy)
        --> 1
        -- 比較を体重に変更
        setComparator(WeightComparator)
        compare(bob, nancy)
        --> 1
    end tell
end run

if 文がなくなりました。...気づいた人もいると思いますが、比較を変えるときに if 文を使うことになるのでは?その通りですね。どこで if 文を使うかが変わっただけです。実際には factory クラスを追加して比較オブジェクトの変更を行うようにします。

Script Editor で開く

script ComparatorFactory

    on createComparator(str)
        if str is "Age" then
            return Comparator(AgeComparator)
        else if str is "Weight" then
            return Comparator(WeightComparator)
        else
            return missing value
        end if
    end createComparator
end script

on Comparator(theObject)
    script Comparator
        property comparatorObject : theObject

        on compare(human1, human2)
            tell comparatorObject of me to return compare(human1, human2)
        end compare
    end script
end Comparator

Comparator() も少し変更。呼び出しは、以下のようになります。

Script Editor で開く

on run
    set nancy to Human("Nancy", 24, 154, 48)
    set bob to Human("Bob", 36, 172, 62)

    set theResult to choose from list {"Age", "Weight"} default items {"Age"}
    if theResult is false then return
    set theResult to theResult as Unicode text

    set theObject to ComparatorFactory's createComparator(theResult)
    tell theObject to compare(bob, nancy)
end run

すっきりしたのではないでしょうか?

このような感じで Strategy パターンは処理(アルゴリズム)の部分を別のオブジェクト(部品)として切り分けておきます。部品は状況によって適宜置き換えられ、呼び出されます。部品の再利用性を高め、呼び出し側の修正といった労力を極力減らし、コードの見通しを良くします。

Strategy パターンは、オブジェクトコンポジションという手法を用いています。継承によらない機能の拡張ということ Decorator パターンのときに書いたと思うのですが、オブジェクトコンポジションは属性にオブジェクトを設定し、そのオブジェクトの機能を呼び出す(委譲)ことです。この手法は頻繁に用いられます。こうすることでオブジェクト間の関係を緩くすることができます。疎結合ってやつですね。逆が密結合。疎結合でオブジェクト間の関係を築くと、一方のオブジェクトの修正が他方のオブジェクトに与える影響を小さくしてくれます。

これは AppleScript でも利用できる手法ですので何かと重宝します。コンポジションを語るときに忘れることができないのがインターフェース。今までにも何回かこの言葉を使ったことがあるのですが、AppleScript では使えないのでちゃんと説明せずに使ってきました。インターフェースは、メソッドの名前と引数だけを定義したものです。抽象クラスとはまた別で、インターフェースはそのメソッドを確実に実装してもらいたいときに使います。あるインターフェースを実装したクラスは、そのメソッドを確実に持っていることになります。ということは、呼び出す方がそのメソッドがあるかないかを気にする必要がないのです。あるのは確実だから。

また、抽象クラスも AppleScript では作ることができません。似たものを作ることはできますが、抽象メソッドを継承先で実装する必要があるといった義務化ができません。

インターフェースも抽象クラスも AppleScript では使うことができません。実装クラス、サブクラスで確実に実装するといった義務を課すことができないわけです。実装するもしないも作る方の好き勝手。コンパイラがその辺りの面倒を見てエラーを出すといったこともないわけです。こういったことは自分でルールを作ってそのルールに則ってオブジェクトを作らないといけないのです。例えば、同じ名前のハンドラで定義しておくようにするとか。

つまり、AppleScript でオブジェクト指向的な開発をする、ということはルール作りから始まるということです。ましてや複数の人間で行うとなると...考えただけでしんどくなります。

インターフェースや抽象クラスを使うことはできないのですが、それだけ AppleScript はフレキシブルなのです。粘土をこねるように自分の好き勝手にできます(だからこそ共有ライブラリもないのでしょうけど)。

最後は Strategy パターンとは関係のない話になってしまいましたが。では、また。