URLs are (not) alone
少し前にこんなエントリを書いた。
要約するとこんな感じ。
- URL はリソースを一意に特定するのがプライマリな役割だが、可読性が高くて指し示すリソースの内容がわかると嬉しい
/articles/2018/03/16
より/articles/great-article
のほうが良いよねという話
- 一方で、リソースの内容がわかるようにするとキーが意味を持つことになり、変更される可能性がある
/articles/great-article
が/articles/awesome-article
に変更される可能性
リソースの管理が運営主体によって為されている場合はキーの変更リスクもある程度抑えられると思うが、UGC になると表記ゆれなどもあって変更頻度が結構高くなることが考える。
Cool URIs don't change なので URL は変わってほしくない。Web ってのはリンクなんだ。
このあたり、Wikipedia, Weblio, ニコニコ大百科, ピクシブ百科事典なんかは UGC だけど(マルチバイト文字の)タイトルを path に含めていてそれがベストプラクティス感がある。
ただ、やっぱり表記ゆれとかが怖いなーと思っていたら可読性について別の考え方が浮かんだ。
OGP に頼るという選択肢はどうだろう?
現代において、URL が貼られる場所(Twitter, Slack, Facebook, LINE, etc...)ではだいたい OGP を読み込んでタイトルや画像を展開してくれる。
URL が不変性を持ったままリソースの内容を表現できればそれに越したことはないが、内容を表現するために不変性が失われるのであれば URL に意味を乗せることはスパッと諦めて、OGP に意味を乗せれば良いのではないか。
一度この考え方でアプリケーションを作ってみようと思っている。
キー、その名状しがたきもの
URL とキーについて考えていたら、URL に含むキーについていろいろ思うところがあったので雑に dump しておく。
暗黙のうちに REST を前提にしてしまっているところはあるが、意識的に REST とは文脈を切り離して素朴に書いてみる。
重複する部分もあるが、URL 側とキー側、それぞれの観点から書いてみる。
URL に求めること
自分が URL に求めることはいくつかあるが主に以下のようなことを求めている
- URL だけで目的のページにたどり着けること
- ページの簡易的な説明になっていること
- ASCII で構成されていること
それぞれの意図するところと重要度について簡単に書く。
[MUST] URL だけで目的のページにたどり着けること
あるページにたどり着くために「URL が指し示すページに行って、次に◯◯というボタンをクリックして……」みたいなことはやりたくない。
URL を入力したら一発で目的のページにたどり着いて欲しい。
(Addressable & Stateless であってほしい)
[SHOULD] ページの簡易的な説明になっていること
例としてブログの URL に含まれる path を考えてみる。
このとき、path が /articles/1234
となっているよりは /articles/introduction-to-key
とかのほうが実際にページを見る前に内容が予測できて嬉しい。
[MAY] ASCII で構成されていること
状況によるが、可能であれば ASCII だけで構成されていると嬉しい。
ASCII 以外を URL に含めるとエンコードされて URL が長くなりがちだし、可読性も下がってしまう。
とはいえ、ASCII 以外を含めるメリットが大きいときもあるので、そういうときは無理に ASCII にはしない。
キーに求めること
URL の中で path に含まれるキーにあたる部分以外について上の条件を満たすのはわりと難しくない。キーをどうするかが悩ましい。
キーに求めることは以下のようなもの。
- リソースを一意に特定できること
- リソースを一意に特定する以外の意味を持たないこと
[MUST] リソースを一意に特定できること
これはキーの定義上必須。
ただし、path に含まれる文字列がそのまま物理的に保存されている必要性は必ずしもない。
[SHOULD] リソースを一意に特定する以外の意味を持たないこと
キーとはリソースなりレコードなりを一意に特定するものであり、それ未満でもそれ超過でもない。
仮に auto increment な primary key を作成順の判別に使っているとしたら、それはキーの目的外利用になる。そもそも、作成順は作成日時から導出できるので整数列にしてしまうということは情報を損失している。
人間は整数を見るとどうしても比較したりソートしたりしてしまうので、この観点からはキーは乱数(UUID v4 やランダム文字列)だと嬉しい。*1
対立とバランス
上に書いたものの中で真っ向から対立するものがある。
- URLはページの簡易的な説明になっていること
- キーはリソースを一意に特定する以外の意味を持たないこと
URL はページの簡易的な説明になっていてほしいけど、URL に含まれるキーはリソースを一意に特定する以外の意味を持ってほしくない。
このふたつの間でどういう風にバランスを取るかというのが良い URL/キーをつくる上で重要だと思っている。
いくつかの例を考えてみる。
例1:商品管理アプリケーション
小売店の商品を管理するアプリケーションを考えてみる。
この店舗が扱う商品すべてに JAN コード(バーコード)が付与されているなら JAN コードは商品を代表する属性として妥当なように思われる。
そこで JAN コードを path に含むキーとして採用して /products/4569951116179
のような path にするのが良いかもしれない。
例2:石コレクターのための石管理アプリケーション
そのへんで拾ってきた石を管理するアプリケーションを考えてみる。
石はこの上なく自然のものだが、石に自然キーはない。
そこで何かしらのサロゲートキーを用意してやる必要があるが、ここで auto increment な整数を使うとキーに必要以上の意味を持たせてしまうことになるため、この場合は /stones/5f498e23
のようなランダムな文字列をキーとして含む path が良いかもしれない。
例3:薄い本管理アプリケーション
薄い本を管理するアプリケーションを考える。
薄い本には ISBN のようなものはないのでこれをキーにすることはできない。*2
とはいえ、本にはタイトルというその本を代表する属性がある。ただし、タイトルは重複する可能性があるのでそのままキーにすることはできないし、多くの場合、エンコーディングが必要な文字を含んでいるのでそのままだと path に含めて扱いにくい。そこで、本を代表する属性であるタイトルを元に新たにキーを考える。
たとえば /books/sekaiichi-kawaii-boku
のような感じ。
キーの選び方
例1で触れたように、リソースを代表する属性がありそれがキーである場合、代表する属性を path に含めるキーにするのが良い気がする。
例2で触れたように、リソースを代表する属性がない場合、人工的で意味を見出しにくいキーを path に含めるのが良い気がする。
例3で触れたように、リソースを代表する属性はあるがそれがキーではない場合、代表する属性を元に人工的で意味のあるキーを path に含めるのが良い気がする。
例1は自然キーを使った path、例2は意味のない人工キーを使った path になっていて対照的。
例3はそれらの中間でリソースを代表する属性から意味のある人工キーを作り出している。いわば、半自然キーとでも言うべきものになっている。
おわりに
URL を考えるだけでもこんなに苦労するのでプログラミングはむずかしい。
書きながらキーについて思うところが増えてきたので、近いうちにまた何か書くかもしれない。
Ruby で 1 < 2 < 3 みたいな比較演算を書けるようにする試み
TL;DR
華麗に失敗した。
発端
こんなモチベーションなので実用は目的にしていない。
何番煎じかもわからないけど、考えてみたかったので考えてみた。
問題をシンプルにする
仕様を絞る。
- 対象クラスを絞って
Integer
だけにする - 境界値を含まない場合だけを考える(
<
,>
だけ) ()
による演算の優先順位変更とかも考えない
アイデア
「左から順番に比較して、その結果を boolean じゃなく以前の 比較結果を保持するいい感じのオブジェクト で返せば良いのでは?」
左から順番に比較していくだけなら、
- 前回の比較が
true
だったのかどうか - 前回の比較における右辺
が取れればいけるだろうという感じ。
初めに考えていた方針
Comparable
をいい感じに魔改造すればいけそう- いい感じのオブジェクト は
TrueClass
,FalseClass
あたりを継承すれば良さそう
とぼんやり考えていたところ、
Comparable
を変更してもInteger
の挙動が変わらないTrueClass
,FalseClass
が Singleton なので継承するとnew
できない
あたりの問題が起こったので、Comparable
じゃなく Integer
を直接触るようにしつつ、比較結果オブジェクトをそのまま条件式で使えるようにするのは諦めて to_b
を呼んで boolean に変換するアプローチに変更。
実装
比較結果を保持するオブジェクトComparisonResult::TrueClass
, ComparisonResult::FalseClass
を作って、Integer#<
, Integer#>
がそれを返すように変更。
コードとしてはこんな感じ。
require 'singleton' module ComparisonResult class TrueClass def initialize(left, right) @left = left @right = right end def to_b true end def <(other) @right < other end def >(other) @right > other end end class FalseClass include Singleton def to_b false end def <(_) self end def >(_) self end end end class Integer def <(other) if (self <=> other) == -1 ComparisonResult::TrueClass.new(self, other) else ComparisonResult::FalseClass.instance end end def >(other) if (self <=> other) == 1 ComparisonResult::TrueClass.new(self, other) else ComparisonResult::FalseClass.instance end end end # < raise '1 < 2' unless (1 < 2).to_b == true raise '2 < 1' unless (2 < 1).to_b == false raise '1 < 2 < 3' unless (1 < 2 < 3).to_b == true raise '1 < 3 < 2' unless (1 < 3 < 2).to_b == false raise '2 < 1 < 3' unless (2 < 1 < 3).to_b == false raise '2 < 3 < 1' unless (2 < 3 < 1).to_b == false raise '3 < 1 < 2' unless (3 < 1 < 2).to_b == false raise '3 < 2 < 1' unless (3 < 2 < 1).to_b == false # > raise '1 > 2' unless (1 > 2).to_b == false raise '2 > 1' unless (2 > 1).to_b == true raise '1 > 2 > 3' unless (1 > 2 > 3).to_b == false raise '1 > 3 > 2' unless (1 > 3 > 2).to_b == false raise '2 > 1 > 3' unless (2 > 1 > 3).to_b == false raise '2 > 3 > 1' unless (2 > 3 > 1).to_b == false raise '3 > 1 > 2' unless (3 > 1 > 2).to_b == false raise '3 > 2 > 1' unless (3 > 2 > 1).to_b == true
感想
一応動く何かはできたけど、思っていたことは全然できなかった。
対象クラス・境界値・優先順位あたりはゴリゴリやればいけるはずだけど、 to_b
が生えているのが「これはない……」という感じ。