のほほん停留所

びぼうろく

Swiftで複数フラグの管理にOptionSetを使うと便利だった

Swiftで複数のフラグを管理するためにOptionSetを使うと便利だった サービスを作っているとき、ユーザーの状態(e.g. 課金)によって特定の機能が解放されていることがあります。それを管理するにはフラグのような仕組みを使って管理するのがよくある手段なのですが、それが複数になってしまうと管理のコストが高くなってしまいます。その複数のフラグを管理するためにOptionSetを使うと便利でした。

Boolによるフラグ管理

struct Permission {
    let isAllownA: Bool // Aという機能が開放されているかのフラグ
    let isAllownB: Bool // Bという機能が開放されているかのフラグ
    let isAllownC: Bool // Cという機能が開放されているかのフラグ
    let isAllownD: Bool // Dという機能が開放されているかのフラグ
    let isAllownE: Bool // Eという機能が開放されているかのフラグ
    let isAllownF: Bool // Fという機能が開放されているかのフラグ
}

管理するフラグが多くなってくるとツライですね…

OptionSetを用いたフラグ管理

struct Permission: OptionSet {
    let rawValue: Int

    static let A = Permission(rawValue: 1 << 0)
    static let B = Permission(rawValue: 1 << 1)
    static let C = Permission(rawValue: 1 << 2)
    static let D = Permission(rawValue: 1 << 3)
    static let E = Permission(rawValue: 1 << 4)
    static let F = Permission(rawValue: 1 << 5)

    init(rawValue: Int) {
        self.rawValue = rawValue
    }

    init(rawValues: [Int]) {
        self.rawValue = rawValues.reduce(0) { $0 + (1 << $1) }
    }
}

今回のOptionSetを用いたフラグの管理はビット列を用いています。各機能のフラグは各ビットのフラグが立っているか見れば分かります。Aという機能が開放されているかどうかは1ビット目を見れば分かり、同様にBという機能が開放されているかは2ビット目を見れば分かります。C ~ Fも同様です。

以下のように、[1, 3, 5]というArray<Int>はB, D, Fという機能が開放されていることを示しており、それをPermissionのコンストラクタで与えています。生成されたPermissionの.contains(member: Permission)メソッドに知りたい機能を与えてあげるとビットが立っているかどうかを返してくれます。

// 101010みたいなイメージ
let permission = Permission(rawValue: [1, 3, 5])
permission.contains(.A) // false
permission.contains(.B) // true
permission.contains(.C) // false
permission.contains(.D) // true
permission.contains(.E) // false
permission.contains(.F) // true

OptionSetによるフラグ管理の良い点

同値判定が楽に書ける

例えば、ユーザーがある行動をした際に特定の機能が開放されたかどうかのテストを書くとき権限の同値判定を行いたいケースを考えます。その際にPermissionオブジェクト同士を比較する際に使う==()の処理がOptionSetを用いたPermissionでは簡単に書けます。

// Boolを用いたフラグ管理のEquatable
extension Permission: Equatable {
    static func == (lhs: Permission, rhs: Permission) -> Bool {
        return lhs.isAllowedA == rhs.isAllowedA
            && lhs.isAllowedB == rhs.isAllowedB
            && lhs.isAllowedC == rhs.isAllowedC
            && lhs.isAllowedD == rhs.isAllowedD
            && lhs.isAllowedE == rhs.isAllowedE
            && lhs.isAllowedF == rhs.isAllowedF
    }
}

// OptionSetを用いたフラグ管理のEquatable
extension Permission: Equatable {
    static func == (lhs: Permission, rhs: Permission) -> Bool {
        return lhs.rawValue == rhs.rawValue
    }
}

Boolを用いたフラグ管理の同値判定はそれぞれのプロパティの等式を比較して、それのAND条件を評価します。プロパティがすべてBoolなので同じプロパティ同士を比べているかどうかは型的には解決できず、新しいフラグが追加されたときは==()の処理を更新しなければなりません。  それに比べて、OptionSetを用いたフラグ管理の同値判定はrawValueの等式を評価すればよいだけで、これは管理するべきフラグが増えた際も常に同様です。

OptionSetによるフラグ管理の悪い点

Permissionへのマッピングが大変

クライアント内でフラグ管理が完結しているとそこまで問題はないと思うのですが、APIのレスポンスなどでフラグの状態を取得する際はその値をPermissionにオブジェクトにマッピングするのが大変です。 以下のようなレスポンスで返ってくると、結局Aがtrueなら1, Bがtrueなら2のようなマッピングをしなくてはなりません。これは結局、複数の方法で管理するのでそのマッピング部分のコストが高くなってしまいます。

{
    "A": true,
    "B": false,
    "C": true,
    "D": false,
    "E": true,
    "F": true
}

APIのレスポンスがArray<Int>のような形であればOptionSetを用いたフラグ管理を考えてみる価値はあると思います。

[
    0,
    2,
    4,
    5
]

参考

https://developer.apple.com/reference/swift/optionset