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 パターンとは関係のない話になってしまいましたが。では、また。

0 件のコメント :

コメントを投稿