この記事は ex-crowdworks Advent Calendar 2024の18日目の記事です。
はじめに
今年、株式会社クラウドワークスを退職した@nisyuuです。今年、食べて正解だった海鮮丼はじもの亭の海鮮丼(華)です。
エンジニアとしてクラウドワークステック(旧クラウドテック)というフリーランスと企業をマッチングするエージェントサービスを開発していました。
Policyオブジェクトパターンは、ソフトウェア設計において権限管理やアクセス制御を明確にするためのデザインパターンです。
特に、Webアプリケーションでは、ユーザーが特定の操作を実行できるかどうかを判定する場面で頻繁に利用されます。
本記事では、Policyオブジェクトパターンの基本的な概念と利点・欠点、さらにRuby on Railsでの実装例について解説します。
Policyオブジェクトパターンとは
Policyオブジェクトパターンは、アプリケーション内のビジネスルールを1つのクラスにカプセル化して管理する設計手法です。このパターンは以下のような特徴があります:
- 役割の分離: 権限管理のロジックをモデルやコントローラーから切り離して、専用のクラスにまとめます
- 単一責任原則(SRP): 各Policyオブジェクトは特定のリソースと操作に対するルールのみを担当します
- テストの容易さ: 権限ロジックを独立したクラスとして実装するため、テストがしやすくなります
基本的な構成
Policyオブジェクトは、通常以下のように構成されます:
-
対象リソース: 許可または拒否を判断するリソース(例:
Post
モデル) -
メソッド: 各操作(例:
create?
、update?
)の許可/拒否を判定するメソッド - コンテキスト: 現在のユーザーやその他の関連情報(例:ログインユーザー)
Policyオブジェクトパターンのメリットとデメリット
メリット
- コードの可読性向上: 権限ロジックが明確に分離されるため、コードベースが読みやすくなります
- 再利用性の向上: Policyオブジェクトを異なるコントローラーやモデル間で再利用可能
- テストの容易化: テストが個別のクラス単位で実施でき、ユニットテストが簡単
- 保守性の向上: 権限ロジックが1か所に集約されているため、変更が容易
デメリット
- クラスの増加: 小規模なアプリケーションでは、ファイルやクラスの数が増えすぎて煩雑になる可能性がある
- 学習コスト: 初心者にとって、別途Policyオブジェクトを作成する設計思想に慣れる必要がある
- パフォーマンス: 設計の複雑さにより、パフォーマンスに若干の影響が出ることもある
Ruby on Railsでの実装例
RailsでのPolicyオブジェクトパターンの実装には、PunditというGemが広く利用されています。以下は、Punditを使った基本的な実装例です。
Gemのインストール
まず、Pundit
をGemfileに追加します。
# Gemfile
gem 'pundit'
インストール後、以下のコマンドでセットアップを行います:
bundle install
rails g pundit:install
Policyクラスの作成
例えば、Post
モデルに対するPolicyオブジェクトを作成します。
rails g pundit:policy post
以下のようなファイルが生成されます:
# app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
def show?
true # 全てのユーザーが閲覧可能
end
def update?
user.admin? || record.user_id == user.id # 管理者または投稿者本人のみ更新可能
end
def destroy?
user.admin? # 管理者のみ削除可能
end
end
コントローラーでの利用
PostPolicy
を利用してアクセス制御を行います。
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
before_action :authenticate_user!
before_action :set_post, only: [:show, :update, :destroy]
def show
authorize @post # Punditのauthorizeメソッドを使用
end
def update
authorize @post
if @post.update(post_params)
redirect_to @post, notice: '投稿を更新しました。'
else
render :edit
end
end
def destroy
authorize @post
@post.destroy
redirect_to posts_path, notice: '投稿を削除しました。'
end
private
def set_post
@post = Post.find(params[:id])
end
def post_params
params.require(:post).permit(:title, :content)
end
end
Policy Specの作成
PolicyオブジェクトのテストはRSpecを使うと簡単です。
# spec/policies/post_policy_spec.rb
require 'rails_helper'
describe PostPolicy do
subject { described_class.new(user, post) }
let(:user) { User.create(role: 'admin') }
let(:post) { Post.create(user: user) }
context '管理者ユーザーの場合' do
it { is_expected.to permit_action(:update) }
it { is_expected.to permit_action(:destroy) }
end
context '通常ユーザーの場合' do
let(:user) { User.create(role: 'user') }
it { is_expected.to forbid_action(:destroy) }
end
end
おわりに
Policyオブジェクトパターンは、アプリケーションの権限管理を分離し、コードの保守性と再利用性を向上させるために有効です。
Railsでは、Punditのようなツールを利用することで、簡単かつ効率的に実装が可能です。
小規模なプロジェクトでは冗長に感じるかもしれませんが、中〜大規模なアプリケーションではそのメリットを感じられるでしょう。