はじめに
この記事シリーズでは、Railsアプリケーション内で複数のコントローラーに分散しているスケジュール関連のロジックを集約し、ScheduleService
クラスを作成して、コードの重複を解消し、保守性とテストのしやすさを向上させるリファクタリング手法について説明します。最初に、なぜサービスオブジェクトが必要なのか、そして既存のコードとの比較を通じてその利点を見ていきます。
問題の特定
例として、予約システムの ReservationsController
と SchedulesController
で全く同じようにスタッフのスケジュールを処理している部分があるとします。これらのコントローラーでは、特定の日に利用可能なスタッフのスケジュールを取得し、その時間スロットの利用可能状況を計算しています。
既存のコード(抜粋)
# ReservationsController, SchedulesController
def confirm
@company = Company.find(params[:company_id])
@date = params[:date] || Date.today
@services = @company.services
@working_staffs_on_date = @company.staffs.joins(:schedules).where(schedules: {at_work: true, shift_fixed: true, date: @date}).includes(:services)
@service_availability = calculate_service_availability(@working_staffs_on_date, @services, @date)
@working_staffs_on_date = @company.staffs.joins(:schedules).where(schedules: {at_work: true, shift_fixed: true, date: @date}).includes(:services)
@service_availability = @services.order("LOWER(services.name)").each_with_object({}) do |service, hash|
staff_schedule_map = @working_staffs_on_date.each_with_object({}) do |staff, map|
if staff.services.include?(service)
map[staff.id] = staff.schedules.select { |sch| sch.date == @date }
end
end
hash[service.id] = @time_slots.each_with_object({}) do |slot, inner_hash|
available_staff_count = staff_schedule_map.count do |_, schedules|
schedules.any? do |schedule|
start_time = schedule.start_time.strftime("%H:%M")
end_time = schedule.end_time.strftime("%H:%M")
time_in_slot = start_time <= slot && slot < end_time
time_unavailable = schedule.time_tables.any? do |tt|
tt.available == false && tt.start_time.strftime("%H:%M") <= slot && tt.end_time.strftime("%H:%M") > slot
end
if time_unavailable
false
elsif time_in_slot
true
else
false
end
end
end
inner_hash[slot] = case available_staff_count
when 0 then '×'
when 1 then '△'
else '○'
end
end
end
end
ScheduleService の導入理由
- DRY原則の適用: 同じロジックが異なるコントローラーで繰り返されており、これを一箇所に集約することで、変更があった際の作業量とエラー発生のリスクを減らします。
- テストのしやすさ: ビジネスロジックをサービスオブジェクトに分離することで、独立してテストが行いやすくなります。
- 再利用性の向上: 他のコントローラーやバックグラウンドジョブなど、異なるコンテキストで簡単に利用できるようになります。
ScheduleService の基本的な構造
サービスオブジェクトを作成する際の基本的な構造を以下に示します。
# app/services/schedule_service.rb
class ScheduleService
def self.calculate_availability(company, date, services, time_slots)
working_staffs_on_date = company.staffs.joins(:schedules).where(schedules: {at_work: true, shift_fixed: true, date: date}).includes(:services)
service_availability = services.order("LOWER(services.name)").each_with_object({}) do |service, hash|
staff_schedule_map = build_staff_schedule_map(working_staffs_on_date, service, date)
hash[service.id] = generate_time_slots_availability(staff_schedule_map, time_slots)
end
service_availability
end
private
def self.build_staff_schedule_map(working_staffs, service, date)
working_staffs.each_with_object({}) do |staff, map|
if staff.services.include?(service)
map[staff.id] = staff.schedules.select { |sch| sch.date == date }
end
end
end
def self.generate_time_slots_availability(staff_schedule_map, time_slots)
time_slots.each_with_object({}) do |slot, inner_hash|
available_staff_count = staff_schedule_map.count do |_, schedules|
schedules.any? do |schedule|
start_time = schedule.start_time.strftime("%H:%M")
end_time = schedule.end_time.strftime("%H:%M")
time_in_slot = start_time <= slot && slot < end_time
time_unavailable = schedule.time_tables.any? { |tt| !tt.available && tt.start_time.strftime("%H:%M") <= slot && tt.end_time.strftime("%H:%M") > slot }
time_in_slot && !time_unavailable
end
end
inner_hash[slot] = case available_staff_count
when 0 then '×'
when 1 then '△'
else '○'
end
end
end
end
次の記事(パート2)では、ScheduleService
の具体的なメソッド実装と、コントローラーでの利用方法について詳しく解説します。
次回の記事