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

構造に関するデザインパターンを取り上げていませんでしたね。そんなわけで、Decorator パターン。

オブジェクト指向における再利用のためのデザインパターン』は、オブジェクトの「生成」に関するパターン、オブジェクトの「構造」に関するパターン、オブジェクトの「振る舞い」に関するパターンに分類されています。それぞれ、「オブジェクト生成の工夫」、「オブジェクトの組み合わせの工夫」、「オブジェクトの動作に関する工夫」と言い換えることができます。 「そんなことわざわざデザインパターンとして教えてもらわなくても前から実行しているよ」というようなものからなるほどというものまでさまざま。

Observer パターンは「振る舞い」、Factory Method パターンは「生成」、Iterator パターンは「振る舞い」、Template Method パターンは「振る舞い」、と。

Decorator パターンは、オブジェクトの構造に関するデザインパターンです。どんな工夫なのか?

例えば、既にあるオブジェクトの動作を拡張したい時。通常は、継承を用います。以下のような感じ。

Script Editor で開く

script Joe

    on getName()
        return name of me
    end getName
end script

script Agnes
    property parent : Joe

    on getName()
        return "My name is " & (continue getName())
    end getName
end script

tell Agnes to getName()
--> "My name is Agnes"

継承した方でハンドラを上書きして拡張を行い、そのままでいい部分は continue で親のハンドラを呼び出す。これでいいのですが、継承に問題がないわけではありません。例えば、スクリプト実行時に動的に親を変更できないとか。継承を使うと限定されてしまうのですね。ちょっとした問題だと言えばそうなのですが。

こういう制限がない方が望ましい状況があります。拡張した機能が汎用的で使い勝手のいいものだった場合。こういう時は特定の親を持つオブジェクトであるより、拡張対象を自由に切り替えられた方が便利です。

そこで、拡張機能だけを持たしたオブジェクトを作り、このオブジェクトの属性に拡張対象のオブジェクトを持たせるようにします。拡張したい部分を追加し、既存の機能を使いたいときは、属性で保持しているオブジェクトの機能を呼び出すようにします。このことを指して委譲と言うようです。AppleScript で委譲というと continue を使った親オブジェクトのハンドラ呼び出しが思い浮かびますが。Decorator パターンは、継承によらずに機能の拡張を行えるようにするという工夫なのです。

まずは、概念を理解するためのコード。Decorator パターンは、以下のようになります。

Script Editor で開く

on Component()
    script Component
        on getName()
            return name of me
        end getName
    end script
end Component

on Decorator(theObject)
    script Decorator
        property targetObject : theObject

        on getName()
            set str to "=="
            set str to str & getName() of targetObject
            set str to str & "=="

            return str
        end getName
    end script
end Decorator

on run
    set theComponent to Component()
    set theDecorator to Decorator(theComponent)
    tell theDecorator to getName()
    --> "==Component=="
end run

Component(= 部品)の getName() を拡張します(拡張対象)。そのために、同じハンドラを持ったオブジェクトを作ります(Decorator = 装飾者)。この Decorator の属性で Component を保持します。Decorator は、この属性を通して Component のオリジナルのハンドラを呼び出します。

このオリジナルのハンドラ呼び出しの前後に処理を追加することができます。ここでは文字列の前後に「==」という文字列を追加しています。さらに面白いのは、Decorator で Decorator を拡張できるということです。

Script Editor で開く

on Component()
    script Component
        on getName()
            return name of me
        end getName
    end script
end Component

on Decorator(theObject)
    script Decorator
        property targetObject : theObject

        on getName()
            set str to "=="
            set str to str & getName() of targetObject
            set str to str & "=="

            return str
        end getName
    end script
end Decorator

on run
    set theComponent to Component()
    set theDecorator to Decorator(theComponent)
    tell theDecorator to getName()
    --> "==Component=="
    set theDecorator2 to Decorator(theDecorator)
    tell theDecorator2 to getName()
    --> "====Component===="
end run

前後に付加する文字列が増えています。

このように Decorator は、一つのスクリプトオブジェクトのハンドラ呼び出しで付加する機能を動的に自由自在に変更することができます。継承ではこういうことができません。多分。また、Decorator は、拡張対象 Component と同じハンドラを持っています(これは、Component パターンが機能を拡張するという体裁を持つデザインパターンなので、こういう制限があります。この辺りは最後の方で少し触れます)。ということは、利用者はそれが Component か Decorator かを意識する必要がありません。

最初に基本的な機能を持つオブジェクトを作り、機能は後で追加していくことでオブジェクトが肥大化することを防ぐことができます。継承で拡張することを考えていると、親のオブジェクトにいろいろな機能を詰め込んでしまうことがあります。そうするとオブジェクトは肥大し、再利用が難しいものになってしまいます。拡張対象を属性で保持し、保持しているオブジェクトの機能を呼び出す...たったこれだけのことなのですが、メリットは大きいようです。

上記のサンプルはパターンの概念だけのものなので...もう少し AppleScript 的なサンプルを。まず、Finder で選択している項目を返すスクリプト。

Script Editor で開く

on FinderEx()
    script FinderEx
        on getSelection()
            tell application "Finder" to selection
        end getSelection
    end script
end FinderEx

これに Decorator を追加します。Finder の selection は、返り値の順番がまちまちです(並んでいるのですが直感的ではないので)。これを順番に並んだ項目で返すようにしましょう。Finder の sort 命令を使います。

Script Editor で開く

on SortDecorator(theObject)
    script SortDecorator
        property targetObject : theObject

        on getSelection()
            set theList to getSelection() of targetObject
            if theList is {} then return {}

            tell application "Finder"
                sort theList by name
            end tell
        end getSelection
    end script
end SortDecorator

on run
    set theComponent to FinderEx()
    set theSortDecorator to SortDecorator(theComponent)
    tell theSortDecorator to getSelection()
end run

これで選択項目は順番が名前順になったリストで返ってきます。Finder の selection は便利なのですが、返ってくる値は、Finder のオブジェクトです。他のアプリケーションに渡すには型をキャストしないといけません。そういう機能も追加してみましょう。

Script Editor で開く

on AliasDecorator(theObject)
    script AliasDecorator
        property targetObject : theObject

        on getSelection()
            set theList to getSelection() of targetObject
            if theList is {} then return {}
            tell application "Finder"
                set tmp to {}
                repeat with thisItem in theList
                    set end of tmp to thisItem as alias
                end repeat
                return tmp
            end tell
        end getSelection
    end script
end AliasDecorator

on run
    set theComponent to FinderEx()
    set theAliasDecorator to AliasDecorator(theComponent)
    tell theAliasDecorator to getSelection()
end run

では、名前の順番に並んだ項目を alias 参照で返して欲しいという時は?既に機能はあるのですから、後は組み合わせるだけです。

Script Editor で開く

on run
    set theComponent to FinderEx()
    set theSortDecorator to SortDecorator(theComponent)
    set theAliasDecorator to AliasDecorator(theSortDecorator)
    tell theAliasDecorator to getSelection()
end run

これで名前で並んだの alias 参照を取得できます(alias 参照を Finder の sort 命令で並び替えはできないので呼び出し順を間違えるとエラーになります)。特になんの手も加えたくないのであれば、FinderEx の getSelection() をそのまま使えばいいのです。これで、目的によって機能を追加したり、組み合わせたりといったことが可能になりますし、修正も楽になりますね。

最後に。先に書いた制限について。Decorator は拡張対象と同じハンドラしか持てない、という制限があります。AppleScript では関係ない話なのですが、微妙に関係する部分もあるので少し触れておきます。例えば、最初のサンプルを以下のように変えたとします。

Script Editor で開く

on Component()
    script Component
        on getName()
            return name of me
        end getName
    end script
end Component

on Decorator(theObject)
    script Decorator
        property targetObject : theObject
        property append : ""

        on setAppend(str)
            set append of me to str
        end setAppend

        on getName()
            set str to append
            set str to str & getName() of targetObject
            set str to str & append

            return str
        end getName
    end script
end Decorator

on run
    set theComponent to Component()
    set theDecorator to Decorator(theComponent)
    tell theDecorator
        setAppend("**")
        getName()
    end tell
end run

追加する文字列を設定できるようにしただけなのですが、利用するには setAppend() で文字列を設定しておく必要があります。こうなるとなにが困るか。Component と Deocrator のどちらであるかを使う人が意識しないといけなくなります。先にも書いたように、同じハンドラしか持っていないということは、両者の違いを意識しなくて済むのです。Component をそのまま使いたいときもあるし、Decorator で拡張した機能を使いたいときもある。けど、Decorator を使うときには必ず setAppend() を呼び出さないとなれば、コードの修正が必要になりますし、冗長です。

両者の違いを考えなくてもいいように同じハンドラしか持たない、という制限を加えているのです。それ以外の要素が入ってくるようなら Decorator で拡張したとはいえません。ここで制限が必要になるのですが、AppleScript にゃ関係のない話。AppleScript のスクリプトオブジェクトは、ハンドラも属性もどこからでもアクセスできる。でんでん。

GoF のデザインパターンは、オブジェクト(クラス)を作る人、それを使ってなんらかの処理をする人、それぞれの立場を考えたものになっています。ほとんどのデザインパターンは、使う人が楽に利用できるようになっています。そのために様々な工夫が施してあります。作る人、使う人の視点に立って見てみるとより納得できます。AppleScript では作る人も使う人も概ね同じ人ですが、将来の自分は赤の他人。作って楽をできるなら楽をしたいものですね。

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

掲示板。そう、掲示板です。なんだか、いたずらされていますね。対処をしたいとは思うのですが、この掲示板はプロバイダが提供しているものなので対処のしようがなかったり。まぁ、基本的にはほっておきます。あまりにも続くようなら、こういう輩を相手に時間を潰すのもなんですので、あっさりと掲示板を閉じます。手軽ではないですが、なにかあればメールの方でご連絡を。GMail の blackcharan さん宛に。今後、メールも GMail に統一させようと思っています。PHP の勉強がてらに掲示板を作るという手もあるな。そうすれば、嫌いな人...もとい、嫌がらせをする人のフィルタリングも思いのまま。

以上、業務連絡でした。

Database Events は終わって、デザインパターンに戻ります。デザインパターンの話って今年中に終わるのかしらん。今回は、Template Method を取り上げます。。デザインパターンの中では比較的単純なパターンだそうです。

Template Method は、『決まった処理手順をテンプレートにし、処理手順の一部だけを置き換えやすくする』デザインパターンです。既存のハンドラを組み合わせて定型的な処理を行う AppleScript にはある意味お似合いのデザインパターンかな。

例えば、Finder で選択している項目に対してなんらかの処理を施したいというスクリプト。画像ファイルを回転したい。画像ファイルを縮小したい。画像ファイルのフォーマットを変更したい...。以下のような感じでしょうか?

Script Editor で開く

property extensionList : {"jpg", "jpeg", "tif", "tiff", "png"}

tell application "Finder"
    set curSelection to selection
    if curSelection is {} then return

    set imageFiles to {}
    repeat with thisItem in curSelection
        if (class of thisItem) is document file and (name extension of thisItem) is in extensionList then
            set end of imageFiles to thisItem as alias
        end if
    end repeat

    my shrinkingImages(imageFiles, 0.8)
    beep 2
end tell

on shrinkingImages(imageFiles, percentage)
    tell application "Image Events"
        launch
        repeat with thisImage in imageFiles
            try
                set imageRef to open thisImage
                scale imageRef by factor percentage
                save imageRef
                close imageRef
            on error
                try
                    close imageRef
                end try
            end try
        end repeat
    end tell
end shrinkingImages

Finder で選択している画像に対して縮小、拡大を行います。上書き保存するので大事な画像に使ってはいけません。では、画像をトリミングするスクリプトを。

Script Editor で開く

property extensionList : {"jpg", "jpeg", "tif", "tiff", "png"}

tell application "Finder"
    set curSelection to selection
    if curSelection is {} then return

    set imageFiles to {}
    repeat with thisItem in curSelection
        if (class of thisItem) is document file and (name extension of thisItem) is in extensionList then
            set end of imageFiles to thisItem as alias
        end if
    end repeat

    my rotateImages(imageFiles, -90) -- *
    beep 2
end tell

on rotateImages(imageFiles, degree)
    tell application "Image Events"
        launch
        repeat with thisImage in imageFiles
            try
                set imageRef to open thisImage
                rotate imageRef to angle degree
                save imageRef
                close imageRef
            on error
                try
                    close imageRef
                end try
            end try
        end repeat
    end tell
end rotateImages

画像を処理するハンドラが変更されました。Finder で処理をしている部分は同じものを使い回しています。実際はもう少し使いやすいように処理した画像を別名で保存したりするのですが、今回はデザインパターンを理解するためのものなので手を抜いています。

AppleScript は、上記のような決まった手順の処理というのが多いです。処理内容は同じで対象アプリケーションが変わっただけとか。こういうときに Template Method パターンを使えばしあわせになれるかもしれません。Template Method パターンでは、まず処理手順のテンプレートを作ります。処理手順のテンプレートというのは、ハンドラ呼び出しの組み合わせです。テンプレートは抽象クラス(AbstractClass。AppleScript にはクラスの概念がないですが、抽象オブジェクトというのもなんなので...)で作り、処理手順の一部を定義します。処理手順の一部というのはハンドラですね。

Template Method では処理のテンプレートは、親オブジェクトで定義し、変更されません。子オブジェクトで変更するのは、個々の処理だけです。処理手順は全ての子オブジェクトで使い回されます。テンプレートたる所以ですね。

上記のスクリプトでは、ファイルの選別とファイルの加工という処理があり、選別、加工と処理手順の流れがあります。

Script Editor で開く

script FileProcessor
    property extensionList : {}

    on selectItems()
        tell application "Finder"
            set curSelection to selection
            if curSelection is {} then return

            set theList to {}
            repeat with thisItem in curSelection
                if (class of thisItem) is document file and (name extension of thisItem) is in extensionList then
                    set end of theList to thisItem as alias
                end if
            end repeat
            return theList
        end tell
    end selectItems

    on processItems(theseItems)
    end processItems

    on run
        set theseItems to my selectItems()
        my processItems(theseItems)
    end run
end script

このような抽象的なオブジェクトを作ります。run ハンドラを templateMethod にしています。templateMethod では、自身で定義されている個別の処理を順番に呼び出します。selectItems() と processItems() が個別の処理になります。前者がファイルの選別を行い、後者で加工を行います。これらの個別の処理はこのスクリプトオブジェクトを継承したスクリプトオブジェクトで実装します。そうすることによって処理の一部だけを変更することが可能になります。

selectItems() ハンドラは既に実装しています。こういう変わらない処理なら、抽象クラスの方で実装しておく方がより楽になります。気に入らなければ継承先で上書きすればいいだけなので。以下のようにこのスクリプトオブジェクトを継承して使います。

Script Editor で開く

script ShrinkingImagesProcessor
    property parent : FileProcessor
    property extensionList : {"jpg", "jpeg", "tif", "tiff", "png"}
    property percentage : 0.5

    on processItems(theseItems)
        if theseItems is {} then return

        tell application "Image Events"
            launch
            repeat with thisImage in theseItems
                try
                    set imageRef to open thisImage
                    scale imageRef by factor percentage of me
                    save imageRef
                    close imageRef
                on error
                    try
                        close imageRef
                    end try
                end try
            end repeat
        end tell
    end processItems

    on setPercentage(num)
        set percentage of me to num
    end setPercentage

    on getPercentage()
        return percentage of me
    end getPercentage
end script

tell ShrinkingImagesProcessor to run

画像の回転も同じように継承を行い変更したい処理手順を上書きするだけです。

Script Editor で開く

script RotateImagesProcessor
    property parent : FileProcessor
    property extensionList : {"jpg", "jpeg", "tif", "tiff", "png"}
    property degree : 90

    on processItems(theseItems)
        if theseItems is {} then return

        tell application "Image Events"
            launch
            repeat with thisImage in theseItems
                try
                    set imageRef to open thisImage
                    rotate imageRef to angle degree of me
                    save imageRef
                    close imageRef
                on error
                    try
                        close imageRef
                    end try
                end try
            end repeat
        end tell
    end processItems

    on setDegree(num)
        set degree of me to num
    end setDegree

    on getDegree()
        return degree of me
    end getDegree
end script

tell RotateImagesProcessor to run

Template Method パターンを用いれば、既にある処理は使い回すことができます。また、各手順は個別に作成しているので修正も容易になります。以前に『どうせなら』という記事で書いた Livedoor Reader のログインスクリプトなどに Template Method を適用すれば、mixi や Gmail などのログインスクリプトも差分だけを書くことで流用することができます。

もちろん、問題がないわけでもありません。Template Method の問題点は、継承の継承を行っていくことでどのオブジェクトのどのハンドラが呼び出されているかが分かりにくくなるということです。以下のような感じです。

Script Editor で開く

script FileProcessor
    property extensionList : {}

    on selectItems()
        tell application "Finder"
            set curSelection to selection
            if curSelection is {} then return

            set theList to {}
            repeat with thisItem in curSelection
                if (class of thisItem) is document file and (name extension of thisItem) is in extensionList of me then
                    set end of theList to thisItem as alias
                end if
            end repeat
            return theList
        end tell
    end selectItems

    on processItems(theseItems)
    end processItems

    on run
        set theseItems to my selectItems()
        my processItems(theseItems)
    end run
end script

script ScriptFileProcessor
    property parent : FileProcessor
    property extensionList : {"scpt", "scptd", "applescript"}

    on processItems(theseItems)
        repeat with thisItem in theseItems
            tell application "Finder" to open thisItem
        end repeat
    end processItems
end script

script JPEGFileProcessor
    property parent : FileProcessor
    property extensionList : {"jpg", "jpeg"}

    on processItems(theseItems)
        if theseItems is {} then return

        tell application "Image Events"
            launch
            repeat with thisImage in theseItems
                try
                    set imageRef to open thisImage
                    rotate imageRef to angle 90
                    save imageRef
                    close imageRef
                on error
                    try
                        close imageRef
                    end try
                end try
            end repeat
        end tell
    end processItems
end script

script TIFFFileProcessor
    property parent : JPEGFileProcessor
    property extensionList : {"tif", "tiff"}
end script

tell TIFFFileProcessor to run

最後の TIFFFileProcessor が FileProcessor を継承している JPEGFileProcessor を継承しています。ここでは個別の処理をそれぞれで定義していませんが、こういう継承を行っているとどのオブジェクトのどのハンドラを呼び出しているのかということが分かりにくくなってきます。バグが見つけにくい、と。あまり深い継承は行わない方がいいようです。

Template Method パターンは、Factory / Factory Method パターンと深い関係があるそうで。オブジェクトの生成処理に Template Method パターンを適用することができますし、Template Method パターンで作ったオブジェクトのどれを利用するかを Factory を使って動的に変更することが可能になります。

デザインパターンは一つだけでなく複数を組み合わせることでさらに威力を発揮するということですね。

Database Events (7)

Database Events (6) の続きです。前回、最初の方でスクリプトメニューから実行すると処理速度は速い、と書きました。これは、Database Events に限った話ではなく、どのアプリケーションのスクリプトでも速くなります(Carbon、Cocoa 両方ともかどうかまでは調べていませんが)。

ところで...System Events の用語説明には表示されない Hidden Suite という Suite をご存知でしょうか?

この Suite では、9 個の命令が定義されています。cancel、confirm、decrement、do action、do script、increment、key down、key up、pick の 9 個です。これらの使い方を詳らかにするのが目的ではないので端折りますが、多くは UI Element の操作を行うための命令です。do action と do script は(System Events 1.0 の頃から使っている人は知っていると思いますが)、フォルダアクションを実行する命令と OSA Script を実行する命令です。今回、取り上げるのは do script 命令。この命令は、run script 命令と同じように引数で指定したスクリプトファイルを実行するための命令です。

おそらく...スクリプトメニューは、この命令を使っているのではないかと思います(ただの推測なのですが)。スクリプトメニューは、Cocoa/Objective-C なのでしょうが、その中で NSAppleScript を使って System Events の do script を実行しているのではないかと思います。以下のスクリプトを実行すると System Events が返ってくることから考えてみても。

Script Editor で開く

on run
    tell application "System Events"
        set f to name of processes whose frontmost of it is true
    end tell

    display dialog (f as text)
end run

スクリプトメニューは、SystemUIServer が動かしているのですが。

つまり、スクリプトを do script 命令で動かせば処理速度は速くなる、ということが言えると思います。では、実験。

まず、実験した環境を。iMac G5(2 GHz Power PC G5)でメモリは 1 GB。HDD は、400 GB。OS は、最新。Mac OS X 10.4.8 です。AppleScript 1.10.7。Script Editor 2.1.1。

以下のスクリプトをコンパイル済みスクリプトとして保存します。

Script Editor で開く

on run
    set ex to ".dbev"
    set theName to "tmp"
    set dir to path to desktop as Unicode text
    set dbFile to dir & theName & ex

    tell application "Database Events"
        if exists database theName then
            set db to database theName
        else
            if exists database (POSIX path of dbFile) then
                set db to database (POSIX path of dbFile)
            else
                set db to make new database with properties {name:theName, location:dir}
                save db
            end if
        end if

        set cd to current date
        tell db
            repeat with i from 1 to 500
                make new record with properties {name:""}
            end repeat
            save
        end tell
        set num to (current date) - cd

        delete db
        quit
    end tell

    return num
end run

単純にデータベースに record を 500 個新規作成するだけのものです。これを Script Editor 上で実行すると、約 27 秒。次に、スクリプトメニューから実行します。このとき、約 10 秒。全く処理速度が違います。そして、do script 命令。

データベースに record を追加するスクリプトを do script 命令で 50 回呼び出して平均値をとってみました(データベスは、毎回削除して新規に作りながら)。結果、9.52 秒。スクリプトメニューの結果と同じです。

ついでにと思い、run script 命令も試してみます。これも 50 回呼び出して平均値をとってみました。結果は、6.12 秒。do script 命令に比べ、約 3 秒近く早くなっています。驚くべき事実。何がお前をそこまで変えてしまったんだ、と問わずにはいられません。

というか、AppleScript の処理速度って上がっていますよね。ただ、Script Editor で開発、デバックを行っていると気づかないし、むしろ、遅い。AppleScript Studio でアプリケーションにしたら速度が上がるなと思っていましたが、ようは Script Editor が悪かったんですね。これって既知のこと?うわっ...時代に取り残されていたよ。

これなら、積極的に Database Events を使ってみようという気になるもの。また、Script Editor でスクリプトを作っていて処理速度が遅いな、と思ったとき一度スクリプトメニューから実行してみるのもいいかも。ただ、デバックがより面倒になるよな...。

Database Events (6)

久しぶりに Database Events。Database Events は、一連の記事(Database Events (1)Database Events (2)Database Events (3)Database Events (4)Database Events (5))を書いて以来、いつか使えるといいな...といった感じで暇なときに時々いじっていたりしました。どうにもパフォーマンスが悪いし...きっと使い方が悪いのかもしれないと思い。

ところで...。iTunes 関連のスクリプトで有名な Doug's AppleScripts for iTunes に Most Played Artists というスクリプトがあります。これ、試してみたことあるでしょうか?

iTunes でのアーティストごとの再生回数をランキング形式でファイルに書き出してくれるものですが、Database Events を使っています。このスクリプトを開いてみると最初にコメントが書かれていますが、ここに 5154 曲の再生回数を集計するのに 6 分 18 秒とあります。Dual 1.8GHz G5 で。

Database Events は、どうにも遅かったような...と思い、はたと気づく。スクリプトをいったんアプリケーション形式で保存して実行してみます。2232 曲で 4 分。スクリプトのままスクリプトメニューから実行してみる。2232 曲で 1 分 50 秒...。これ、使えるじゃん。Script Editor で実行していたから遅かったのか。迂闊だった...。

試してみると、他のアプリケーションでも同じでした。スクリプトメニューから実行すると処理が早くなる。Script Editor から試すと遅い。開発環境になんて不向きなんでしょう。まさか、これって既知のこと?うわっ、時代に取り残されていたよ...。

そんなわけで急遽 Database Events に取り組む。Database Events が使えるなら、なんだか可能性が増える感じがするなー、と思う。根拠もアイデアもないけど。

DBMS を使うとき、最初にデータベースの構造を設計すると思います。後で変更できないからですね。が、Database Events では、変更し放題です。record には、field いくつでも作ることができます。また、最初の record と 次の record が同じ field を持たないといけないということもありません。record と field の name 属性は、同じものが複数あってもかまいません。database を必ずファイルに保存しないといけない、ということもありません。処理のために一時的なデータ置き場として使うことが可能です。使い捨てとして。Doug's AppleScripts for iTunes の Most Played Artists は、まさにそういった使い方をしています。

Database Events であるデータベースファイルを開くと、他のスクリプトからでもそのデータベースを使うことができ(共有される)、また、閉じることも可能です。たとえ、処理中であっても。これを防ぐ方法はありません。with transaction が解決法として思い浮かびますが、Database Events は対応していません。Database Events は、終了するときにすべてのデータベースを閉じます。これも、他のスクリプトで何らかの処理を行っていても、否応無しにデータベースは閉じられます。System Events に追加された一連のトランザクション関連の命令(abort transaction、begin transaction、end transaction)が解決方法を提供してくれるのかもしれませんが、使い方は Web 上では発見できず。同じ疑問を書いていた人はいたけど。これは、探し方が悪いのかもしれませんが。

database は、write 命令のファイル書き出しのようにデータベースファイルを開き、閉じることができます。対応するのは、open 命令と close 命令。が、close 命令は不安定で時々エラーになります。確実に閉じることができるのは delete 命令。delete 命令は、record と field の削除も行えます。open 命令でファイルをデータベースファイルを開くことができますが、POSIX path を指定する必要があります。make 命令で database を作るとき、location 属性に指定するのは、POSIX file の方です。location 属性以外は、すべて POSIX path で指定します。

exists 命令では開かれているデータベースが存在するかどうかを確認することができるとともに、POSIX path を指定することでデータベースファイルが存在するかどうかも確認することができます。

ある一つのデータベースを複数のスクリプトから変更すると、後から来た方の命令でデータは上書きされます。このとき、トランザクションを行っていなくてもデータの不整合がおこることはありません。どうも、内部的に適切に処理を行っているような感じがします。ただ、処理中に他のスクリプトからデータベースを閉じられる可能性がある、ということに留意しておく必要はあります。このときはもちろんエラーになり、データは正しく保存されません(もしくは、データベースが壊れる)。

open 命令は、既存のデータベースを開くことができますが、open するたびに Database Events で開かれているデータベースの数が増えていきます。これは、make 命令も同じです。

と、まあいろいろと書きましたが、実際のスクリプトで見ていきましょう。まず、デスクトップに PREF.dbev というデータベースがあります。open 命令で開くには、以下のようにします。

Script Editor で開く

set theFile to "PREF.dbev"
set dir to path to desktop as Unicode text
set dbPath to dir & theFile

tell application "Database Events"
    set db to open database (POSIX path of dbPath)
    databases
    --> {database "PREF" of application "Database Events", database "PREF" of application "Database Events"}
end tell

データベースファイルの POSIX path を指定し、かつ、database とクラスを指定します。これで開くことができるのですが、結果はなぜか同じものが二つ返ってきます。実のところ...というほどでもないのですが、データベースファイルを開くには以下のようにするだけでいいのでした。

Script Editor で開く

set theFile to "PREF.dbev"
set dir to path to desktop as Unicode text
set dbPath to dir & theFile

tell application "Database Events"
    set db to database (POSIX path of dbPath)
    databases
    --> {database "PREF" of application "Database Events"}
end tell

database (POSIX path) とするだけでデータベースが開かれます。その結果に open 命令を行っているので同じものが複数返ってくるのです。以下のような感じですね。

Script Editor で開く

set theFile to "PREF.dbev"
set dir to path to desktop as Unicode text
set dbPath to dir & theFile

tell application "Database Events"
    set db to database (POSIX path of dbPath)
    set dbCopy to open db
    databases
    --> {database "PREF" of application "Database Events", database "PREF" of application "Database Events"}
end tell

open 命令は、既に開いているデータベースを再度開き、同じものを結果として返します。複製を作っているような感じなのですが...何度も繰り返して open を行うと、使わないにもかかわらず開かれているデータベースの数は増えていきます。上記のスクリプトを何回か繰り返して実行してみると分かると思います。これは、make 命令も同じです。

Script Editor で開く

set theFile to "PREF.dbev"
set dir to path to desktop as Unicode text
set dbPath to dir & theFile

tell application "Database Events"
    make new database with properties {name:theFile, location:dbPath}
    databases
end tell

このスクリプトを何回か繰り返してもエラーにはなりません。ただ、開かれているデータベースの数が増えるだけです。不必要にも関わらず、同じデータベースが複数開かれているということもありえるので注意が必要です(open 命令ではなく、データベースファイルのパスを指定する方では、数は増えません)。

データベースを閉じるのが close 命令です。

Script Editor で開く

set theFile to "PREF.dbev"
set dir to path to desktop as Unicode text
set dbPath to dir & theFile

tell application "Database Events"
    set db to database (POSIX path of dbPath)
    set dbCopy to open db
    close db
    databases
    --> {database "PREF" of application "Database Events"}
end tell

また、delete 命令でも閉じることができます。

Script Editor で開く

set theFile to "PREF.dbev"
set dir to path to desktop as Unicode text
set dbPath to dir & theFile

tell application "Database Events"
    set db to database (POSIX path of dbPath)
    set dbCopy to open db
    close db
    delete dbCopy
    databases
    --> {}
end tell

ただ、close 命令は(何が原因か分かりませんが)エラーになることがあります。

exists で database(もしくは、record や field)の存在を確認することができます。

Script Editor で開く

set theFile to "PREF.dbev"
set dir to path to desktop as Unicode text
set dbPath to dir & theFile

tell application "Database Events"
    set db to database (POSIX path of dbPath)
    exists database "PREF"
    --> true
end tell

exists にパスを指定することでデータベースファイルが存在するかどうかの確認もできます。

Script Editor で開く

set theFile to "PREF.dbev"
set dir to path to desktop as Unicode text
set dbPath to dir & theFile

tell application "Database Events"
    exists database (POSIX path of dbPath)
    --> true
    databases
    --> {database "PREF" of application "Database Events"}
end tell

存在すれば真を返し、データベースは開かれます。存在しないと偽を返し、エラーにはなりません。データベースファイルがなければ作成し、あればそれを開くという処理は open 命令を使うよりも exists 命令を使う方がいいでしょう。

ただ、Database Events はどんなファイルであってもデータベースとして開いてしまうようで、例えば、テキストファイルを指定してもデータベースとして開いてしまいます。以下のスクリプトは、スクリプトファイルを指定した例です。

Script Editor で開く

set theFile to "Database.scpt"
set dir to path to desktop as Unicode text
set dbPath to dir & theFile

tell application "Database Events"
    exists database (POSIX path of dbPath)
    --> true
    databases
    --> {database "Database" of application "Database Events"}
end tell

開けるからといって使えるわけではありません。この辺りの注意はスクリプトを作る人の責任になる...のでしょうね。

Database Events は、起動しているあいだに開かれたデータベースをすべて知っています。

Script Editor で開く

set theFile to "PREF.dbev"
set dir to path to desktop as Unicode text
set dbPath to dir & theFile

tell application "Database Events"
    exists database (POSIX path of dbPath)
    databases
    --> {database "PREF" of application "Database Events"}
end tell

このスクリプトを実行します。その後、新規ドキュメントを作って以下のスクリプトを実行します。

Script Editor で開く

tell application "Database Events"
    delete databases
    databases
    --> {}
end tell

このように他のスクリプトから閉じることができます。もちろん、操作もできます。が、処理中のデータベースがあるかどうかを調べることはできません。Database Events は、終了時にすべてのデータベースを自動的に閉じます。そのために quit delay という属性で Database Events が起動してから何秒後に終了するかを指定することができます。ただ、データベースを修正したかどうか、セーブする必要があるかどうかといったことを調べる方法は Database Events には用意されていません。しかし、修正されていた場合、データベースを閉じるときに Database Events が保存を行います(データベースがファイルとして既に保存されているなら、です)。Database Events が終了するときではありませんので注意を。

他のスクリプトからでも開かれているデータベースにアクセスできますが、操作の競合やデータの不整合が起こることはないようです。処理は順番待ちで実行されます。小難しい処理は Database Events が責任を持って行っている、という感じでしょうか。データベースの操作にそれほど神経質にならなくてもいいのかもしれません(あくまで、主観。もしかしたら、全然違うかもしれません)。

最後に一つだけ書きたいことがあるのですが...さすがに、これ以上書くと長くなるので次回に続く...ということで。