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

Factory Method の続きです。前回の最後の方に Factory(SimpleFactory) の例として以前に掲示板の方でご指摘いただいたコードを掲載しました。そのコードを Factory Method で書き直すとどうなるのでしょうか?

これがなかなか頭が痛い問題でして...。Factory Method で書き直してはいたのです。が、掲載を見送ったのでした。納得できなくて(というか、よく分からなくて)。とりあえず、もう一度、Factory Method パターンについて。

Factory Method パターンは、オブジェクトの生成を直接に行わず、オブジェクトの生成処理とそのオブジェクトを使用するものとの分離を行い、再利用性を高めます。AppleScript ではオブジェクトの生成というとハンドラ呼び出しがそれに当たります(もしくは、load script 命令による読み込み。copy 命令は...複製ですよね)。例えば、以下のような感じです。

Script Editor で開く

on TextEdit()
    script TextEdit
        on selectedText()
            tell application "TextEdit"
                if not (documents exists) then return ""
                activate
            end tell

            set tmp to the clipboard

            tell application "System Events"
                tell process "textedit"
                    keystroke "c" using command down
                end tell
            end tell

            if (the clipboard) is tmp then return ""

            return the clipboard as Unicode text
        end selectedText
    end script
end TextEdit

on KEdit()
    script KEdit
        on selectedText()
            tell application "KEdit"
                selected text of front document
            end tell
        end selectedText
    end script
end KEdit

-- client
on run
    set theEditor to TextEdit()
    tell theEditor to selectedText()
end run

使用者(run ハンドラ)は、オブジェクトの生成を直接行っています。この場合に困るのは生成するオブジェクトを変更した時。使用者はこの部分を変更しなくてはいけません。生成処理が変更されたら使用者にも影響が出ます。オブジェクトを使う方としてはこのような影響は受けたくありません。そこで、生成の処理をハンドラにしてしまいます。

Script Editor で開く

script SimpleFactory
    on getEditor()
        tell application "System Events"
            set a to ((name of processes whose frontmost is true) as Unicode text)
        end tell

        if a is "TextEdit" then
            return TextEdit()
        else if a is "KEdit" then
            return KEdit()
        else
            return missing value
        end if
    end getEditor
end script

-- client
on run
    tell application "TextEdit" to activate
    set theEditor to SimpleFactory's getEditor()
    if theEditor is missing value then return missing value

    tell theEditor to selectedText()
end run

こうしておくと生成処理に変更が加えられても使用者は気にすることなく利用できます。これが、Factory パターン(SimpleFactory ともいう)。これだけでもかなりの恩恵があります。では、Factory と Factory Method の違いは?

Factory は、オブジェクトの生成処理とどのオブジェクトを生成するかという判断も使用者から隠します。Factory Method パターンでは生成するオブジェクトごとに工場を用意し、ひとつの工場でひとつのオブジェクトを生成します。生成されるオブジェクトがどんなオブジェクトかということは使用者からは隠されますが、どのオブジェクトを生成するかという判断は使用者に委ねられます。生成するオブジェクトを変更したい場合、工場を持っているオブジェクトを切り替える必要があります。この辺りはできれば実行時に判断してオブジェクトを変更して欲しいものです。そのため、なんらかの仕掛けが必要になります。例えば、文字列からオブジェクトを生成するとか。

単純だけど分かりやすい SimpleFactory を使った方が無理がありません。この辺りは、ケースバイケース。Factory Method パターンは、大量のオブジェクトを生成する必要があるときや一緒に使って欲しいオブジェクトがある時などに利用できるパターンだそうで、あるオブジェクトとあるオブジェクトを一緒に使うということがはっきり分かっている時以外は適用しない方がいい、との注意もあります。それぞれのオブジェクトが単独で使えるような場合、なんでそのオブジェクトでオブジェクトを生成しているのかということが分かりにくくなるからです。

もしかしたら、Factory Method ではなく、Abstract Factory パターンの方がしっくりくるのかもしれません。まぁ、それは Abstract Factory の時のお話。では、引っ張りましたが Factory Method パターンを使って書き直したスクリプトを。

まず、生成されるオブジェクトをスクリプトファイルで保存します。

Script Editor で開く

on TextEdit()
    script TextEdit

        on selectedText()
            tell application "TextEdit"
                if not (documents exists) then return ""
                activate
            end tell

            set the clipboard to ""

            tell application "System Events"
                tell process "textedit"
                    keystroke "c" using command down
                end tell
            end tell
            return the clipboard as Unicode text
        end selectedText
    end script
end TextEdit

on init()
    return TextEdit()
end init

これを TextEdit.scpt として保存。init() は必ず実装します。以下のスクリプトを KEdit.scpt として保存。

Script Editor で開く

on KEdit()
    script KEdit

        on selectedText()
            tell application "KEdit"
                selected text of front document
            end tell
        end selectedText
    end script
end KEdit

on init()
    return KEdit(application "KEdit")
end init

オブジェクトを生成する工場を持つ以下のスクリプトを保存。

Script Editor で開く

on EditorEx(theEditor)
    script EditorEx
        property editor : theEditor

        on selectedText()
            set theEditor to my editorFacorty()
            if theEditor is missing value then return missing value
            tell theEditor to selectedText()
        end selectedText

        on editorFacorty()
            set appName to short name of (info for (path to editor))
            set fileName to appName & ".scpt"
            set scptFolder to path to scripts folder from user domain as Unicode text
            set scptFolder to scptFolder & "Applications:ExtraEditor:"
            set scptFile to scptFolder & fileName as Unicode text
            try
                return init() of (load script file scptFile)
            on error eMessage number eNumber
                return missing value
            end try
        end editorFacorty
    end script
end EditorEx

on init(theEditor)
    EditorEx(theEditor)
end init

ここでは、Editor.scpt という名前で保存しました。これらのファイルを ~/Library/Scripts/Applications/ に ExtraEditor というフォルダを作り、この中に保存します(もちろん、どこでもいいのですが)。Applications の中に保存するのは、スクリプトメニューに表示されないからです(ExtraEditor というアプリケーションがない限り)。

使用者は以下のような感じで呼び出します。

Script Editor で開く

on run
    tell application "System Events" to set f to name of (path to frontmost application)

    set scptFolder to path to scripts folder from user domain as Unicode text
    set scptFolder to scptFolder & "Applications:ExtraEditor:"
    set scptFile to scptFolder & "Editor.scpt"
    -- 初期化
    set theEditor to init(application f) of (load script file scptFile)
    -- 選択テキストを取得
    set str to selectedText() of theEditor
    if str is missing value then return
    display dialog str
end run

対象のエディタを指定して初期化します。ここでは、前面にあるアプリケーションの名前で初期化しています。前面のアプリケーションが対象のエディタでなかった場合、missing value が返ってきます。スクリプトメニューなどから実行してみてください。あとは、勝手にオブジェクトが判断するのでらくちん。バンドル形式にしてバンドルの中に全部のスクリプトを入れても構いません(その場合、修正が必要です)。

これなら対応エディタの数が増えても使用者の方で変更はいらないですね、と作った後に気がつく。オブジェクトを作成する方も変更がいらないですし。ひとつの工場がひとつのオブジェクトを作成するという部分もあっていますし、と自画自賛。いや、もうここまで作るのにどれだけの無駄スクリプトを書いたことか。累々たるってところです。ちなみに、今回掲載したスクリプトは、最後の最後で全面的に変更したものです。以前のものは...見れたものではなかったり。

ところで、short name って AppleScript 1.10 からでしたっけ?なら、Mac OS 10.4 以前では動かないですね。

若干、強引な感じもなきにしもあらずですが...こんなところでいかがでしょうか?

追記 - 06/09/29

掲示板の方でご指摘をいただきました。これ、よくよく見ると editorFacorty() ハンドラだけでいいですね。パターンにとらわれ過ぎていました。自戒の意味も込めて修正せずにこのままにしておきます。

0 件のコメント :

コメントを投稿