リファクタリング: ScheduleServiceを導入する（パート1）

## はじめに

この記事シリーズでは、Railsアプリケーション内で複数のコントローラーに分散しているスケジュール関連のロジックを集約し、`ScheduleService` クラスを作成して、コードの重複を解消し、保守性とテストのしやすさを向上させるリファクタリング手法について説明します。最初に、なぜサービスオブジェクトが必要なのか、そして既存のコードとの比較を通じてその利点を見ていきます。

## 問題の特定

例として、予約システムの `ReservationsController` と `SchedulesController` で全く同じようにスタッフのスケジュールを処理している部分があるとします。これらのコントローラーでは、特定の日に利用可能なスタッフのスケジュールを取得し、その時間スロットの利用可能状況を計算しています。

### 既存のコード（抜粋）

```ruby
# 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 の導入理由

1. **DRY原則の適用**: 同じロジックが異なるコントローラーで繰り返されており、これを一箇所に集約することで、変更があった際の作業量とエラー発生のリスクを減らします。
2. **テストのしやすさ**: ビジネスロジックをサービスオブジェクトに分離することで、独立してテストが行いやすくなります。
3. **再利用性の向上**: 他のコントローラーやバックグラウンドジョブなど、異なるコンテキストで簡単に利用できるようになります。

## ScheduleService の基本的な構造

サービスオブジェクトを作成する際の基本的な構造を以下に示します。

```ruby
# 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` の具体的なメソッド実装と、コントローラーでの利用方法について詳しく解説します。

## 次回の記事

https://qiita.com/KM9973/items/958689d2e9d077d501cc