モジュールが「知っている範囲」「知らない範囲」を厳格にデザインしたほうが良い
ここで、「モジュール」とは、関数・クラス・ライブラリ・マイクロサービス等、あらゆる粒度のものを指します。
よくない例
例えば、以下のリポジトリレイヤーのバックエンドコードを見てください。
async function updateArticle(
me: User,
id: ArticleId,
content: Content,
) {
if (me.role !== "admin") {
const existing = await orm.getArticle(id)
if (existing.userId !== me.id) {
return {
ok: false,
errorMessage: "non-admins can only update their own articles",
}
}
}
await orm.updateArticle({ id, content })
return { ok: true }
}
関心の分離という意味で、あんまり良くないコードです。
どんなロールの人が何を更新できるべきかというのはユースケース(ドメイン)レベルの関心事であり、リポジトリに漏洩すべきではありません。[1]
よい例
このような場合、あくまで例えばですが、こういう書き方が望ましいと思います。
async function updateArticle(
id: ArticleId,
content: Content,
updatableCheck?: (existing: Article) => boolean,
) {
if (updatableCheck != null) {
const existing = await orm.getArticle(id)
if (!updatableCheck(existing)) {
return {
ok: false,
errorMessage: "updatable check failed for the existing article",
}
}
}
await orm.updateArticle({ id, content })
return { ok: true }
}
// 呼び出し側
async function callerFunction(me: User) {
// ...
const updatableCheck = me.role === "admin"
? undefined
: (existing: Article) => existing.userId === me.id
const result = await updateArticle(articleId, articleContent, updatableCheck)
// ...
}
なんでここまでするの
どうして、こうまでして「リポジトリレイヤーはユーザーのことを忘れなければならない」のでしょう?ややこしくしているだけでは?
理由はあります。こうしないと抽象化がなされず、スケールできないからです。
冒頭のよくない実装方法にした場合・・・
例えば、どこかで要件が増えて、人間のユーザーに紐づかないやり方で記事を更新するAPIを実装することになったら?このケースでは、渡すべきme
なんてのは存在しないから、この引数をオプショナルにする・・・?
または、同じadminでもユーザー画面から来た場合と管理画面から来た場合で挙動を変えたくなったら?kickedFromScreen
なんていう引数を追加して、判定を行うようにする・・・?
要件が増えるたびに、リポジトリレイヤーに埋め込まれたドメイン知識がどんどん膨れ上がります。
そして、あるとき、単に、無条件にちょっと記事を更新する処理を書きたくなった人が、あまりの複雑さに混乱し、仕方なく別のシンプルな関数を書いてそっちを使うことになります。そうして、カオスが広がります。
さいごに
もちろん、この話は、リポジトリに限った話ではありません。
将来的に複数箇所から呼ばれる可能性のあるコード(ほとんどあらゆるコード)は、外部の知識をなぜか知っている状態にしてはいけません。なぜなら、呼び出し元が増えるたびに、本来互いに無関係であるはずの、あらゆる呼び出し元の外部の知識が埋め込まれていって、スパゲッティが完成するからです。
ちなみに、逆も然りで、呼び出し元は呼び出し先の内部実装の知識をなぜか持っている状態にしてはいけません。この場合は、内部実装を二度と変更できなくなるという形で、苦しみが生まれます。
実際どんな実装が望ましいか(各モジュールの関心範囲がどの範囲であるべきか)はケースバイケースですが、とにかく考え方自体はとても大事だと私は思っています。
大事なんですけど、最初は理解するのが難しいことなので、こういう記事にしてみました。お役に立てれば嬉しいです。
分かりづらかったりしたらフィードバックください!
-
厳密には、このコードベースの「リポジトリ」の意味にもよりますし、Row Level SecurityなどでDBレイヤーにuserIdへの関心を持たせている場合は変わってくるとは思いますが、シンプルなパターンの話として捉えてください。 ↩︎
Discussion