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

SwiftでFactory Methodパターン

Swift

なつやすみだ! ひゃっはー∩( ・ω・)∩ ※ふなっしーっぽく

こんにちは。たーせるです。

最近、Xcode 6 が beta 5 にバージョンアップされました。それに伴い、Swift もだいぶバージョンアップされたようです。今日は新しくなった Swift さんとふたたび戯れてみようと思います。

Swift で Factory Method

Factory Methodは、特定クラスのインスタンス化を Factory クラスに一任するテクニックです。

通常、インスタンス化は、コンストラクタ(言語によってはイニシャライザ)を用いるのが一般的です。ふつうはそれで充分です。よほどの事がない限り、困ることは滅多にありません。

しかし、単体テストのときは本番用の API クラスをモックに切り替えたい場合があります。

本番用 API クラスとモックを切り替える際、基本的にAPI クラスを利用する側のコードは影響を受けないように設計すべきです単体テストのたびに、テスト対象のコードを書き換える行為は危険を伴うからです。

クラス名をハードコードすると、生成するインスタンスを実行時に切り替えることができなくなってしまいます。

この問題を解決するのが Factory Method パターンです。ステップバイステップで見ていきましょう。

まずは普通の手法

こんなクラスを作ります。簡単ですね。

public class Tercel {
    private let CLASS_NAME = "たーせる"
    private var name : String
    
    // イニシャライザ
    public init(name : String) {
        self.name = name
    }
    
    // てきとうなメソッド
    public func sayHello() {
        println("[\(CLASS_NAME)] printlf(\"Hello, \(name)\"\\n);")
    }
}

でもって、上記 Tercel クラスを以下のように使うとします。

普通にインスタンス化しています。

private let NAME: String = "たーちん"

// ここでクラス名を明示してインスタンス化を行っている!!
private var tercels: [Tercel] = [ Tercel(name: "\(NAME) 1号"),
                                  Tercel(name: "\(NAME) 2号"),
                                  Tercel(name: "\(NAME) 3号")]

for tercel in tercels { tercel.sayHello() }

実行するとこうなります。

[たーせる] printlf("Hello, たーちん 1号"\n);
[たーせる] printlf("Hello, たーちん 2号"\n);
[たーせる] printlf("Hello, たーちん 3号"\n);

ところがテストの段階になって、Tercel クラスをすべて以下のクラスに切り替える必要が出てきました*1。入力値に限らず、固定のダミーメッセージを出力するやる気のないクラスです。

public class MockTercel {
    // 心底てきとうなイニシャライザ
    public init(name: String) { }
    
    // 心底てきとうなメソッド
    public func sayHello() {
        println("チキンタツタ美味しい!一番好きなバーガーです!")
    }
}

クラスの切り替え作業には、通常、たくさんの修正が必要です。

private let NAME: String = "たーちん"

private var tercels: [MockTercel] = [ MockTercel(name: "\(NAME) 1号"),  // ← 修正
                                      MockTercel(name: "\(NAME) 2号"),  // ← 修正
                                      MockTercel(name: "\(NAME) 3号")]  // ← 修正

for tercel in tercels { tercel.sayHello() }

テストのたびに、いちいちこんな修正をするのはあまりよい方法ではありません。

Factory Method パターンの適用

はじめからこうすればよかった篇

はじめに API 側のインタフェースを決めるため、プロトコルを作ります。

public protocol Tercel {
    init(name: String)
    func sayHello()
}
public protocol Factory {
    func createInstance(name: String) -> Tercel
}

上記の Factory は、任意の Tercel オブジェクトをインスタンス化する役割を持っています。生成するインスタンスは、TercelFactory のサブクラス内に隠蔽されるため、API を利用する側は常にファクトリにインスタンスを作ってもらう形になります。

private let NAME: String = "たーちん"

let factory : Factory = TercelFactory()

private var tercels: [Tercel] = [ factory.createInstance("\(NAME) 1号"),
                                  factory.createInstance("\(NAME) 2号"),
                                  factory.createInstance("\(NAME) 3号")]

for tercel in tercels { tercel.sayHello() }

インスタンスの生成をファクトリが担うようになり、クラス名がハードコードされなくなったことにより、API を利用する側は生成されるインスタンスを意識する必要がなくなりました

一方、API 側、すなわち切り替えられるクラス側は、 Tercel プロトコルを採用して以下のようになります。

// 本チャン用
public class ConcreteTercel : Tercel {
    private let CLASS_NAME = "たーせる"
    private var name: String
    
    public required init(name: String) {
        self.name = name
    }
    
    public func sayHello() {
        println("[\(CLASS_NAME)] printlf(\"Hello, \(name)\"\\n);")
    }
}
// 代替用
public class MockTercel : Tercel {
    public required init(name: String) { }
    public func sayHello() {
        println("チキンタツタ美味しい!一番好きなバーガーです!")
    }
}

また、Factory プロトコルを採用したクラスは以下のようになります。生成するインスタンスを決定づける処理は、TercelFactory の中に集約されます。

public class TercelFactory : Factory {
    public func createInstance(name: String) -> Tercel {
        return ConcreteTercel(name: name)
    }
}

もし本番クラス(ConcreteTercel)からテスト用クラス(MockTercel)に切り替えたいときは、TercelFactory のコードの一箇所だけを修正すればよいことになります。

しつこいようですが、Tercel オブジェクトを使う側は、切り替えを一切意識する必要はありません。

おまけ

たとえば、テストモードかどうかを判断する制御フラグなんぞを Singleton に持たせておき、テストの前処理部でフラグを立てるようコーディングすると、生成されるインスタンスを一度にモックに切り替えることができて便利です。

// テストモードかどうかの制御フラグ
struct AppUtil {
    static var isTestMode : Bool = false
}

public protocol Factory {
    func createInstance(name: String) -> Tercel
}

public class TercelFactory : Factory {
    public func createInstance(name: String) -> Tercel {
        return AppUtil.isTestMode ?
            MockTercel(name: name) : ConcreteTercel(name: name)
    }
}

さいごに

本当は Abstract Factory の話もしたかったのですが、なんか予想外に長くなったのでこのへんで一旦切ります。

おやすみなさい。よいゆめを。

*1:データベースまわりの処理など外接を行うクラスは、単体テストの段階ではダミーの振る舞いが仕組まれた「モック」にすり替えることが多いです。

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