📄

PDF生成の保守性とデバッグ性を改善する方法 〜 複雑な条件分岐とエラーハンドリングの根本的解決策

に公開

PDF生成の保守性とデバッグ性を改善する方法

https://zenn.dev/taian/articles/3412eea89d62d0

はじめに

大規模なRailsアプリケーションでPDF生成機能を運用していると、避けて通れない2つの大きな問題があります。

  1. 複雑な条件分岐による保守性の低下
  2. エラーハンドリングとデバッグの困難さ

この記事では、実際のプロダクション環境で遭遇したこれらの課題を分析し、根本的な解決策を提案します。

1. 複雑な条件分岐による保守性の低下

🔍 よく見る問題パターン

# 典型的な問題コード
class DocumentPdf < Prawn::Document
  def generate_content
    if document.type_a?
      if document.has_special_layout?
        generate_type_a_with_special_layout
      elsif document.has_discount?
        generate_type_a_with_discount
      else
        generate_standard_type_a
      end
    elsif document.type_b?
      if document.is_corporate?
        generate_corporate_type_b
      else
        generate_individual_type_b
      end
    elsif document.type_c?
      # さらに条件分岐...
    end
    # 条件分岐が延々と続く...
  end
end

❌ この設計の問題点

  1. 単一責任原則違反: 1つのクラスが複数のドキュメントタイプの責任を持つ
  2. 開放閉鎖原則違反: 新しいタイプ追加時に既存コードの修正が必要
  3. テストの複雑化: 条件の組み合わせパターンが指数的に増加

💡 解決策:ファクトリーパターン + ストラテジーパターン

# 1. ファクトリーパターンでオブジェクト生成を分離
class PdfGeneratorFactory
  def self.create(document)
    case document.document_type
    when 'type_a' then TypeAPdfGenerator.new(document)
    when 'type_b' then TypeBPdfGenerator.new(document)
    when 'type_c' then TypeCPdfGenerator.new(document)
    else
      raise ArgumentError, "Unsupported type: #{document.document_type}"
    end
  end
end

# 2. 抽象基底クラスで共通インターフェースを定義
class BasePdfGenerator
  def initialize(document)
    @document = document
  end

  def generate
    pdf = create_pdf_document
    render_header(pdf)
    render_body(pdf)
    render_footer(pdf)
    pdf.render
  end

  protected

  def create_pdf_document
    Prawn::Document.new(page_size: 'A4', margin: [16, 16])
  end

  # サブクラスで実装必須
  def render_header(pdf)
    raise NotImplementedError
  end

  def render_body(pdf)
    raise NotImplementedError
  end

  def render_footer(pdf)
    raise NotImplementedError
  end
end

# 3. 各タイプ専用のクラスで具体的な実装
class TypeAPdfGenerator < BasePdfGenerator
  protected

  def render_header(pdf)
    pdf.text @document.title, size: 18, style: :bold
    pdf.move_down 10
  end

  def render_body(pdf)
    pdf.text @document.content
    pdf.move_down 20
  end

  def render_footer(pdf)
    pdf.text "Page 1", align: :center
  end
end

# 使用例
document = Document.find(params[:id])
pdf_generator = PdfGeneratorFactory.create(document)
pdf_data = pdf_generator.generate

✅ 改善効果

  • 単一責任: 各クラスが1つのドキュメントタイプのみを担当
  • 拡張性: 新しいタイプの追加が既存コードに影響しない
  • テスト性: 各クラス単位で独立したテストが可能

2. エラーハンドリングとデバッグの困難さ

🔍 よく見る問題パターン

# 典型的な問題コード
def generate_pdf
  begin
    pdf = Prawn::Document.new
    render_complex_content(pdf)  # 300行の複雑な処理...
    pdf.render
  rescue => e
    Rails.logger.error "PDF generation failed: #{e.message}"
    # どこで失敗したかわからない...
    raise e
  end
end

❌ この設計の問題点

  1. エラー箇所の特定困難: 大きなメソッド内でエラーが発生すると原因箇所が不明
  2. 中間状態の不可視: 処理途中での状態が見えない
  3. デバッグ情報不足: ログに詳細な情報が記録されない

💡 解決策:段階的追跡システム

# 1. PDF生成の各段階を追跡するトラッカー
class PdfDebugTracker
  attr_reader :steps

  def initialize(context = {})
    @context = context
    @steps = []
    @start_time = Time.current
  end

  def track_step(step_name, metadata = {})
    step_start = Time.current
    
    begin
      Rails.logger.info "[PDF] Starting: #{step_name}"
      result = yield
      
      @steps << {
        name: step_name,
        status: 'success',
        duration: Time.current - step_start
      }
      
      Rails.logger.info "[PDF] ✅ #{step_name} completed"
      result
      
    rescue => e
      @steps << {
        name: step_name,
        status: 'error',
        error: e.message
      }
      
      Rails.logger.error "[PDF] ❌ #{step_name} failed: #{e.message}"
      log_summary
      raise e
    end
  end

  def log_summary
    Rails.logger.error "[PDF Summary] Steps completed: #{@steps.count}"
    @steps.each_with_index do |step, i|
      icon = step[:status] == 'success' ? '✅' : '❌'
      Rails.logger.error "  #{i+1}. #{icon} #{step[:name]}"
    end
  end
end

# 2. デバッグ機能付きベースクラス
class DebuggablePdfGenerator < BasePdfGenerator
  def initialize(document)
    super
    @tracker = PdfDebugTracker.new(
      document_id: document.id,
      type: document.document_type
    )
  end

  def generate
    @tracker.track_step("PDF initialization") do
      validate_prerequisites
    end

    pdf = @tracker.track_step("Create PDF document") do
      create_pdf_document
    end

    @tracker.track_step("Render header") do
      render_header(pdf)
    end

    @tracker.track_step("Render body") do
      render_body(pdf)
    end

    @tracker.track_step("Render footer") do
      render_footer(pdf)
    end

    @tracker.track_step("Finalize PDF") do
      pdf.render
    end
  end

  private

  def validate_prerequisites
    errors = []
    errors << "Document is nil" if @document.nil?
    errors << "Document has no content" if @document.content.blank?
    
    if errors.any?
      raise "Prerequisites failed: #{errors.join(', ')}"
    end
  end
end

# 3. カスタム例外クラス
class PdfGenerationError < StandardError
  attr_reader :context, :step

  def initialize(message, context: {}, step: nil)
    super(message)
    @context = context
    @step = step
  end
end

# 4. 具体的な実装例
class TypeAPdfGenerator < DebuggablePdfGenerator
  protected

  def render_header(pdf)
    @tracker.track_step("Render title") do
      raise PdfGenerationError, "Title is required" if @document.title.blank?
      pdf.text @document.title, size: 18, style: :bold
    end
  end

  def render_body(pdf)
    @tracker.track_step("Render sections", count: @document.sections.count) do
      @document.sections.each_with_index do |section, i|
        @tracker.track_step("Render section #{i+1}") do
          pdf.text section.title, size: 14, style: :bold
          pdf.text section.content
          pdf.move_down 20
        end
      end
    end
  end
end

✅ 改善効果

  • 段階的追跡: 各処理ステップの成功/失敗が明確に記録される
  • 詳細なコンテキスト: エラー発生時の状況が詳細に把握できる
  • 開発効率向上: どこでエラーが発生したか即座に特定可能

まとめ

設計改善の要点

課題 従来のアプローチ 改善後のアプローチ 得られる効果
複雑な条件分岐 大きなif-else文 ファクトリー + ストラテジー 拡張性・可読性・テスト性の向上
デバッグ困難 try-catch + ログ 段階的追跡システム エラー原因の迅速な特定

実装時のポイント

  1. 段階的な移行: 既存システムを一度に変更せず、新機能から適用
  2. テスト戦略: 各クラス・各ステップ単位でのテスト実装
  3. 監視体制: ログ分析とアラート設定による運用改善

良い設計は、将来の変更を容易にし、チーム全体の開発効率を向上させます。複雑さと戦うための投資として、これらのパターンを活用してください。


📝 あなたのプロジェクトではどのような条件分岐やデバッグの課題がありますか?

TAIANテックブログ

Discussion