💴

新紙幣をきっかけに、ユーザーフレンドリーなID(識別子)を考えてみる

2024/08/23に公開

マイベストのプロダクト開発部(UXチーム/ポイントユニット)でバックエンドエンジニアをしているゆーだいです。
マイベストテックブログ連載:「みんなでお買い物サポートクラブ」リリースまでの開発の裏側の2日目を担当します。

この記事で書くこと

エンジニアなら普段ID(識別子)を使ったり、見たりすることが多いと思います。
中でもUUIDは、よく使用されており、「聞いたことあるIDランキング」があれば、必ずトップ3に入るでしょう。🥇

そんなIDですが、ユーザーの目に直接触れる機会は少ないものだと私は考えます。
今回は、IDがユーザーの目に触れる場所に登場し、問い合わせなどの際はそのIDを用いる。
そのように使われるIDの場合、どのようなものを使うのが良いのか?ということについて考えました。

具体的にどんなケースで使用するIDなのか

みんなでお買い物サポートクラブでは、クチコミ投稿やアンケート回答で貯めたポイントを現金に交換することができます。

その交換を申請する際に、使用する「申請ID」が今回の主役になります。
この「申請ID」は主に、以下の画像にある2箇所でユーザーの目に触れることになります。

表側の制約

M8rLhr7JEpJlqFGUMmOxg==みたいなIDはユーザーフレンドリーじゃない。」というメモがこのタスクを依頼された時に書かれていました。

「ユーザーフレンドリーではないID」
まずは、これがどういうことなのかを考えます。

もし、ユーザーが交換申請を進める中で困ったことがあった場合は、お問い合わせの際に、この「申請ID」を記載してもらい、それを用いて調査することになります。

ユーザーに見える&使ってもらう可能性がある文字列という観点から、以下の2つの要素に分解しました。

  • 一般のユーザーが「976a9097-0252-4afa-b517-f8ee400e99cb」みたいな文字列を見た時に、少なからず「なんだこの長い文字列?」みたいに感じる人がいるのではないだろうか。
  • 記号が入っている場合、1つのまとまりの文字列として判定されず、コピーしにくい。

いずれも、とても細かいことだと思います。
ただ、入力することを考えた時に、長いよりは短い方が良いし、コピーしづらいよりはしやすい方が良いですよね。

「コピーボタンを設置すれば良いのでは?」と思う方もいるかもしれませんが、サービス内で申請IDが表示されるのは、交換申請時のみで、申請IDが欲しい時は、ほとんどのユーザーはメール本文から、コピーすることになります。
そのため、コピーボタンを置くという対策は、あまり意味のない対策になってしまいます。

裏側の制約

ユーザーに見える部分の話もありますが、裏側の仕様上の制約も存在します。

みんなでお買い物サポートクラブでは、自社で付与したポイントを現金化し、その現金を楽天銀行のかんたん振込(メルマネ)を利用して送金しています
メルマネのシステムを利用する際に、ID(識別子)が必要になるため、そこに「申請ID」を使用しています。
メルマネの仕様として、そのIDは「20文字以内の半角英数字」である必要があります。

また、お金関係の話であることから、今回のIDの特性上、「推測できない値」であることが望ましいです。

今回作成したいID(識別子)の制約

以上の表側と裏側の制約をまとめると、以下のようになります。

  • 20文字以内の半角英数字で構成されていること。
  • 推測ができないこと。
  • コピペや入力がなるべく行いやすいこと。

また、交換申請が行われた際に、生成するIDなので、超高速で生成される。みたいな必要があまりないことも特徴のひとつです。

普段使っているサービスでは、どのような値が使われているのか

作成したいIDの条件が整ったので、世の中にはどのようなIDがあるのか、普段自分が使っているサービスでIDが目に触れるものがないか調査を行いました。

その際に参考になった記事を2つ紹介しておきます。
ただ、ここで紹介されているものは「20文字以内の英数字」の制約を満たしていないものばかりでした。
https://qiita.com/kawasima/items/6b0f47a60c9cb5ffb5c4
https://qiita.com/okm-uv/items/0b84c75e7524640dab11

そのため、次に自分が使っているサービスのIDについて考えました。

最初に浮かんだのは、PayPayでした。

ミスドを食べた時の支払い履歴があったので、それを見に行きました。(いつどこでミスドを食べていたかバレても全然良いけど、一応隠しておきます🍩)
PayPayでは、支払いごとに取引番号を確認することができ、20桁の数字で構成されていました。
余談ですが、コピペボタンが設置されており、ユーザーに優しいUIになっていました。
そして、本当に余談ですが、ミスドではハニーチュロ推しです。

他にも、Gitでコミットを行った際に生成されるコミットIDやクレジットカードの番号や郵便番号、口座番号などなどについても考えてみました。

紙幣(お金)のIDが参考になりそう??

さまざまな識別子を見ていく中で、ふと新紙幣についてのニュースを思い出しました。💰
「紙幣の札番号(画像の水色枠部分)がゾロ目のものがマニアの中で高値で取引されるので、新しくなった今がゲットするチャンス!」みたいなやつです。

「そういえば、超身近なお金にも識別子が存在しているな〜」とか思いながら、札番号の説明が書いてあるページを訪れました。

2024年(令和6年)7月3日に発行が開始された銀行券の記番号は、6桁のアラビア数字をはさんで、アルファベットが頭と末尾に2文字組み合わされ、「AA123456BB」や「CD777777EF」というように表されます。アルファベットは全部で26文字ありますが、I(アイ)とO(オー)は数字の1、0に間違いやすいため、これらを除く24文字が使われています。また、数字は「000001」から「900000」までの90万通りが使われています。これらの組み合わせにより、記番号は2,985億 9,840万枚で一巡します。

「ふむふむ、アルファベット4文字と数字6文字で構成されているのか〜」と読んでいると、IとOが除かれていることが記載されていました。

アルファベットは全部で26文字ありますが、I(アイ)とO(オー)は数字の1、0に間違いやすいため、これらを除く24文字が使われています。

「おお!これは、ユーザーフレンドリー!!」

たしかに、小文字のlや大文字のI、Oなどは数字と読み間違えることが多いので、納得感がありました。
札番号は20文字以内の英数字という制約も満たしており、コピペもしやすく、生成の方法をランダムにすれば、推測されることもありません。
そして、アルファベット4文字と数字6文字を使うと、3,300億以上のパターンを表現することができます。

実装

ということで、紙幣の札番号を参考に、「申請ID」を作成することにしました。
実装は非常にシンプルで、以下のようになります。

ポイント交換の情報を管理するモデル(Point::Exchange)で、「申請ID」を取得するため、Concernとして定義しています。
メソッド自体は至ってシンプルで、アルファベットの桁数と数字の桁数をgenerate_identifierに渡すことによって、識別子を生成してくれます。

models/concerns/user_friendly_identifier_generatable.rb
module UserFriendlyIdentifierGeneratable
  extend ActiveSupport::Concern

  included do
    def generate_identifier(alphabet_num, numerical_num)
      "#{generate_alphabet(alphabet_num)}#{generate_numerical(numerical_num)}"
    end

    def generate_alphabet(alphabet_num)
      # IとOは1と0に見えるため、除外
      alphabets = ('A'..'Z').to_a - ['I', 'O']
      Array.new(alphabet_num) { alphabets.sample }.join
    end

    def generate_numerical(numerical_num)
      Array.new(numerical_num) { rand(0..9) }.join
    end
  end
end

Point::Exchangeモデル側で、UserFriendlyIdentifierGeneratableをincludeすることでgenerate_identifierメソッドを使用可能にします。

モデル側には、set_identifierメソッドを用意し、万が一、いや億が一、重複が起こらないようにするため、リトライ処理を加えてあげることで、完成です。

models/point/exchange.rb
module Point
  class Exchange < ApplicationRecord
    include UserFriendlyIdentifierGeneratable
    ...

    def set_identifier
      return if identifier.present?

      tries = 0
      loop do
        raise 'Failed to generate identifier' if tries > MAX_TRIES

        self.identifier = generate_identifier(4, 6)
        break unless self.class.exists?(identifier: identifier)

        tries += 1
      end
    end
...
end

仮に、1000万回交換申請が行われたとして、IDが1発で衝突する可能性は0.003%くらいです。
MAX_TRIESを仮に10に設定したとしたら、約5.9×10^-46になるので、このset_identifierメソッドがエラーを吐くことはなさそうですね。

ちなみに、現時点では、500円から交換申請を行うことができるので、1,000万回交換申請が行われているということは50億円分のポイントが交換されたことになります😇

おわりに

今回は、ユーザーフレンドリーなIDの生成について、些細なタスクから、少し飛躍させて考えてみました。
実際のところ、「英数字を組み合わせた文字列をIDに使う。」ということは、他のサービスや札番号を参考にせずとも、思いつくことができると思います。
しかし、ただなんとなく「こんな感じが良さそう」という意見ではなく、ちょっとしたソースや根拠があると、提案を受け入れる側も「それで行こう!」と言いやすいのではないでしょうか?

みんなでお買い物サポートクラブがたくさんのユーザーに使用され、申請IDがコピペされる時がくれば良いな!と思います!

まあでも、申請IDがコピペされる時って、お問い合わせが発生してる時だから、コピペされることがない方がいいのか!!!

おわり

Discussion