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 ]