概要
- 自分が作成するアプリの中で、ここはポリシーパターンでやった方が良いと感じる場所があったので、改めて学び直したい
- 今回は、ドメイン駆動設計(DDD)におけるポリシーパターンについて、TypeScriptとPHPでの実装例を交えながら解説し、学びを深めようと思う
はじめにポリシーパターンとは何か
ポリシーパターンは、複雑なビジネスルールをわかりやすく表現し、コードの可読性と保守性を高めるためのデザインパターンです。
つまりは複雑だが重要な処理をわかりやすく書くためのデザインパターンである
ポリシーパターンを使うメリット
ポリシーパターンを使用することで、以下のようなメリットがあります。
- ビジネスルールの集約:複雑なビジネスルールを1つのクラスにまとめることで、コードの可読性と保守性が向上します。
- 柔軟性の向上:ビジネスルールの変更や追加が容易になり、システムの柔軟性が高まります。
- テストしやすさの向上:ビジネスルールが独立したクラスになることで、ユニットテストが書きやすくなります。
ポリシーパターンを使わない場合、ビジネスルールがドメインモデルやアプリケーション層に散らばってしまい、コードの複雑性が増し、保守性が低下する可能性があります。
TypeScriptでの実装例
UserPolicyインターフェース
interface UserPolicy {
canCreateUser(): boolean;
canUpdateUser(): boolean;
canDeleteUser(): boolean;
canViewUserList(): boolean;
}
UserPolicyインターフェースは、ユーザーの権限に関するメソッドのシグネチャを定義しています。このインターフェースを実装することで、各ポリシークラスが共通のメソッドを持つことを保証します。
AdminUserPolicyクラス
class AdminUserPolicy implements UserPolicy {
canCreateUser(): boolean {
return true;
}
canUpdateUser(): boolean {
return true;
}
canDeleteUser(): boolean {
return true;
}
canViewUserList(): boolean {
return true;
}
}
AdminUserPolicyクラスは、管理者ユーザーの権限を定義しています。管理者は全ての操作(ユーザーの作成、更新、削除、一覧表示)が可能なため、全てのメソッドがtrueを返します。
PrivilegedUserPolicyクラス
class PrivilegedUserPolicy implements UserPolicy {
canCreateUser(): boolean {
return false;
}
canUpdateUser(): boolean {
return true;
}
canDeleteUser(): boolean {
return false;
}
canViewUserList(): boolean {
return true;
}
}
PrivilegedUserPolicyクラスは、特権ユーザーの権限を定義しています。特権ユーザーはユーザーの作成と削除はできませんが、更新と一覧表示は可能です。
RegularUserPolicyクラス
class RegularUserPolicy implements UserPolicy {
canCreateUser(): boolean {
return false;
}
canUpdateUser(): boolean {
return false;
}
canDeleteUser(): boolean {
return false;
}
canViewUserList(): boolean {
return true;
}
}
RegularUserPolicyクラスは、一般ユーザーの権限を定義しています。一般ユーザーはユーザー一覧の表示のみ可能で、その他の操作(作成、更新、削除)はできません。
Userクラス
class User {
private policy: UserPolicy;
constructor(policy: UserPolicy) {
this.policy = policy;
}
canCreateUser(): boolean {
return this.policy.canCreateUser();
}
canUpdateUser(): boolean {
return this.policy.canUpdateUser();
}
canDeleteUser(): boolean {
return this.policy.canDeleteUser();
}
canViewUserList(): boolean {
return this.policy.canViewUserList();
}
}
Userクラスは、ユーザーを表すクラスです。コンストラクタでUserPolicyオブジェクトを受け取り、privateなpolicyプロパティに保持します。各メソッド(canCreateUser, canUpdateUser, canDeleteUser, canViewUserList)は、policyオブジェクトの対応するメソッドを呼び出すことで、ユーザーの権限に応じた振る舞いを実現しています。
このようにポリシーパターンを使うことで、ユーザーの権限に関するビジネスルールを明確に表現し、Userクラスからビジネスルールを切り離し、明確に示すことができます。
利用例
const adminUser = new User(new AdminUserPolicy());
console.log(adminUser.canCreateUser()); // true
console.log(adminUser.canUpdateUser()); // true
console.log(adminUser.canDeleteUser()); // true
console.log(adminUser.canViewUserList()); // true
const regularUser = new User(new RegularUserPolicy());
console.log(regularUser.canCreateUser()); // false
console.log(regularUser.canUpdateUser()); // false
console.log(regularUser.canDeleteUser()); // false
console.log(regularUser.canViewUserList()); // true
このように、Userインスタンスを作成する際に適切なポリシーを渡すことで、ユーザーの権限に応じた振る舞いを実現できます。
PHPでの実装例(Laravelフレームワーク)
// app/Policies/RolePolicy.php
class RolePolicy
{
public function isAdmin(User $user)
{
return $user->role === 'admin';
}
public function isEditor(User $user)
{
return in_array($user->role, ['admin', 'editor']);
}
public function isViewer(User $user)
{
return in_array($user->role, ['admin', 'editor', 'viewer']);
}
}
RolePolicyクラスでは、ユーザーの役割を判定するメソッドを定義しています。各メソッドは、ユーザーの役割に応じてtrueまたはfalseを返します。
// app/Providers/AuthServiceProvider.php
use App\Policies\RolePolicy;
public function boot()
{
$this->registerPolicies();
Gate::define('admin', [RolePolicy::class, 'isAdmin']);
Gate::define('editor', [RolePolicy::class, 'isEditor']);
Gate::define('viewer', [RolePolicy::class, 'isViewer']);
}
AuthServiceProviderでは、RolePolicyのメソッドをLaravelのGateと関連付けています。これにより、コントローラーやミドルウェアなどでGateを使ってユーザーの権限を確認できるようになります。
// routes/api.php
Route::middleware('auth:api')->group(function () {
Route::get('/admin', function () {
// 管理者のみがアクセス可能なルート
})->middleware('can:admin');
Route::get('/editor', function () {
// 管理者と編集者がアクセス可能なルート
})->middleware('can:editor');
Route::get('/viewer', function () {
// 管理者、編集者、閲覧者がアクセス可能なルート
})->middleware('can:viewer');
});
api.phpでは、各ルートにmiddlewareを使用して、適切な役割のユーザーのみがアクセスできるようにしています。can:adminのようにGateを使ってユーザーの権限を確認しています。
もしポリシーパターンを使わずに、コントローラーやミドルウェアなどでユーザーの権限を直接確認していた場合、ビジネスルールが複数の場所に散在することになり、コードの保守性が低下する可能性があります。
まとめ
現在はポリシーパターンが少ないため良いが、今後パターンが増えてくると多くをクラスに記述する必要が出てくるため、難易度が上がってしまうと感じる。
しかし、今後の拡張性を考えるとポリシーパターンを利用しない手はないので、どうにかこうにかやっていこうと思う。