私はベイマックス。

演算子オーバーロードを利用して、(○-○)の評価結果を"私はベイマックス。あなたの心とカラダを守ります。"にしてみるテスト。

// Playground - noun: a place where people can play

import Cocoa

func - (left: String, right: String) -> String {
    return "私はベイマックス。あなたの心とカラダを守ります。"
}

// Swiftでは、識別子に黒丸記号「●」を使えない……
let= "こんにちは、たーせるさん"

(○-○)

Quickの手習い

前回までのあらすじ

今日やったこと

せっかくなので、Quickで使えるmatcherについて、いろいろ試してみました。

xUnitと違って、基本的にmatcherには動詞の原形が使われており、慣れるまではタイポしそうです。

等値判定 equal

    override func spec() {
        describe("等値のテスト") {
            it("2と3の和は5と等しい") {
                let value = 2 + 3
                expect(value).to(equal(5))
            }
            
            it("400と600の和は100とは異なる") {
                let value = 400 + 600
                expect(value).toNot(equal(100))
            }
        }
    }

ポインタ比較 beIdenticalTo

    override func spec() {
        describe("ポインタ比較のテスト") {
            class Hoge : NSObject { }

            it("二つのHogeは同じポインタ") {
                let obj: Hoge = Hoge()
                let obj2 = obj
                
                expect(obj).to(beIdenticalTo(obj2))
            }
            
            it("二つのHogeは異なるポインタ") {
                let obj: Hoge = Hoge()
                let obj2: Hoge = Hoge()
                
                expect(obj).toNot(beIdenticalTo(obj2))
            }
        }
    }

大小比較 beLessThan / beGreaterThan

    override func spec() {
        describe("大小比較のテスト") {
            
            describe("不等号で比較する場合"){
                it("99は100より小さい") {
                    expect(99).to(beLessThan(100))
                }
            
                it("100は100より小さくない") {
                    expect(100).toNot(beLessThan(100))
                }
            
                it("101は100より大きい") {
                    expect(101).to(beGreaterThan(100))
                }
            
                it("100は100より大きいくない") {
                    expect(100).toNot(beGreaterThan(100))
                }
            }
            
            describe("等号つき不等号で比較する場合") {
                it("100は100以下") {
                    expect(100).to(beLessThanOrEqualTo(100))
                }
                
                it("101は100以下ではない") {
                    expect(101).toNot(beLessThanOrEqualTo(100))
                }
                
                it("100は100以上") {
                    expect(100).to(beGreaterThanOrEqualTo(100))
                }
                
                it("99は100以上ではない") {
                    expect(99).toNot(beGreaterThanOrEqualTo(100))
                }
            }
        }
    }

誤差判定 beCloseTo

    override func spec() {
        describe("浮動小数点誤差のテスト") {
            it("1.0と0.99999の誤差が0.00001以内である") {
                expect(1.0).to(beCloseTo(0.99999, within: 0.00001))
            }
            it("1.0と1.000011の誤差が0.00001以内ではない") {
                expect(1.0).toNot(beCloseTo(1.000011, within: 0.00001))
            }
        }
    }

Is-aのテスト beAnInstanceOf / beAKindOf

    override func spec() {
        describe("Is-aのテスト") {
            it("文字列「HelloWorld」はNSStringのサブクラスである") {
                expect("HelloWorld").to(beAKindOf(NSString))
            }
            
            it("文字列「HelloWorld」はNSStringのインスタンスではない") {
                expect("HelloWorld").toNot(beAnInstanceOf(NSString))
            }
            
            it("文字列「HelloWorld」は_NSContiguousStringのインスタンスである") {
                expect("HelloWorld").to(beAnInstanceOf(_NSContiguousString))
            }
        }
    }

真理値 beTrue / beFalse

    override func spec() {
        describe("真理値のテスト") {
            it("TrueはTrueである") {
                expect(true).to(beTrue())
            }
            
            it("TrueはFalseではない") {
                expect(true).toNot(beFalse())
            }

            it("FalseはFalseである") {
                expect(false).to(beFalse())
            }
            
            it("FalseはTrueではない") {
                expect(false).toNot(beTrue())
            }
        }
    }

Nil判定 beNil

    override func spec() {
        describe("Nil判定") {
            it("NilはNilである") {
                let nilObj:NSObject? = nil
                expect(nilObj).to(beNil())
            }
        }
    }

空値判定 beEmpty

    override func spec() {
        describe("空値判定") {
            it("要素が1つも存在しない配列は空オブジェクトである") {
                let emptyObj:[String] = []
                
                expect(emptyObj).to(beEmpty())
            }
            
            it("要素が1つも存在しない辞書は空オブジェクトである") {
                let emptyObj: [String:Int] = [:]
                
                expect(emptyObj).to(beEmpty())
            }
            
            it("文字数0の文字列は空オブジェクトである") {
                let emptyString = ""
                
                expect(emptyString).to(beEmpty())
            }
        }
    }

前方一致 beginWith

    override func spec() {
        describe("前方一致判定") {
            it("配列「AAA, BBB, CCC」の最初の要素は「AAA」である") {
                let array:[String] = ["AAA", "BBB", "CCC"]
                
                expect(array).to(beginWith("AAA"))
            }
            
            it("文字列「HelloWorld」は「Hell」で始まる") {
                let helloString = "HelloWorld"
                
                expect(helloString).to(beginWith("Hell"))
            }
        }
    }

後方一致 endWith

    override func spec() {
        describe("後方一致判定") {
            it("配列は「AAA, BBB, CCC」の最後の要素は「CCC」である") {
                let array:[String] = ["AAA", "BBB", "CCC"]
                
                expect(array).to(endWith("CCC"))
            }
            
            it("文字列「HelloWorld」は「ld」で終わる") {
                let helloString = "HelloWorld"
                
                expect(helloString).to(endWith("ld"))
            }
        }
    }

含有判定 contain

    override func spec() {
        describe("含有判定") {
            it("文字列「イチゴミルク」は「ゴミ」を含む") {
                let ichigoMilk = "イチゴミルク"
                
                expect(ichigoMilk).to(contain("ゴミ"))
            }
            
            it("文字列「ポークソテー」は「クソ」を含む") {
                let porkSauté = "ポークソテー"
                
                expect(porkSauté).to(contain("クソ"))
            }
        }
    }

iOS界隈の新手のテスティングフレームワークQuickをCocoaPods的なもので導入して満足して死ぬまでの記録

これまでのあらすじ

ひょんなことがきっかけで『iOSアプリ テスト自動化入門』を読みました。今年の3月20日くらいに出版された比較的新しい本なのですが、その直後にSwiftの出現とか冷戦の影響とかのせいでユニットテストを取り巻く状況は急速に変化してしまったようです。

同書の中で、ユニットテストにおけるフレームワークとしてXCTestGHUnitKiwiの3つが紹介されていましたが、そんな最中(さなか)に新手のQuickが颯爽と登場まーた新しいものつくって…


Swift界隈で話題沸騰中のテストフレームワーク Quick とは? - Qiita


[Swift] Quick で始める振る舞いテスト入門 #1 インストール | Developers.IO

序文

ところで話は変わりますが、昨日は朔旦冬至(さくたんとうじ)といって新しいことを始めるのにぴったりの日だったそうです。

なんか1日遅れているような気がしますが、ときには新しいこともしたいですしQuickにちょっと触ってみます。ちょっと触るだけだから

ちなみに上記記事では「QuickはCocoaPodsからインストールできない」と書いてありますが、一応公式に「How to install Quick using beta CocoaPods」という情報がある通り、やってやれないことはないようです。

そんなわけで、今回はQuickをCocoaPods経由でプロジェクトに導入するまでの話を備忘録的に書いていきます。ところどころ躓きました。後半適当になります。

なお使用したXcodeのバージョンは6.1.1です。CocoaPodsのバージョンは0.35.0です。

プロジェクトの作成

まずXcodeを起動して、適当にプロジェクトを作ります。

今回はテスティングフレームワークを動かすことだけが目的で、べつにアプリを作りたいわけではないので、プロジェクトの種類はなんでもよいと思います。ちなみに僕はiOSのSingle View Applicationを選択し、プロジェクトを「Sample1223」と名付けました。

Bundlerの導入

せっかくですが、ここで一旦 Xcodeを終了させます

How to install Quick using beta CocoaPods」にとりあえずBundlerを入れろって書いてあったのでおとなしく入れます。

Bundlerってなんだろう。Rubyerの言うことは分からん


Ruby - ツールを使いたいだけの人のための bundler 入門 (例: vagrant + veewee) - Qiita

ターミナルを起動して、gem installコマンドを打ちます。

bash
$ sudo gem install bundler

あ、sudo*1で実行しないと下記のようなエラーが出てしまうことがあります。

ERROR:  While executing gem ... (Gem::FilePermissionError)
    You don't have write permissions for the /Library/Ruby/Gems/2.0.0 directory.

Gemfileの作成

続いて、.xcodeprojのあるディレクトリまでcdで移動し、Gemfileを作ります*2。以後、特に断りがない限りはこのディレクトリで作業を行うものとします。

bash
$ vi Gemfile

Gemfileの中身は、「How to install Quick using beta CocoaPods」で書けと言われた通りに書いています。

Gemfile
source 'https://rubygems.org'

gem 'cocoapods', :git => 'https://github.com/CocoaPods/CocoaPods.git', :branch => 'swift'
gem 'cocoapods-core', :git => 'https://github.com/CocoaPods/Core.git'
gem 'xcodeproj',  :git => 'https://github.com/CocoaPods/Xcodeproj.git'
gem 'claide', :git => 'https://github.com/CocoaPods/CLAide.git'

gemインストール

bash
$ sudo bundle install

下記のメッセージが表示されたら成功。

Your bundle is complete!
Use `bundle show [gemname]` to see where a bundled gem is installed.

Podfileの作成

続いて、NimbleとQuickを取得するようPodfileに書いてやります。このPodfileも、.xcodeprojのあるディレクトリに作ります。

bash
$ vi Podfile
Podfile
platform :ios, "8.0"

source 'https://github.com/CocoaPods/Specs.git'

pod 'Nimble', :git => "https://github.com/Quick/Nimble"
pod 'Quick', :git => 'https://github.com/Quick/Quick', :tag => 'v0.2.1'

あとはEscからの:wq!で上書き終了して、以下のコマンドを打つだけでOK。

bash
$ bundle exec pod install

実験、そしてまさかのエラー

Finderで.xcodeprojのあるディレクトリに行き、Xcode拡張子が.xcworkspaceの方を開きます。

Xcodeが勝手に作ったテスト用のソースSample1223Tests.swiftを以下のように書き換えてみました。

import Quick

class Sample1223Tests: QuickSpec {
}

すると、1行目のimportでまさかのエラー。「No such module 'Quick'」と怒られてしまいました。

f:id:tercel_s:20141223115025p:plain

どうやら、テストコードからCocoaPodsで追加したライブラリをうまく参照できていないようです。

参照の設定

設定手順は以下の画像を参照してください(手抜き)。

f:id:tercel_s:20141223115224p:plain

これで、「No such module 'Quick'」エラーが消えました! わはー

テストコードを書いてみる

というわけで、Developers.ioのこの記事から丸ごとパクった以下のコードで実験してみます。

import Quick
import Nimble

class Sample1223Tests : QuickSpec {
    override func spec() {
        it("is equal") {
            let value = 2 + 3
            expect(value).to(equal(5))
        }
    }
}

Command+uで無事にテストができました。
f:id:tercel_s:20141223193041p:plain
めでたし。

と見せかけて、まだまだ続くよ

今回導入するQuickとNimbleはテストでしか使わないライブラリなので、Podfileを書き換えてSample1223Tests以外のビルドターゲットからはQuickやNimbleを呼べなくしてやります。targetに「Sample1223Tests」を指定するだけです。

Podfile
platform :ios, "8.0"

target :Sample1223Tests, :exclusive => true do
        source 'https://github.com/CocoaPods/Specs.git'

        pod 'Nimble', :git => "https://github.com/Quick/Nimble"
        pod 'Quick', :git => 'https://github.com/Quick/Quick', :tag => 'v0.2.1'
end
bash
$ bundle exec pod update

ところが、Podfileを修正してCommand+uしたらまたしても謎のエラーが発生しました。

f:id:tercel_s:20141223182220p:plain

なんだこれは。

リンカエラーはさておき、まずはシェルスクリプト起動エラーからなんとかせねば。と思っていたら、GitHubで「diff: /../Podfile.lock: No such file or directory · Issue #2303 · CocoaPods/CocoaPods · GitHub」といういかにも関係ありそうな議論を見つけました。


diff: /../Podfile.lock: No such file or directory · Issue #2303 · CocoaPods/CocoaPods · GitHub

結局これを読んだところでなにひとつ解決しなかったわけですが。

試行錯誤

おそらく、target指定を行わないライブラリが1つ以上ないと、プロジェクトがConfiguration Fileを参照できなくてビルド時にコケるんじゃないか……とあたりをつけました。真偽のほどは分かりません。

f:id:tercel_s:20141223191738p:plain

そこで試しに、Podfileにtarget指定なしでAFNetworking を追加してみます(べつにAFNetworkingでなくてもよいのですが、定番中の定番なのでとりあえず)。

Podfile
platform :ios, "8.0"

pod 'AFNetworking'

target :Sample1223Tests, :exclusive => true do
        source 'https://github.com/CocoaPods/Specs.git'
        pod 'Nimble', :git => "https://github.com/Quick/Nimble"
        pod 'Quick', :git => 'https://github.com/Quick/Quick', :tag => 'v0.2.1'
end

pod updateを実行。

bash
$ bundle exec pod update

そして再度プロジェクトを開き、Configurationの設定を確認。プロジェクトと各ビルドターゲットに対して、Based on Configuration Fileを以下のように設定してCommand+u

f:id:tercel_s:20141223192245p:plain

シェル起動エラーがリンカエラーを道連れにして消えてくれました。
f:id:tercel_s:20141223193440p:plain
今度こそ本当にめでたし。

とはいえば、要りもしないライブラリ(今回はAFNetworking)が導入されてしまうのはあまり気持ちのよいものではありませんな。

実際に何か作るときには、何かしら一つはライブラリを入れるだろうから特に気にすることはないのかもしれませんが…。

まとめ

  • Quickは一応CocoaPodsでインストールできる
  • xUnit ぽくないから慣れるの大変そう
  • CocoaPodsの挙動がいまいちわからない
  • クリスマスが今年もやってくる*3
  • ヒメはヒメなのヒメなのだ*4

*1:root権限。

*2:Gemfileをどこに作ってよいのかわからなかったのでとりあえずプロジェクトのルートっぽいところに作りました。流儀として間違っていたらごめんなさい。

*3:ケンタッキーフライドチキンのCMソング。聞くと鬱になる。

*4:弱虫ペダルの主人公・小野田坂道が好きなアニメの主題歌。劇中では小野田が入学した時点で既に彼のiPodに収録されていたが、季節が変わってインターハイ終了後の秋葉原でもまだ店頭でCDが平積みされているなど異様なまでのロングセラーを誇っている。

Sprite Kitに初めて触ってみた

iOS端末でパーティクルをキラキラさせたい

こんにちは。何を血迷ったか超巨大端末のiPhone 6 Plusを予約してしまったたーせるです。

今もって手元に届いていないにも関わらず、待ちきれずに店頭のホットモックをお触りした結果、これは僕には合わないと確信しました。ちっちゃい方にしておけばよかった(それでも充分デカいけど)。

閑話休題

Appleの新言語・Swiftのお陰で、Objective-Cよりもだいぶプログラミングのフットワークは軽くなりました。

今までは触る気すら起きなかった技術に対しても少しだけ積極的に触れたくなり、言語が変わるだけでも視野の広がりを実感します。

というわけで、本日はSprite Kitをほんのちょっとだけ触ってみました。特にゲームが作りたかったわけではなく、単にパーティクルを飛ばしたかっただけ。ただそれだけです。

Swift + Sprite Kitでパーティクルを飛ばしたら割と綺麗だったので動画にしてみました。わはー∩( ・ω・)∩

なお余談ですが、Xcode 6.0.1でsksファイルを開くと、Xcodeがクラッシュしてしまうようです。

Swiftで弱参照コンテナ

今日のテーマ

  • 問題点: Swiftのコンテナをそのまま使うとオブジェクトを強参照してしまう
  • やること: オブジェクトの弱参照を格納するコンテナを作る

Swift復習:うんこの生成と消滅

まずは普通にクラスを作って、メモリの確保と解放が行われることを確認するよ。

// 一意なIDを返すIDシーケンサ
public struct IDSequensor {
    private static var _id:UInt64 = 0
    public static var ID:UInt64 {
        get { return _id++ }
    }
}

// てきとうなクラス
public class Unko {
    private let myId:UInt64
    public init(id: UInt64) {
        myId = id
        println("ID:\(myId)のインスタンスが作られました")
    }
    
    deinit {
        println("ID:\(myId)のインスタンスが殺されました")
    }
}

// ウンコインスタンスを作って…
var unko:Unko? = Unko(id: IDSequensor.ID)

// 殺す
unko = nil

これを実行すると、コンソールに以下のメッセージが出力されます。ここまでは想定の範囲内

ID:0のインスタンスが作られました
ID:0のインスタンスが殺されました
Program ended with exit code: 0

失敗例:コンテナを普通に使う

少しステップアップして、生成したうんこオブジェクトをコンテナに貯蔵する仕組みを考えてみるよ。

  • うんこが作られると同時に、コンテナに登録
  • うんこが死ぬと同時に、コンテナから削除

オブジェクトの生死と連動しているので、まずは素直にイニシャライザとデイニシャライザの中に登録と削除の処理を書いてみます。

初めに言っておきますが、この方法は失敗します

// てきとうなクラス
public class Unko {
    private let myId:UInt64
    public init(id: UInt64) {
        myId = id
        
        // New!!
        // 生まれた瞬間うんこプールに追加(なんだこのネーミング…)
        globalUnkoPool[myId] = self
        
        println("ID:\(myId)のインスタンスが作られました")
    }
    
    deinit {
        
        // New!!
        // 死に際にうんこプールから削除
        globalUnkoPool.removeValueForKey(myId)
        
        println("ID:\(myId)のインスタンスが殺されました")
    }
}

// New!!
// グローバルうんこプール
// 作ったウンコが自動的に溜められるコンテナ
var globalUnkoPool : [UInt64 : Unko] = [:]



// ウンコインスタンスを作って…
var unko:Unko? = Unko(id: IDSequensor.ID)

// 殺す
unko = nil

これを実行すると、なんとデイニシャライザが動きません。メモリリークです。

ID:0のインスタンスが作られました
Program ended with exit code: 0

上記コードでは、グローバルうんこプールがうんこインスタンスを強参照しているせいで、うんこオブジェクトを生成すると参照カウンタがいきなり2になってしまいます。上記のコードでうんこに nil を代入しても、参照カウンタが 1 しか減らないため、いつまでもメモリが解放されない問題を孕んでいます。

最終成果:弱参照コンテナ

以下の例では、オブジェクトの弱参照をコンテナに入れるようにしています。

// 一意なIDを返すIDシーケンサ
public struct IDSequensor {
    private static var _id:UInt64 = 0
    public static var ID:UInt64 {
        get { return _id++ }
    }
}

// 任意の型の弱参照を保持するクラス
public class Weak<T: AnyObject> {
    private weak var _element:T?
    public init(element: T) {
        _element = element
    }
}

// てきとうなクラス
public class Unko {
    private let myId:UInt64
    public init(id: UInt64) {
        myId = id
        
        // 生まれた瞬間のうんこをWeakで包んでプールに追加
        globalUnkoPool[myId] = Weak(element: self)
        
        println("ID:\(myId)のインスタンスが作られました")
    }
    
    deinit {
        
        // 死に際にうんこプールから削除
        globalUnkoPool.removeValueForKey(myId)
        
        println("ID:\(myId)のインスタンスが殺されました")
    }
}

// グローバルうんこプール
// 作ったウンコの弱参照が自動的に溜められるコンテナ
var globalUnkoPool : [UInt64 : Weak<Unko>] = [:]



// ウンコインスタンスを作って…
var unko:Unko? = Unko(id: IDSequensor.ID)

// 殺す
unko = nil

実行すると、ちゃんとうんこが死にます。やったね。

ID:0のインスタンスが作られました
ID:0のインスタンスが殺されました
Program ended with exit code: 0

参考: objective c - How do I declare an array of weak references in Swift? - Stack Overflow

SwiftでVisitorパターン

導入

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

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

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

SwiftでIteratorパターン

最近は、もっぱら言語レベルでサポートされるようになってしまった Iterator です。

増補改訂版Java言語で学ぶデザインパターン入門』では、記念すべき最初の章で紹介されるパターンです。堅物な(?) Java 向けの解説ということもあり、この本の Iterator パターンの章では、クラスやインタフェースが不自然なほど*1登場しますが、Swift はやや言語仕様が洗練されたこともあり、かなり自然に Iterator パターンを適用できます。

独自クラスでインデクサ [ ] とか for-in 構文とかを使えるようにするためのヒントになるかも。あまりヒントっぽくないかも。

ちなみに動作確認は Xcode 6 beta 5 で行っています。これより古い(または新しい)バージョンの Xcode では動かないかも知れません。

// ----------------------------------------
// Book (本) クラス
public class Book {
    private var _name: String
    
    public var name: String {
        get {
            return _name
        }
    }
    
    public init(name: String) {
        self._name = name
    }
}

// ----------------------------------------
// BookShelf (本棚) クラス
// SequenceTypeプロトコルを採用するのがチャームポイント
public class BookShelf: SequenceType {
    private var books:[Book] = []
    
    // プロパティさんです
    public var capacity: Int {
        get { return books.capacity }
    }
    
    public var startIndex: Int {
        get { return books.startIndex }
    }
    
    public var endIndex: Int {
        get { return books.endIndex }
    }
    
    public var count: Int {
        get { return books.count }
    }
    
    // インデクサ [ ] でアクセスできるようになるよ
    public subscript(index: Int) -> Book {
        get { return books[index] }
        set { books[index] = newValue }
    }
    
    public subscript(range: Range<Int>) -> Slice<Book> {
        get { return books[range] }
        set { books[range] = newValue }
    }
    
    // for-inでアクセスできるようになるよ
    public func generate() -> GeneratorOf<Book> {
        var index : Int = 0
        return GeneratorOf<Book> {
            return index < self.books.count ? self.books[index++] : .None
        }
    }
    
    // ↑の代わりに↓みたいに書いてもOK
    // ※ Arrayに委譲する書き方だよ
    // public func generate() -> IndexingGenerator<[Book]> {
    //     return books.generate()
    // }

    
    public func append(book: Book) {
        books.append(book)
    }
}

// ----------------------------------------
var bookShelf = BookShelf()
bookShelf.append(Book(name: "ももたろう"))
bookShelf.append(Book(name: "きんたろう"))
bookShelf.append(Book(name: "たーせるの大冒険"))

// [ ]でアクセスしてみるテスト
for var i = 0; i < bookShelf.count; ++i {
    println(bookShelf[i].name)
}

for i in bookShelf.startIndex ..< bookShelf.endIndex {
    println(bookShelf[i].name)
}

// というか for-in 構文でアクセスできる
for book in bookShelf {
    println(book.name)
}

*1:あくまでSwift目線から見たときの印象ですよ。

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