Dependency Injection in MVVM Architecture with ReactiveCocoa Part 3: モデルの設計

[前回Part 2](http://qiita.com/yoichitgy/items/ab9a3c019b4e3e89c63d)の翻訳から間が空いてしまいましたが、[ReactiveCocoa v4.0 RC1](https://github.com/ReactiveCocoa/ReactiveCocoa/releases)がリリースされてAPIが安定し、英語記事の更新も落ち着いたので翻訳を再開します。Part 3は以下のブログ記事の翻訳です[^訳注1]。

[Dependency Injection in MVVM Architecture with ReactiveCocoa Part 3: Designing the Model](https://yoichitgy.github.io/post/dependency-injection-in-mvvm-architecture-with-reactivecocoa-part-3-designing-the-model/)

* * *

[前回のブログ記事](http://qiita.com/yoichitgy/items/ab9a3c019b4e3e89c63d)では、Model、View、ViewModelの各フレームワークからなるアプリを開発するために、Xcodeのプロジェクトを設定しました。今回のブログ記事では、MVVMアーキテクチャのModel部分を開発します。エンティティとサービスで構成されるModelを設計し、dependency injectionを行う方法について学びます。MVVMアーキテクチャにより、システムを疎結合にすることが特徴です。[ReactiveCocoa](https://github.com/ReactiveCocoa/ReactiveCocoa)を使用して、MVVMアーキテクチャのModel、View、ViewModelを疎結合にするために不可欠なイベントハンドリングを行います。また、JSONからSwiftの型へマッピングを行う[Himotoki](https://github.com/ikesyo/Himotoki)の使い方について学びます。

ソースコードはGitHubのリポジトリからダウンロードできます。

- [SwinjectMVVMExample](https://github.com/Swinject/SwinjectMVVMExample): 発展形を含むプロジェクト
- [SwinjectMVVMExample_ForBlog](https://github.com/yoichitgy/SwinjectMVVMExample_ForBlog): (Xcodeや外部フレームワークの更新を除き) ブログ記事に沿った説明のための簡略化したプロジェクト

![SwinjectMVVMExample ScreenRecord](https://yoichitgy.github.io/images/post/2015-08/SwinjectMVVMExampleScreenRecord.gif)

## Himotoki

[Himotoki](https://github.com/ikesyo/Himotoki)はSwiftで書かれたタイプセーフなJSONデコード専用ライブラリです。JSONからSwiftの型へのマッピングを`Decodable`プロトコルとして簡潔に記述できます。特に、イミュータブルな (let) プロパティへのマッピングをサポートしているところが特徴です。

使い方は簡単です。ここでは、`{ "some_name": "Himotoki", "some_value": 1 }`のようなJSONを`SomeValue`型にマッピングするとします。マッピングの記述のため、`SomeValue`型を`Decodable`プロトコルに準拠させます。

    struct SomeValue {
        let name: String
        let value: Int
    }

    extension SomeValue: Decodable {
        static func decode(e: Extractor) throws -> Group {
            return try SomeValue(
                name: e <| "some_name",
                value: e <| "some_value"
            )
        }
    }

`decode`関数の中で、`SomeValue`のイニシャライザの引数としてマッピングを表現しています。ここでは、`some_name`を`SomeValue`の`name`プロパティへ、`some_value`を`value`プロパティへマッピングしています。

JSONデータからマッピングしたインスタンスを取得するためには、AlamofireやNSJSONSerializationから返される
`[String: AnyObject]`型のJSONデータを引数として`decode`関数を呼びます。

    func testSomeValue() {
        // JSON data returned from Alamofire or NSJSONSerialization.
        let json: [String: AnyObject] = ["some_name": "Himotoki", "some_value": 1]

        let v: SomeValue? = try? decode(json)
        XCTAssert(v != nil)
        XCTAssert(v?.name == "Himotoki")
        XCTAssert(v?.value == 1)
    }

Himotokiを利用することにより、JSONデータを扱うためのコードを減らすことができます[^1]。Himotokiはネストされたデータやオプショナルパラメータもサポートしています。詳しくは[プロジェクトのページ](https://github.com/ikesyo/Himotoki)を参照してください。

## Pixabay APIの仕様

[Pixabay APIのドキュメント](https://pixabay.com/api/docs/)によると、Pixabayのサーバから返ってくるJSONデータは以下ようなフォーマットになっています。画像情報の配列を`hits`、見つかった画像の総数を`total`、APIで利用可能な画像の総数を`totalHits`として取り出すことができます。

    {
        "total": 12274,
        "totalHits": 240,
        "hits": [
            {
                "id": 11574,
                "pageURL": "https://pixabay.com/en/sonnenblumen-sonnenblumenfeld-flora-11574/",
                "type": "photo",
                "tags": "sunflower, sunflower field, flora",
                "previewURL": "https://pixabay.com/static/uploads/photo/2012/01/07/21/56/sunflower-11574_150.jpg",
                "previewWidth": 150,
                "previewHeight": 92,
                "webformatURL": "https://pixabay.com/get/3b4f5d71752e6ce9cbcf/1356479243/aca42219d23fd9fe0cc6f1cc_640.jpg",
                "webformatWidth": 640,
                "webformatHeight": 396,
                "imageWidth": 1280,
                "imageHeight": 792,
                "views": 10928,
                "downloads": 1649,
                "likes": 70,
                "user": "WikiImages"
            },
            {
                "id": "256",
                "pageURL": "https://pixabay.com/en/example-image-256/",
                "type": "photo",
                // ... etc.
            },
            //... 18 more hits for page number 1
        ]
    }

## Modelの設計の概要

今回の設計では、Modelをエンティティとサービスで構成します。簡単に言うと、エンティティは対象となるシステムに存在する概念あるいはオブジェクトです[^2]。サービスは状態のないオペレーションで、エンティティには収まらないものです。

ViewModelとModel、そしてModelと外部システムを疎結合にするため、それらのインターフェイスをプロトコルとして定義します。下の図で`ImageSearching`と`Networking`がプロトコルです。`ImageSearch`と`Network`はそれらの実装です。ViewModelは`ImageSearching`プロトコルを通してModelにアクセスし、そのプロトコルの実装である`ImageSearch`は`Networking`プロトコルを通して外部のシステムにアクセスします。外部システムがJSONデータを持つイベントを発生させ、そのイベントがViewModelへと伝播していく時に`ImageSearch`によってデータが`ResponseEntity`や`ImageEntity`に変換されます。

![Model Designg](https://qiita-image-store.s3.amazonaws.com/0/86677/30ba4f89-d90e-4ead-daaf-59f3508100a7.png)

## エンティティ

この節では、Pixabayから返される画像とレスポンスを表すエンティティを定義していきます。`ExampleModel`ターゲットに以下の内容の`ImageEntity.swift`を追加してください。Project Navigator上の`ExampleModel`グループ (フォルダアイコン) を右クリックして`New File...`を選択し、`Swift File`を選択すれば、`ExampleModel`ターゲットにファイルを追加できます。Xcodeがどのターゲットにファイルを追加するか聞いてきた時には`ExampleModel`のみチェックしておいてください。

**ImageEntity.swift**

    import Himotoki

    public struct ImageEntity {
        public let id: UInt64

        public let pageURL: String
        public let pageImageWidth: Int
        public let pageImageHeight: Int

        public let previewURL: String
        public let previewWidth: Int
        public let previewHeight: Int

        public let imageURL: String
        public let imageWidth: Int
        public let imageHeight: Int

        public let viewCount: Int64
        public let downloadCount: Int64
        public let likeCount: Int64
        public let tags: [String]
        public let username: String
    }

    // MARK: Decodable
    extension ImageEntity: Decodable {
        public static func decode(e: Extractor) throws -> ImageEntity {
            let splitCSV: String -> [String] = { csv in
                csv.characters
                    .split { $0 == "," }
                    .map {
                        String($0).stringByTrimmingCharactersInSet(
                            NSCharacterSet.whitespaceCharacterSet())
                    }
            }

            return try ImageEntity(
                id: e <| "id",

                pageURL: e <| "pageURL",
                pageImageWidth: e <| "imageWidth",
                pageImageHeight: e <| "imageHeight",

                previewURL: e <| "previewURL",
                previewWidth: e <| "previewWidth",
                previewHeight: e <| "previewHeight",

                imageURL: e <| "webformatURL",
                imageWidth: e <| "webformatWidth",
                imageHeight: e <| "webformatHeight",

                viewCount: e <| "views",
                downloadCount: e <| "downloads",
                likeCount: e <| "likes",
                tags: (try? e <| "tags").map(splitCSV) ?? [],
                username: e <| "user"
            )
        }
    }

`ImageEntity`がイミュータブルなプロパティを持つ`struct`として定義してあることに注意してください。イミュータブルであることにより、エンティティを使用する際の安全性を確保できます[^3]。`ExampleViewModel`ターゲットから参照できるよう、エンティティの型は`public`で定義してあります。

`ImageEntity`には、Swiftの命名慣習とアプリの構成に合わせ、JSONの要素と異なる名前にしたプロパティもあります。`id`、`viewCount`、`downloadCount`、`likeCount`は、32-bitシステムでも大きな値を扱えるよう、`UInt64`や`Int64`で定義してあります。JSONの要素`tags`は[CSV](https://en.wikipedia.org/wiki/Comma-separated_values)形式の文字列となっていますが、`ImageEntity `では`(try? e <| "tags").map(splitCSV)`により分割した文字列の配列になっています。`?? []`を`map`の返り値に適用することにより、JSONデータに`tags`がなくて`nil`が返ってきた場合は代わりに空配列が使用されます。

`ExampleModel`ターゲットに以下の内容の`ResponseEntity.swift `を追加してください。ここでは、配列をマッピングするために`<||`演算子を使用しています。JSONの`total`要素は無視しています。もし後で必要になったら追加すればよいでしょう。

**ResponseEntity.swift**

    import Himotoki

    public struct ResponseEntity {
        public let totalCount: Int64
        public let images: [ImageEntity]
    }

    // MARK: Decodable
    extension ResponseEntity: Decodable {
        public static func decode(e: Extractor) throws -> ResponseEntity {
            return try ResponseEntity(
                totalCount: e <| "totalHits",
                images: e <|| "hits"
            )
        }
    }

`ImageEntity`をテストするため、`ExampleModelTests`ターゲットに以下の内容の`Dummy.swift`と`ImageEntitySpec.swift`を追加してください。

**Dummy.swift**

    let imageJSON: [String: AnyObject] = [
        "id": 12345,
        "pageURL": "https://somewhere.com/page/",
        "imageWidth": 2000,
        "imageHeight": 1000,
        "previewURL": "https://somewhere.com/preview.jpg",
        "previewWidth": 200,
        "previewHeight": 100,
        "webformatURL": "https://somewhere.com/image.jpg",
        "webformatWidth": 600,
        "webformatHeight": 300,
        "views": 54321,
        "downloads": 4321,
        "likes": 321,
        "tags": "a, b c, d ",
        "user": "Swinject"
    ]

**ImageEntitySpec.swift**

    import Quick
    import Nimble
    import Himotoki
    @testable import ExampleModel

    class ImageEntitySpec: QuickSpec {
        override func spec() {
            it("parses JSON data to create a new instance.") {
                let image: ImageEntity? = try? decode(imageJSON)

                expect(image).notTo(beNil())
                expect(image?.id) == 12345
                expect(image?.pageURL) == "https://somewhere.com/page/"
                expect(image?.pageImageWidth) == 2000
                expect(image?.pageImageHeight) == 1000
                expect(image?.previewURL) == "https://somewhere.com/preview.jpg"
                expect(image?.previewWidth) == 200
                expect(image?.previewHeight) == 100
                expect(image?.imageURL) == "https://somewhere.com/image.jpg"
                expect(image?.imageWidth) == 600
                expect(image?.imageHeight) == 300
                expect(image?.viewCount) == 54321
                expect(image?.downloadCount) == 4321
                expect(image?.likeCount) == 321
                expect(image?.tags) == ["a", "b c", "d"]
                expect(image?.username) == "Swinject"
            }
            it("gets an empty array if tags element is nil.") {
                var missingJSON = imageJSON
                missingJSON["tags"] = nil
                let image: ImageEntity? = try? decode(missingJSON)

                expect(image).notTo(beNil())
                expect(image?.tags.isEmpty).to(beTrue())
            }
            it("throws an error if any of JSON elements except tags is missing.") {
                for key in imageJSON.keys where key != "tags" {
                    var missingJSON = imageJSON
                    missingJSON[key] = nil
                    let image: ImageEntity? = try? decode(missingJSON)

                    expect(image).to(beNil())
                }
            }
            it("ignores an extra JSON element.") {
                var extraJSON = imageJSON
                extraJSON["extraKey"] = "extra element"
                let image: ImageEntity? = try? decode(extraJSON)

                expect(image).notTo(beNil())
            }
        }
    }

`Dummy.swift`の中では、ダミーのJSONデータのインスタンスを`imageJSON`として定義しています。AlamofireやNSJSONSerializationから返ってくるような`[String: AnyObject]`型のJSONデータをHimotokiは扱うため、`imageJSON`は`String`ではなく`[String: AnyObject]`として定義しています。

`it("parses JSON data to create a new instance.")`では、`ImageEntity`のすべてのプロパティがJSONからマッピングされていることを確認しています。

`it("gets an empty array if tags element is nil.")`では、JSONデータに`tags`要素が含まれない場合に、`tags`プロパティに空配列がセットされることを確認しています。

`it("throws an error if any of JSON elements except tags is missing.")`では、`tags`以外のJSON要素が欠けている場合に`decode`関数がエラーを投げることを確認しています。不正な、あるいは壊れたJSONデータは`try?`で`nil`が返ってくるか見ることにより確認できます。

`it("ignores an extra JSON element.")`では、JSONデータに余分な要素があっても`decode`関数は`ImageEntity`のインスタンスを返すことを確認しています。余分な要素を無視することにより、将来のPixabay APIの変更に対してJSONデータのマッピングが安全になります。

それでは次に、`ExampleModelTests`ターゲットに以下の内容の`ResponseEntitySpec.swift`を追加してください。テストの内容は見たとおり簡単です。

**ResponseEntitySpec.swift**

    import Quick
    import Nimble
    import Himotoki
    @testable import ExampleModel

    class ResponseEntitySpec: QuickSpec {
        override func spec() {
            let json: [String: AnyObject] = [
                "totalHits": 123,
                "hits": [imageJSON, imageJSON]
            ]

            it("parses JSON data to create a new instance.") {
                let response: ResponseEntity? = try? decode(json)

                expect(response).notTo(beNil())
                expect(response?.totalCount) == 123
                expect(response?.images.count) == 2
            }
        }
    }

`Command-U`を入力してユニットテストを実行しましょう。パスしましたね。ここでは問題なくテストがパスしたと思いますが、実際の開発ではテストがパスするまでエンティティやテストの修正を繰り返すことになります。

## ネットワークサービス

[シンプルな天気アプリの例](http://qiita.com/yoichitgy/items/75b09e6ff06199d7cf80)の時と同様に、`Networking`に適合する`Network`型にAlamofireをカプセル化します。今回は、ReactiveCocoaを使ってネットワークのイベントをハンドリングします。`ExampleModel`ターゲットに以下の内容の`Networking.swift`と`Network.swift`を追加してください。

**Networking.swift**

    import ReactiveCocoa

    public protocol Networking {
        func requestJSON(url: String, parameters: [String : AnyObject]?)
            -> SignalProducer<AnyObject, NetworkError>
    }

**Network.swift**

    import ReactiveCocoa
    import Alamofire

    public final class Network: Networking {
        private let queue = dispatch_queue_create(
            "SwinjectMMVMExample.ExampleModel.Network.Queue",
            DISPATCH_QUEUE_SERIAL)

        public init() { }

        public func requestJSON(url: String, parameters: [String : AnyObject]?)
            -> SignalProducer<AnyObject, NetworkError>
        {
            return SignalProducer { observer, disposable in
                let serializer = Alamofire.Request.JSONResponseSerializer()
                Alamofire.request(.GET, url, parameters: parameters)
                    .response(queue: self.queue, responseSerializer: serializer) {
                        response in
                        switch response.result {
                        case .Success(let value):
                            observer.sendNext(value)
                            observer.sendCompleted()
                        case .Failure(let error):
                            observer.sendFailed(NetworkError(error: error))
                        }
                    }
            }
        }
    }

[天気アプリの例](http://qiita.com/yoichitgy/items/75b09e6ff06199d7cf80)では、ネットワークサービスはコールバックを用いてレスポンスを受け渡していました。今回は、`requestJSON`メソッドが`SignalProducer<AnyObject, NetworkError>`を返し、それによりオブザーバにイベントを伝えるようにしています。Alamofire (あるいはNSJSONSerialization) がJSONデータを`AnyObject`型 (実体は配列または辞書) として返すため、`SignalProducer`はジェネリック型として`AnyObject`をとっています。

`requestJSON`メソッドは、トレイリングクロージャを使って`SignalProducer`のインスタンスを生成して返しています。そのクロージャの中では、`sendNext`、`sendCompleted`、`sendFailed`を呼び出して、AlamofireのレスポンスのイベントからReactiveCocoaのイベントに変換しています。`SignalProducer`インスタンスの`start`メソッドが呼ばれるまでは、インスタンス生成時に渡されたクロージャは実際には実行されないことに注意してください。

デフォルトではAlamofireはレスポンスをメインスレッド (メインキュー) で返すため、ディスパッチキューをAlamofireに渡してバックグラウンドでレスポンスを返すようにしています。

`sendFailed`メソッドは`NetworkError`のインスタンスを引数にとります。`ExampleModel`ターゲットに以下の内容の`NetworkError.swift`を追加してください。Alamofireから送られてくる`NSError`をアプリ固有のエラー型に変換しています[^4]。

**NetworkError.swift**

    import Foundation

    public enum NetworkError: ErrorType {
        /// Unknown or not supported error.
        case Unknown

        /// Not connected to the internet.
        case NotConnectedToInternet

        /// International data roaming turned off.
        case InternationalRoamingOff

        /// Cannot reach the server.
        case NotReachedServer

        /// Connection is lost.
        case ConnectionLost

        /// Incorrect data returned from the server.
        case IncorrectDataReturned

        internal init(error: NSError) {
            if error.domain == NSURLErrorDomain {
                switch error.code {
                case NSURLErrorUnknown:
                    self = .Unknown
                case NSURLErrorCancelled:
                    self = .Unknown // Cancellation is not used in this project.
                case NSURLErrorBadURL:
                    self = .IncorrectDataReturned // Because it is caused by a bad URL returned in a JSON response from the server.
                case NSURLErrorTimedOut:
                    self = .NotReachedServer
                case NSURLErrorUnsupportedURL:
                    self = .IncorrectDataReturned
                case NSURLErrorCannotFindHost, NSURLErrorCannotConnectToHost:
                    self = .NotReachedServer
                case NSURLErrorDataLengthExceedsMaximum:
                    self = .IncorrectDataReturned
                case NSURLErrorNetworkConnectionLost:
                    self = .ConnectionLost
                case NSURLErrorDNSLookupFailed:
                    self = .NotReachedServer
                case NSURLErrorHTTPTooManyRedirects:
                    self = .Unknown
                case NSURLErrorResourceUnavailable:
                    self = .IncorrectDataReturned
                case NSURLErrorNotConnectedToInternet:
                    self = .NotConnectedToInternet
                case NSURLErrorRedirectToNonExistentLocation, NSURLErrorBadServerResponse:
                    self = .IncorrectDataReturned
                case NSURLErrorUserCancelledAuthentication, NSURLErrorUserAuthenticationRequired:
                    self = .Unknown
                case NSURLErrorZeroByteResource, NSURLErrorCannotDecodeRawData, NSURLErrorCannotDecodeContentData:
                    self = .IncorrectDataReturned
                case NSURLErrorCannotParseResponse:
                    self = .IncorrectDataReturned
                case NSURLErrorInternationalRoamingOff:
                    self = .InternationalRoamingOff
                case NSURLErrorCallIsActive, NSURLErrorDataNotAllowed, NSURLErrorRequestBodyStreamExhausted:
                    self = .Unknown
                case NSURLErrorFileDoesNotExist, NSURLErrorFileIsDirectory:
                    self = .IncorrectDataReturned
                case
                NSURLErrorNoPermissionsToReadFile,
                NSURLErrorSecureConnectionFailed,
                NSURLErrorServerCertificateHasBadDate,
                NSURLErrorServerCertificateUntrusted,
                NSURLErrorServerCertificateHasUnknownRoot,
                NSURLErrorServerCertificateNotYetValid,
                NSURLErrorClientCertificateRejected,
                NSURLErrorClientCertificateRequired,
                NSURLErrorCannotLoadFromNetwork,
                NSURLErrorCannotCreateFile,
                NSURLErrorCannotOpenFile,
                NSURLErrorCannotCloseFile,
                NSURLErrorCannotWriteToFile,
                NSURLErrorCannotRemoveFile,
                NSURLErrorCannotMoveFile,
                NSURLErrorDownloadDecodingFailedMidStream,
                NSURLErrorDownloadDecodingFailedToComplete:
                    self = .Unknown
                default:
                    self = .Unknown
                }
            }
            else {
                self = .Unknown
            }
        }
    }

それでは`Network`型のユニットテストを追加しましょう。`ExampleModelTests`ターゲットに以下の内容の`NetworkSpec.swift `を追加してください。

**NetworkSpec.swift**

    import Quick
    import Nimble
    @testable import ExampleModel

    class NetworkSpec: QuickSpec {
        override func spec() {
            var network: Network!
            beforeEach {
                network = Network()
            }

            describe("JSON") {
                it("eventually gets JSON data as specified with parameters.") {
                    var json: [String: AnyObject]? = nil
                    let url = "https://httpbin.org/get"
                    network.requestJSON(url, parameters: ["a": "b", "x": "y"])
                        .on(next: { json = $0 as? [String: AnyObject] })
                        .start()

                    expect(json).toEventuallyNot(beNil(), timeout: 5)
                    expect((json?["args"] as? [String: AnyObject])?["a"] as? String)
                        .toEventually(equal("b"), timeout: 5)
                    expect((json?["args"] as? [String: AnyObject])?["x"] as? String)
                        .toEventually(equal("y"), timeout: 5)
                }
                it("eventually gets an error if the network has a problem.") {
                    var error: NetworkError? = nil
                    let url = "https://not.existing.server.comm/get"
                    network.requestJSON(url, parameters: ["a": "b", "x": "y"])
                        .on(failed: { error = $0 })
                        .start()

                    expect(error)
                        .toEventually(equal(NetworkError.NotReachedServer), timeout: 5)
                }
            }
        }
    }

ここでは、ネットワークをテストするための簡単で安定したサーバとして[httpbin.org](https://httpbin.org/)を使用しています。そのサーバは[Alamofireのユニットテスト](https://github.com/Alamofire/Alamofire/blob/1978c2c926b0eabedc858d4cde0533e00686ccd6/Tests/ResponseTests.swift)でも使用されています。試しにブラウザで [https://httpbin.org/get?a=b&x=y](https://httpbin.org/get?a=b&x=y) にアクセスしてみると、どのようにレスポンスが返ってくるかわかります。

最初のテストでは、リクエストで指定したようにレスポンスのJSONに`"a"`および`"x"`要素があり、それぞれ値が`"b"`および`"y"`となっていることを確認しています。`json`変数を使用して、サーバからのレスポンスを非同期に受け取っています。`requestJSON`から返される`SignalProducer`にオブザーバ (つまりイベントハンドラ) を追加するため、`json`変数にレスポンスをセットするクロージャを引数として`on`メソッドを呼んでいます。その後、`start`メソッドにより`SignalProducer`のシグナルを開始しています。レスポンスは`toEventually`メソッドにより非同期でチェックされています。

2番目のテストでは、エラーの場合に`NetworkError`が発生することを確認しています。擬似的にエラーを発生させるため、存在しないURLを`requestJSON`に渡しています。

ユニットテストを実行し、次の節に移りましょう。

## 画像検索サービス

この節では、Pixabay APIを利用して画像を検索するサービスを定義していきます。ここが`ExampleModel`の中心です。

最初に、`ExampleModel`ターゲットに以下の内容の`Pixabay.swift`を追加してください。APIのURLとパラメータを定義しています。`apiKey`には[Pixabayから取得したご自身のAPIキー](https://pixabay.com/api/docs/)を指定してください。

**Pixabay.swift**

    internal struct Pixabay {
        internal static let apiURL = "https://pixabay.com/api/"

        internal static var requestParameters: [String: AnyObject] {
            return [
                "key": Config.apiKey,
                "image_type": "photo",
                "safesearch": true,
                "per_page": 50,
            ]
        }
    }

    extension Pixabay {
        private struct Config {
            private static let apiKey = "" // Fill with your own API key.
        }
    }

次に、`ExampleModel`ターゲットに以下の内容の`ImageSearching.swift`を追加してください。`ResponseEntity`の`SignalProducer`を返す`searchImages`メソッドを持つプロトコルを定義しています。

**ImageSearching.swift**

    import ReactiveCocoa

    public protocol ImageSearching {
        func searchImages() -> SignalProducer<ResponseEntity, NetworkError>
    }

最後に、`ExampleModel`ターゲットに以下の内容の`ImageSearch.swift`を追加してください。

**ImageSearch.swift**

    import ReactiveCocoa
    import Result
    import Himotoki

    public final class ImageSearch: ImageSearching {
        private let network: Networking

        public init(network: Networking) {
            self.network = network
        }

        public func searchImages() -> SignalProducer<ResponseEntity, NetworkError> {
            let url = Pixabay.apiURL
            let parameters = Pixabay.requestParameters
            return network.requestJSON(url, parameters: parameters)
                .attemptMap { json in
                    if let response = (try? decode(json)) as ResponseEntity? {
                        return Result(value: response)
                    }
                    else {
                        return Result(error: .IncorrectDataReturned)
                    }
            }
        }
    }

`ImageSearch`は`Networking`に依存しており、その依存性はイニシャライザで注入されています ([Initializer Injectionパターン](https://github.com/Swinject/Swinject/blob/master/Documentation/InjectionPatterns.md))。

`searchImages`メソッドは、`network.requestJSON`から返される`SignalProducer<AnyObject, NetworkError>`を`SignalProducer<ResponseEntity, NetworkError>`に変換しています。`SignalProducer`の変換で`attemptMap`を使っています。`attemptMap`に渡したクロージャの中では、`decode`を呼んでJSONデータを`ResponseEntity`インスタンスにマッピングしています。マッピングが成功した場合、`ResponseEntity`インスタンスを`Result(value: response)`で包んで返しています。失敗した場合[^5]、`Result(error: .IncorrectDataReturned)`としてエラーを返しています。もしある値をエラーでなく別の値に変換するだけであれば、`SignalProducer`の`map`メソッドが利用できます。

`(try? decode(json)) as ResponseEntity?`のキャストは見慣れないかもしれませんが、`decode`関数が`ResponseEntity`型を扱うようSwiftコンパイラの型推論を補助しているものです。もしキャストが`(try? decode(json)) as? ResponseEntity`であれば、ソースコードをコンパイルすることができません。

それではユニットテストを書きましょう。`ExampleModelTests`ターゲットに以下の内容の`ImageSearchSpec.swift`を追加してください。

**ImageSearchSpec.swift**

    import Quick
    import Nimble
    import ReactiveCocoa
    @testable import ExampleModel

    class ImageSearchSpec: QuickSpec {
        // MARK: Stub
        class GoodStubNetwork: Networking {
            func requestJSON(url: String, parameters: [String : AnyObject]?)
                -> SignalProducer<AnyObject, NetworkError>
            {
                var imageJSON0 = imageJSON
                imageJSON0["id"] = 0
                var imageJSON1 = imageJSON
                imageJSON1["id"] = 1
                let json: [String: AnyObject] = [
                    "totalHits": 123,
                    "hits": [imageJSON0, imageJSON1]
                ]

                return SignalProducer { observer, disposable in
                    observer.sendNext(json)
                    observer.sendCompleted()
                }.observeOn(QueueScheduler())
            }
        }

        class BadStubNetwork: Networking {
            func requestJSON(url: String, parameters: [String : AnyObject]?)
                -> SignalProducer<AnyObject, NetworkError>
            {
                let json = [String: AnyObject]()

                return SignalProducer { observer, disposable in
                    observer.sendNext(json)
                    observer.sendCompleted()
                }.observeOn(QueueScheduler())
            }
        }

        class ErrorStubNetwork: Networking {
            func requestJSON(url: String, parameters: [String : AnyObject]?)
                -> SignalProducer<AnyObject, NetworkError>
            {
                return SignalProducer { observer, disposable in
                    observer.sendFailed(.NotConnectedToInternet)
                }.observeOn(QueueScheduler())
            }
        }

        // MARK: - Spec
        override func spec() {
            it("returns images if the network works correctly.") {
                var response: ResponseEntity? = nil
                let search = ImageSearch(network: GoodStubNetwork())
                search.searchImages()
                    .on(next: { response = $0 })
                    .start()

                expect(response).toEventuallyNot(beNil())
                expect(response?.totalCount).toEventually(equal(123))
                expect(response?.images.count).toEventually(equal(2))
                expect(response?.images[0].id).toEventually(equal(0))
                expect(response?.images[1].id).toEventually(equal(1))
            }
            it("sends an error if the network returns incorrect data.") {
                var error: NetworkError? = nil
                let search = ImageSearch(network: BadStubNetwork())
                search.searchImages()
                    .on(failed: { error = $0 })
                    .start()

                expect(error).toEventually(equal(NetworkError.IncorrectDataReturned))
            }
            it("passes the error sent by the network.") {
                var error: NetworkError? = nil
                let search = ImageSearch(network: ErrorStubNetwork())
                search.searchImages()
                    .on(failed: { error = $0 })
                    .start()

                expect(error).toEventually(equal(NetworkError.NotConnectedToInternet))
            }
        }
    }

先頭で3つのスタブを定義しています。`GoodStubNetwork`は正しいJSONデータを出力する`SignalProducer`を返します。`BadStubNetwork`は、空の辞書の形で不正なJSONデータを出力する`SignalProducer`を返します。`ErrorStubNetwork`は、JSONデータの代わりにエラーを出力する`SignalProducer`を返します。`.observeOn(QueueScheduler())`を使うことにより、すべての`SignalProducer`においてイベントをバックグラウンドで出力し、非同期なネットワークのレスポンスを模擬しています。

`spec()`の中では、3つのユニットテスト (あるいはスペック) を定義しています。1つ目は、`attemptMap`が正しくJSONデータを`ResponseEntity`に変換していることをチェックしています。2つ目は、`attemptMap`がJSONデータをエラーに変換するケースをチェックしています。3つ目は、`ErrorStubNetwork`から出力されるエラーが`ImageSearch`を通して渡ってくることをチェックしています。

`Command-U`を入力してテストを実行しましょう。パスしましたね！これで、MVVMアーキテクチャのModelの中心的な部分を実装し終えました。

## まとめ

例題アプリのModel部分をMVVMアーキテクチャで開発することを通して、ModelをViewModelおよび外部システムから疎結合にする設計方法を学びました。プロトコルを使用することにより、実装に対する直接的な依存を排除できました。疎結合にしたコンポーネント間のイベントハンドリングのためにRectiveCocoaを利用したました。また、Himotokiを使うと簡単にJSONデータをエンティティにマッピングできることを見ました。[次回のブログ記事](http://qiita.com/yoichitgy/items/2857b98807e6c4b3fec1)では、ViewとViewModel部分を設計して実装していきます。

もし質問、提案、問題などがあれば気軽にコメントをどうぞ。

[^1]: [関数型プログラミング](https://en.wikipedia.org/wiki/Functional_programming)に慣れていたら、[Argo](https://github.com/thoughtbot/Argo)も便利なJSONパーサかもしれません。
[^2]: [DDD (ドメイン駆動設計)](https://en.wikipedia.org/wiki/Domain-driven_design)では、エンティティはドメインモデルの概念であり、アイデンティティにより定義されます。このブログ記事では、アイデンティティは関係なく、単に概念やオブジェクトを指す言葉としてエンティティを用います。
[^3]: イミュータブルあるいはミュータブルであることのより詳しい説明は[このページ](http://programmers.stackexchange.com/questions/151733/if-immutable-objects-are-good-why-do-people-keep-creating-mutable-objects)を参照してください。
[^4]: `ErrorType`についてより深く学ぶために、"[How to Implement the ErrorType Protocol](https://realm.io/news/testing-swift-error-type/)"を読むことをお薦めします。
[^5]: `try?`は単にブログ記事を簡略化するために使用しています。`do-try-catch`のほうがエラー情報に基づいたハンドリングに適しています。
[^訳注1]: 訳注: 英語版の著者本人による翻訳のため、翻訳に関わる著作権上の問題はありません。