Iganinのブログ

日頃の開発で学んだ知見を中心に記事を書いています。

【iOS】RawReperesentableを使用してID間の使用ミスを避ける方法

はじめに

アプリを作成しているとクラスやStructの一意性の判別のためにidを良く使用します。 例えばUserを定義した場合、その一意性を決めるためにUser.idを定義します。 多くの場合はidはStringかIntで定義するかと思います。

idが一つだけならば良いのですが、 jobIdやgroupIdなど複数のIDを扱うようになると、 jobIdとgroupIdにString型を使用していた場合、それらのIdの入れ間違いが発生し得ます。 Idは一意性を担保するため、この間違いは大きな障害に繋がりかねません。

本記事ではこのような問題を発生させない方法を記載します。

環境設定

以下の環境を使用しています。

  • Xcode10.2.1
  • Swift 5.0.1

方法

import Foundation

typealias Identifiable = RawRepresentable & Codable & Equatable & Hashable

struct User: Codable {
    let id: ID
    let name: String
    let job: Job
    
    struct ID: Identifiable {
        typealias RawValue = String
        let rawValue: RawValue
    }
}

struct Job: Codable {
    let id: ID
    let name: String
    
    struct ID: Identifiable {
        typealias RawValue = String
        let rawValue: RawValue
    }
}

let userString = """
{
    "id": "11111",
    "name": "Tanaka",
    "job": {
        "id": "01",
        "name": "engineer"
    },
}
"""

let userData = userString.data(using: .utf8)!


let user: User
do {
    user = try JSONDecoder().decode(User.self, from: userData)
    print("User: \(user)")
    print("UserID: \(user.id.rawValue)")
    print("UserJobID: \(user.job.id.rawValue)")
} catch let error {
    fatalError(error.localizedDescription)
}

do {
    let encodedUserData = try JSONEncoder().encode(user)
    let encodedUserString = String(data: encodedUserData, encoding: .utf8)!
    print(encodedUserString)
} catch let error {
    fatalError(error.localizedDescription)
}

上記のようにすることで、 UserのidはUser.ID型、JobのidはJob.ID型となり別々の型となるためIDの間違いがなくなります。

IDをRawReperesentableに準拠させることで、文字列から直接User.ID型にDecodeしたり、User.IDから文字列としてのEncodeが可能になります。 また、ID型をEquatableとHashableに準拠させることで、ID自体の同値判定やDictionaryのkeyとしてID自体を使用することを可能にしています。

IDについては各Struct内に定義するのが良いかと思いますが、 IDとしてStringしか使用しないなどの取り決めがある場合は以下のようにすると定義回数が減らせるのでいいかもしれません。

struct ID<T>: Identifiable {
    let rawValue: String
}

struct User: Codable {
    id: ID<User>
}

またRawRepresentableに準拠する際には以下の書き方もできます。

struct ID: RawRepresentable {
    let rawValue: String
}

RawRepresentableについて

RawRepresentableに関して知識が曖昧だったため学習がてらまとめてみます。

  • rawValueとして指定された型へ、もしくは型から変換できる。
  • Enumの型としてString, Int, 不要小数点型(CGFloat, Floatなど)を指定した場合はCompilerが自動的にRawRepresentableを付与する。
  • OptionSet ProtocolはRawRepresentable Protocolに準拠している。

まとめ

IDの間違いは致命的なバグに繋がり得るため慎重な扱いが必要です。 IDを定義する際の選択肢として本記事の方法がお役に立てば幸いです。

参考