読者です 読者をやめる 読者になる 読者になる

SwiftでVisitorパターン

Swift

導入

昔、ダブルディスパッチのところで似たような話をした気もするけど……。

時と場合によっては多態性がうまく機能しないことがある、という例。

import Foundation

// プロトコル P
@objc public protocol P { }

// プロトコルPを採用したクラス A, B, C
public class A : P { }
public class B : P { }
public class C : P { }

// P, A, B, C それぞれを引数に取る関数
func hoge(arg: P) { println("P") }
func hoge(arg: A) { println("A") }
func hoge(arg: B) { println("B") }
func hoge(arg: C) { println("C") }

// てきとうな配列。要素には A, B, C のインスタンスが入ってる
var array: [P] = [A(), B(), C(), B(), A()]

for element in array {
    hoge(element)
}

// [結果]
//     P
//     P
//     P
//     P
//     P

上記のコードに示した通り、配列の要素はインスタンス化された A, B, C のいずれかである。にも関わらず、なぜか実行結果はすべて P が出力されてしまっている。

これを意図した通りに動くよう修正する最も簡単な方法は、hoge() 関数を以下のように修正すればよい。しかしながら型チェックの濫用とif文の連鎖はちょっとダサい

// P, A, B, C それぞれを引数に取る関数
/*
func hoge(arg: P) { println("P") }
func hoge(arg: A) { println("A") }
func hoge(arg: B) { println("B") }
func hoge(arg: C) { println("C") }
*/

// 超イケてない。
func hoge(arg: P) {
    if arg is A {
        println("A")
    } else if arg is B {
        println("B")
    } else if arg is C {
        println("C")
    } else {
        println("P")
    }
}

// [結果]
//     A
//     B
//     C
//     B
//     A

Visitorパターン登場

ではどうすればよいだろうか。

まずは以下のように、各 hoge() を Visitor という一つのクラスで包んでやる。そして P, A, B, C に accept() 関数を足し、各々のクラスから Visitor の hoge() を呼んでやるのだ。

// ----------------------------------------
// プロトコル P
// ※ なんかメソッドが増えてる
public protocol P {
    func accept(visitor: Visitor)
}

// ----------------------------------------
// プロトコルPを採用したクラス A, B, C
public class A : P {
    public func accept(visitor: Visitor) { visitor.hoge(self) }
}
public class B : P {
    public func accept(visitor: Visitor) { visitor.hoge(self) }
}
public class C : P {
    public func accept(visitor: Visitor) { visitor.hoge(self) }
}

// ----------------------------------------
// なんか新しいクラス Visitor
public class Visitor {
    // P, A, B, C それぞれを引数に取る関数
    func hoge(arg: P) { println("P") } // ←要るのか?
    func hoge(arg: A) { println("A") }
    func hoge(arg: B) { println("B") }
    func hoge(arg: C) { println("C") }
}

// ----------------------------------------
// てきとうな配列。要素には A, B, C のインスタンスが入ってる
var array: [P] = [A(), B(), C(), B(), A()]
var visitor = Visitor()

for element in array {
    // elementにvisitorを渡す
    element.accept(visitor)
}

// ----------------------------------------
// [結果]
//     A
//     B
//     C
//     B
//     A

こうすると、めでたく多態性によって処理が振り分けられる。これが Visitor パターン。

拡張

ちなみに、先程の Visitor クラスを継承して、Visitor1 を作ったとする(コード前半は略)。

すると、元のクラス P, A, B, C には一切手を加えていないにも関わらず、配列走査時の処理を拡張することができる。これは一応 Visitor パターンの強みと言われている。

// ----------------------------------------
// Visitor (さっきと同じ)
public class Visitor {
    public func hoge(arg: P) { println("P") }
    public func hoge(arg: A) { println("A") }
    public func hoge(arg: B) { println("B") }
    public func hoge(arg: C) { println("C") }
}

// Visitorを継承して作ったクラス ←New!!
public class Visitor1: Visitor{
    public override func hoge(arg: A) { println("あ") }
    public override func hoge(arg: B) { println("い") }
    public override func hoge(arg: C) { println("う") }
}

// ----------------------------------------
var array: [P] = [A(), B(), C(), B(), A()]
var visitor = Visitor1()    // ← New!!

for element in array {
    element.accept(visitor)
}

// ----------------------------------------
// [結果]
//     あ
//     い
//     う
//     い
//     あ

これは、要素クラス A, B, C については、ひとまず accept() を組み込んでさえおけば、その後一切影響を与えずに新たな処理を追加することができることを意味している。

あとデータ構造とアルゴリズムの分離とかいろいろ尤もらしい利点もあるけど面倒なので省略。

余談

ここからは余談。

各クラスに処理を持たせる

ダブルディスパッチなどという大げさなものを使わなくとも、先程の hoge() 相当の機能を各クラスに負わせればよいのではなかろうか。これは一見悪くなさそうに見える。

// ----------------------------------------
// プロトコル P
public protocol P { func foo() }

// ----------------------------------------
// プロトコルPを採用したクラス A, B, C
public class A : P { public func foo() {println("A") } }
public class B : P { public func foo() {println("B") } }
public class C : P { public func foo() {println("C") } }

そして、走査する際には各要素の foo() を呼んでやればよい。

// ----------------------------------------
var array: [P] = [A(), B(), C(), B(), A()]

for element in array {
    element.foo()
}

// ----------------------------------------
// [結果]
//     A
//     B
//     C
//     B
//     A

確かにうまく動いている。

だが、後になってクラスごとに新たな振る舞いを追加する必要が生じた場合、この方法は破綻する。

以下のような foo2() を新たに追加し、foo() と foo2() を動的にスイッチすることを考えてみよう。

要するに上記の「拡張」の章の再現を想定している。

// ----------------------------------------
var array: [P] = [A(), B(), C(), B(), A()]

for element in array {
    element.foo2()
}

// ----------------------------------------
// [こういう結果にしたい]
//     あ
//     い
//     う
//     い
//     あ

恐らく、foo() のときと同じように、各クラスに foo2() を追加すればよいと考えるかも知れない。

しかし今後さらに foo3() 、foo4()、……と必要なメソッドが増えたとき、関係するクラス全てに修正が入ることになる。恐ろしく面倒だ。

そもそも A, B, C 個々のクラスに対して、集成型を走査するときにだけ使われるメソッドで埋め尽くされるのはどう考えてもまずい。

extensionは使えるか

そこで、既存のクラス A, B, C の汚染を防ぎつつ機能追加を考えてみる。

Swift の場合、extension を使って既存クラスに機能を追加する事が容易である。

// ----------------------------------------
// プロトコル P
public protocol P { }

// ----------------------------------------
// プロトコルPを採用したクラス A, B, C
public class A : P { }
public class B : P { }
public class C : P { }

// ----------------------------------------
// 後付けで処理を増やす
extension A { public func foo2() { println("あ") } }
extension B { public func foo2() { println("い") } }
extension C { public func foo2() { println("う") } }

この場合でも、正しく foo() を呼ぶためにはオブジェクトを適切にダウンキャストせねばならない。

安全にダウンキャストする為には型チェックが必須で、以下のようにいまひとつすっきりしないコードになってしまう。

// ----------------------------------------
var array: [P] = [A(), B(), C(), B(), A()]

for element in array {
    (element as? A)?.foo2()
    (element as? B)?.foo2()
    (element as? C)?.foo2()
}

型が違うだけで同名のメソッドを呼ぶ処理が三行も書かれているのは確かに汚いが、個人的にはギリギリ許容範囲だと思う。

身もふたもないけど、そもそもVisitor は使いどころがかなり限られる上に欠点も多いパターンだし、設計をがんばった割には後にあまり拡張されない事も多かったりする。

GoF のパターンの中で最難関と言われるだけあって[要出典]、僕もあまり活用できていない。

Copyright (c) 2012 @tercel_s, @iTercel, @pi_cro_s.