APIKitでRequestのイニシャライザに渡すのはValueObjectやEntityじゃないほうが良い

  • 11
    いいね
  • 1
    コメント

はじめに

iOSアプリ開発のために、APIKit(もしくはインターフェースをAPIKitふうにしたもの)を利用していると、コードレビューの際に他人の使い方を見ることがあって、そういうときにAPIKitの使い方について人に指摘することがあったのでそれを書いておく。

Requestのイニシャライザに渡すのはValueObjectやEntityじゃないほうがいい

APIKitのようなやり方の良さの一つとして、リクエストとレスポンスが何らかのWebAPIドキュメントと見比べたときに明確さがあるということだと思う。

それで大抵の場合、必要なパラメータをイニシャライザで渡すことになるけど、そのイニシャライザでは自作のValueObjectやEntityを渡すのではなく、StringやBool, NSNumberなんかのほうがいいですよという話。

例を挙げて説明

架空のサービスへのログインのリクエストを例として挙げる。このWebAPI仕様としてはメールアドレスとパスワードからアクセストークンを返していて、アクセストークンを取得することをアプリでは便宜上ログインとしている。

class Login: DemoRequest {
    typealias Response = String

    let email: String
    let password: String

    init(email: String, password: String) {
        self.email = email
        self.password = password
    }

    var method: HTTPMethod {
        return .post
    }

    var path: String {
        return "/oauth/token"
    }

    var parameters: Any? {
        let params: [String: Any] = [
            "grant_type": "password",
            "username": email,
            "password": password
        ]
        return params
    }

    func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Response {

        // いろいろ省略
        return accessToken
    }
}

いろいろと省略しているけど、言いたいことは、上の例のイニシャライザがユーザーに関する情報と同じだからといって、EntityであるUserなどとしないほうがいい。つまり下記のようにclass Userがあるからといってもそれをリクエストのイニシャライザに使わない方がいい。

final class User {
    let id: NSNumber
    let email: String
    let password: String
    let firstName: String
    let lastName: String
    var nickname: String?
    // 色々省略
}
class Login: DemoRequest {
    typealias Response = String

    let user: User

    init(user: User) {
        self.user = user
    }

    var parameters: Any? {
        // いろいろ省略。userからemailとパスワードを取り出す
        return params
    }

    // 省略
}

なぜRequestのイニシャライザにEntityやValueObjectを使わないか

例として書くのが楽だからメールアドレスとパスワードだけのパラメータにしたが、それによって理由がわかりにくいかもしれない

  • WebAPIの仕様書では必須パラメータとして記述されているメールアドレス、パスワードとUserとは関連性が薄い
    • 強引な理由: class Userを変更してemailをドメインと分割したいような場合、その変更がRequestまで関連してくる
    • Userの中にはOptionalな項目もあり、それが無かったらどうなるのか分かりづらくなる
  • テストコードを書く際にメールアドレスとパスワードだけあればいいのにわざわざUserを作るのが面倒

その他

しかしResponseはValueObjectかEntityのほうがいい

リクエストはなるべくシンプルにした方がいいとは思うものの、レスポンスに関してはValueObjectやEntityのほうが良い場合がある。なぜなら、RestのAPIならレスポンスは使い回しの型情報を意識している事が多く、レスポンスのイニシャライザで必須な項目を組み立てるほうが秩序だっているし効率がいい。

つまり、最初の例のレスポンスはStringとしてaccessTokenを返しているものの、有効期限なんかがあればstruct AccessToken を作ってそれをレスポンスとするほうがいい。

struct AccessToken {
    let token: String
    let expirationDate: Date

    init(token: String, expirationDate: Date) {
        self.token = token
        self.expirationDate = expirationDate
    }
}

// MARK: - JSONからinitする処理省略
extension AccessToken {}

おわりに

必須の項目を機能のイニシャライザに渡すことで制限が生まれ、その制限が秩序を生むのでコードを修正する際に既存の仕様に沿ったコードを書きやすい。言い換えると、不具合修正や機能追加の際に既存の機能をぶっ壊しにくい。もし、イニシャライザにEntityやValueObjectを渡すことになると、それらのオプショナルな項目がどういう影響をするのかを確認しなければいけなくなる不安が出てきてしまうというのも上記の理由の一つだとは思う。

それでプログラミング言語やプラットフォームに関係なく、複数の処理をまとめたUseCaseクラスを利用する際もイニシャライザで必須項目は同じようにする事が多い。例えばコメントを投稿しつつツイートするとかと言った処理を一つのクラスにまとめたりする。そういうパターンに名前があるんじゃないかと思うけどいまいち見つからない。

ネットで見つけたのはUseCase パターンというものだけどそれってイニシャライザに対して言及してるわけじゃないからなーという気持ちなんで知ってればコメントで教えて欲しいわけです。

A Case For Use Cases
https://webuild.envato.com/blog/a-case-for-use-cases/