構造に関するデザインパターンを取り上げていませんでしたね。そんなわけで、Decorator パターン。
『オブジェクト指向における再利用のためのデザインパターン』は、オブジェクトの「生成」に関するパターン、オブジェクトの「構造」に関するパターン、オブジェクトの「振る舞い」に関するパターンに分類されています。それぞれ、「オブジェクト生成の工夫」、「オブジェクトの組み合わせの工夫」、「オブジェクトの動作に関する工夫」と言い換えることができます。 「そんなことわざわざデザインパターンとして教えてもらわなくても前から実行しているよ」というようなものからなるほどというものまでさまざま。
Observer パターンは「振る舞い」、Factory Method パターンは「生成」、Iterator パターンは「振る舞い」、Template Method パターンは「振る舞い」、と。
Decorator パターンは、オブジェクトの構造に関するデザインパターンです。どんな工夫なのか?
例えば、既にあるオブジェクトの動作を拡張したい時。通常は、継承を用います。以下のような感じ。
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 パターンは、以下のようになります。
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 を拡張できるということです。
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 で選択している項目を返すスクリプト。
on FinderEx()
script FinderEx
on getSelection()
tell application "Finder" to selection
end getSelection
end script
end FinderEx
これに Decorator を追加します。Finder の selection は、返り値の順番がまちまちです(並んでいるのですが直感的ではないので)。これを順番に並んだ項目で返すようにしましょう。Finder の sort 命令を使います。
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 のオブジェクトです。他のアプリケーションに渡すには型をキャストしないといけません。そういう機能も追加してみましょう。
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 参照で返して欲しいという時は?既に機能はあるのですから、後は組み合わせるだけです。
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 では関係ない話なのですが、微妙に関係する部分もあるので少し触れておきます。例えば、最初のサンプルを以下のように変えたとします。
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 では作る人も使う人も概ね同じ人ですが、将来の自分は赤の他人。作って楽をできるなら楽をしたいものですね。