今日も今日とてドメインをモデリングしているみなさん、こんにちは。
今回は別にドメインモデリングの話をしたいわけではなくて、そんな話ができるような知識ベースが私の中にあるわけでもないのですが、それに近い何かだとは思います(?)
とある現場にて
「このシステムのドキュメントってありますか?」
「そんなのないよ。なんで?」
「管理者と一般ユーザーの権限の違いを把握したくて」
「コード読めば分かるでしょ」
「あっはい分かりましたありがとうございますお邪魔しました」
言いたいことは色々あるでしょうが、そこはぐっと堪えていただいて。
コードを読めば分かるとのことなので、さっそく目を通してみましょう。
if ($user['role'] === 'admin') {
/* 管理者限定の処理 */
}
どこか懐かしい気持ちになるコードですね。
'admin'
をハードコーディングしているのが良くないというのは分かると思います($user
が配列なのもポイント高いですけどね)。
このままでは修正が面倒なので定数を作ることにしましょう。
define('ADMIN', 'admin');
if ($user['role'] === ADMIN) {
/* 管理者限定の処理 */
}
これがダメという話ではありません。変更すべき箇所が減るのは良いことです。システムの規模によってはこれで充分という場合も少なくはないでしょう。
しかし、これは私たちが真に恐れるべき本質的な複雑さを解決するものではないのです。
ユーザーの役割と権限を分けて考える
ここが一番重要なのですが、ユーザーが持つ役割と、役割ごとに与えられる権限は別物です。本来異なるはずの概念を混同してしまうと、モデリングされなかった概念は揮発してシステム全体に霧散してしまいます。
それの何がよろしくないのか? 「このシステムにおいてroleがadminなユーザーは何ができるのか」という、roleごとの権限を把握することが非常に難しくなり、冒頭の会話劇のような状況に陥ってしまうのです。
それを避けるため、役割と権限を分けた実装を試みることにします。
とりあえず役割だけをenumで表してみましょう。
enum UserRole: string {
/** 管理者 */
case Admin = 'admin';
/** 一般ユーザー */
case Normal = 'normal';
}
ではこれらの役割に対して、権限とはどういう概念なのでしょうか?
こちらもenumで表現してみましょう。
// ただのマーカー
interface UserPermissionInterface {
}
// 権限を表現するEnum
enum UserPermission implements UserPermissionInterface {
/** 閲覧権限 */
case Show;
/** 編集権限 */
case Edit;
}
roleが特定の権限を有しているか判定するメソッドを、先程のUserRole
に追加します。
enum UserRole: string {
/** 管理者 */
case Admin = 'admin';
/** 一般ユーザー */
case Normal = 'normal';
/** roleが特定の権限を有しているか判定するメソッド */
public function hasPermission(UserPermissionInterface $permission): bool {
$permissions = match ($this) {
// Adminは閲覧と編集の権限を持っている
self::Admin => [
UserPermission::Show,
UserPermission::Edit,
],
// Normalは閲覧権限のみ持っている
self::Normal => [
UserPermission::Show,
],
};
return in_array($permission, $permissions);
}
}
そうするとユーザーの権限によって処理を分ける記述はこのように書けます。
if ($user['role']->hasPermission(UserPermission::Edit)) {
/* 編集に関する処理 */
}
はてさて、これでいったい何がどう良くなったんでしょうか?
まず、UserRole
とUserPermission
を作成したことでファイルが増えたので、そのぶん複雑化しました。条件式も長くなって読みにくいです。ついでにenumだとかmatchだとか、よく分からない新しい機能をすぐに使いたがるのも気に食わないですね。
一方、UserRole.php
を見ればroleごとの権限が一目瞭然になるというメリットがあります。まあそもそもそれをメリットだと捉えるかどうか、という話ですね。「散らばっていないと分かりにくいじゃないか!」ということであれば、そうしたほうがよいでしょう。
まとめ
改良できそうなところは多々ありますが、目的は達したのでこれでよしとします。1
改良前と改良後の条件式をもう一度比べてみてほしいのですが、はじめは「ユーザーの役割がadminか」という条件だったのが、「ユーザーが編集権限を持っているか」という条件に変わっているのが分かるでしょうか?
// 改良前
if ($user['role'] === ADMIN) {
/* 管理者限定の処理 */
}
// 改良後
if ($user['role']->hasPermission(UserPermission::Edit)) {
/* 編集に関する処理 */
}
本来重要なのは「ユーザーの役割が何であるか」ではなく「ユーザーが特定の権限を有しているか」だったのですが、改良前はそのことを発見できていなかったわけですね。
今回は比較的単純な例で説明させてもらいましたが、表面的な分類に目を奪われて重要な概念を見落としていないか考えることは、コードの見通しをよくするうえで非常に重要です。
忌憚なくマサカリいただければ幸いです。それでは!
-
UserPermissionは商品管理やユーザー管理などの業務ドメインごとに分けたほうがよさそうですが、あとから気軽にリファクタできる範疇だと思います。 ↩