🤖

ビット演算を使ったUI表示判定実践 with React

2022/10/15に公開

ビット演算を使ったフラグ判定方法についてフロントエンドでの実装例を示しながら説明します。プロダクト開発にありがちな権限やオプションプランによる組み合わせ爆発問題を意識しており、発展的な内容になっています。

ビット演算とは

データを二進数(0 と 1 の羅列)とみなして、ビットの移動やビット単位の論理演算などを行うものです[1]。この記事は実践記事のため詳細説明を省きますが、以下の記事がとても分かりやすかったです。

https://qiita.com/drken/items/7c6ff2aa4d8fce1c9361

実装例

ユーザーに設定された権限と機能によりメニュー内容を制御する UI を、ビット演算を使って開発していきます。

ユーザーが属性として管理者等の権限を持っており、さらに FeatureToggle やオプションプランによって利用できる機能がユーザーごとに制限されているサービスを想定しています。

最終的な実装

ユーザーの権限と、機能の ON / OFF の組み合わせによって表示内容が変化します。

こちらを元に解説していきます。

前提条件

権限

権限は以下の3種類です。4 ビットの二進数で設定します。

権限 二進数 シフト演算子
管理者 0001 1 << 0
編集者 0010 1 << 1
閲覧者 0100 1 << 2
シフト演算子列について

シフト演算子という列を設けていますが、これは二進数を表現するのに便利な方法です。以下のように対応しています。

// コメントは対応する10進数
0b0001 === 1 << 0 // 1
0b0010 === 1 << 1 // 2
0b0100 === 1 << 2 // 4

JS 及び TS であれば二進数を 0b0001 のように表現できますが、シフト演算子を用いると何桁目のフラグが有効になっているか分かりやすいので個人的にはこちらを推しています。

管理者もしくは編集者に許可するという権限は以下のように表すことが出来ます。

0b0011 // 管理者(1ビット目)と編集者(2ビット目)が有効になっている

ここで本実装例の仕様として、ユーザーは複数の権限を持たないことを付け加えます。つまり管理者と編集者が有効(0b0011)という表現はありますが、ユーザーに設定できるのはフラグが 1 ビットのみ上がっている状態(0b0010など)ということです。管理者かつ編集者とはならないことに注意してください。

機能

機能については以下です。こちらもユーザーに紐付いていることを想定しています。機能が増加することを見越して 8 ビットとしておきましょう。

機能 二進数 シフト演算子 概要
ダッシュボード - - 標準機能
テナント設定 - - 標準機能
管理者・編集者が設定画面にアクセスできる
メール通知 00000001 1 << 0 管理者・編集者が設定画面にアクセスできる
統計画面 00000010 1 << 1 管理者・編集者が設定画面にアクセスできる
多段階認証 00000100 1 << 2 管理者・編集者が設定画面にアクセスできる

権限と異なり、ユーザーは複数の機能を使えることに注意してください。

0100 // OK: 閲覧者ユーザー
0101 // NG: 管理者かつ閲覧者というユーザーは存在しない
00000111 // OK: メール通知, 統計画面, 多段階認証が利用できるユーザーは存在する

この性質の違いにより、後述する判定方法が異なります。

実装

ここからは実装していきます。

定義

前述した権限と機能を実装していきます。

// 権限
const NUMBER_OF_ROLE_DIGITS = 4; // 桁数
const ROLE = {
  ADMINISTRATOR: 1 << 0, // 管理者
  EDITOR: 1 << 1, // 編集者
  VIEWER: 1 << 2, // 閲覧者
} as const;

// 機能
const NUMBER_OF_FEATURE_DIGITS = 8; // 桁数
const FEATURE = {
  MAIL_NOTIFICATION: 1 << 0, // メール通知
  STATICTICS: 1 << 1, // 統計機能
  MULTI_AUTHENTICATION: 1 << 2, // 多段階認証
} as const;

次はこの設定を使って複雑な権限を表現していきます。

表現方法

前述した「管理者と編集者が有効(0b0011)」という表現を作る関数mergeRoleFlugを用意します。内部では|で論理和を計算しています。

const mergeRoleFlug = (...roles: number[]) => roles.reduce((sum, role) => sum | role, 0);

// 編集者以上の権限を表現する
const editorOrMore = mergeRoleFlug(ROLE.ADMINISTRATOR, ROLE.EDITOR); // 1 | 2 = 3 (0011)

これで複数の権限の表現が分かりました。機能も同じように実装することが出来ます(省略)。

判定方法

ユーザーが管理者もしくは編集者の場合にtrueを返す関数canUseEditorOrMoreを実装します。

// クロージャーで実装
const canUseRole = (mask: number) => (role: number) => Boolean(mask & role)

const canUseEditorOrMore = canUseRole(editorOrMore)

canUseEditorOrMore(0b0001) // 管理者: true
canUseEditorOrMore(0b0010) // 編集者: true
canUseEditorOrMore(0b0100) // 閲覧者: false

論理積を使うことで、mask と与えられた role の共通のフラグを判定できます。

次は機能の判定を実装してみましょう。ユーザーがメール通知を利用できる場合にtrueを返す関数canUseMailNotificationは以下のように実装します。

// クロージャーで実装
const canUseFeature = (mask: number) => (feature: number) => (mask & feature) === mask

const canUseMailNotification = canUseFeature(FEATURE.MAIL_NOTIFICATION)

canUseMailNotification(0b00000001) // メール通知: true
canUseMailNotification(0b00000011) // メール通知と統計: true
canUseMailNotification(0b00000110) // 統計と多段階認証: false

mask と与えられた feature の論理積を算出後、mask と等価であることをみることで判定することが出来ます。権限の算出方法と異なるので注意が必要です。

これらを組み合わせることで以下のような表現が可能になります。

// 管理者及び編集者かつ、メール機能が利用できるユーザー
canUseEditorOrMore(user.role) && canUseMailNotification(user.feature)

あとはこれまで作ってきた関数を組み合わせて仕様に合った画面を実装します。
前述した最終的な実装を参照してください。

さらに一工夫

紹介した関数の引数はすべてnumber型となっていました。関数canUseRoleの引数にFeatureのフラグを与えることが出来てしまうので、専用型を使うのが良いです。過去記事で紹介している Opaque 型で比較的楽に実装できます。

https://zenn.dev/mssknd/articles/da2ba877a63310

// この例の型は1つですが、ユーザーに設定できる権限(0b0010など)と、権限の組み合わせ(0b0011など)を表現する型を分けるのも良い
type RoleFlug = Opaque<"RoleFlug", number>;

const validateRoleFlug = (flug: number) => {
  // 0 ~ 15 の値であること、などの生成条件を書く
  if (flug >= 0 && flug < (NUMBER_OF_ROLE_DIGITS ^ 2))
    return flug as RoleFlug;
  throw new Error();
};

// validateRoleFlug の出力は RoleFlug 型になる
const ROLE = {
  ADMINISTRATOR: validateRoleFlug(1 << 0), // 0001
  EDITOR: validateRoleFlug(1 << 1), // 0010
  VIEWER: validateRoleFlug(1 << 2) // 0100
} as const;

// 型を制限出来るようになる
const canUseRole = (mask: RoleFlug) => (role: RoleFlug) => Boolean(mask & role);

まとめ

ビット演算を用いたフラグ判定について実装例を交えて解説しました。

プロダクトが大きくなってくると権限が増えたり、オプションプランが増えたりするのはよくあることです。気にせずいるといつの間にか組み合わせが爆発してしまい、複雑な if 文ネストの山が生まれるんですよね。今回の例で説明した 3 権限 3 機能の組み合わせでも 3 \times 2 ^ 3 = 24 通りあります。それぞれ倍の 6 権限 6 機能になると 6 \times 2 ^ 6 = 384 通りになります。網羅的テストなども必要になってくると辛いところがありますね(権限周りだとよくある)。ビット演算を使ったフラグを使うと組み合わせ爆発を比較的簡潔に表現できるため有用だと思っています。

ビット演算にはもともと苦手意識があったんですが、こうやって実装してみると理解が進んで面白い。

脚注
  1. ビット演算とは - 意味をわかりやすく - IT用語辞典 e-Words ↩︎

GitHubで編集を提案

Discussion