🫠

Ruby/Railsで学ぶオブジェクト指向入門 インターフェースと抽象クラスって何ぞや

に公開

はじめに

こんにちは!本記事は、Ruby/Railsを使ってオブジェクト指向プログラミングの基礎について記載しています。本記事は継承とポリモーフィズム編の続きとなります。
https://zenn.dev/osakayakyu/articles/1eeae55c3c5890

5. インターフェースと抽象クラス

5.1 ポリモーフィズムの基本概念

ポリモーフィズム(多態性)は、オブジェクト指向プログラミングの重要な概念の一つで、「多くの形態を持つ」という意味です。同じインターフェース(メソッド名)を持つ異なるクラスのオブジェクトを、統一的な方法で扱えるようにする機能です。

ポリモーフィズムには主に以下のような利点があります:

  1. コードの柔軟性: 新しいクラスを追加しても、それを使用するコードを変更する必要がありません。
  2. 拡張性: システムを拡張する際に、既存のコードを変更せずに新しい機能を追加できます。
  3. コードの簡潔さ: 異なる型のオブジェクトを扱うための条件分岐が少なくなります。

ポリモーフィズムの簡単な例を見てみましょう:

class Animal
  def speak
    raise NotImplementedError, "サブクラスで実装する必要があります"
  end
end

class Dog < Animal
  def speak
    "ワンワン!"
  end
end

class Cat < Animal
  def speak
    "ニャーン!"
  end
end

class Duck < Animal
  def speak
    "ガーガー!"
  end
end

# ポリモーフィズムを活用した関数
def animal_concert(animals)
  puts "動物たちのコンサートが始まります!"
  animals.each do |animal|
    puts animal.speak
  end
  puts "コンサート終了!"
end

# 異なる動物のオブジェクトを作成
animals = [Dog.new, Cat.new, Duck.new, Dog.new, Cat.new]

# 同じインターフェース(speak)で異なる実装を呼び出す
animal_concert(animals)

この例では、speakメソッドが共通のインターフェースとなっています。animal_concert関数は、渡されるオブジェクトの具体的なクラスを気にせず、すべてのオブジェクトに対して同じようにspeakメソッドを呼び出しています。各クラスがspeakメソッドを独自に実装しているため、それぞれに適した動作が行われます。

ポリモーフィズムは、継承だけでなく、同じメソッド名を持つ異なるクラス間でも実現できます。Rubyではダックタイピング(duck typing)と呼ばれるアプローチが一般的です。

ダックタイピング

「アヒルのように歩き、アヒルのように鳴くなら、それはアヒルである」という考え方に基づいたアプローチです。オブジェクトの型よりも、オブジェクトが何をできるか(どのメソッドを持っているか)を重視します。

class Car
  def move
    "車が走ります"
  end
end

class Boat
  def move
    "ボートが航行します"
  end
end

class Plane
  def move
    "飛行機が飛びます"
  end
end

# 乗り物に関係なく、移動させる関数
def travel_with(vehicle)
  puts "旅行を始めます"
  puts vehicle.move
  puts "旅行が終わりました"
end

# 異なるクラスのオブジェクトを同じように扱う
travel_with(Car.new)
travel_with(Boat.new)
travel_with(Plane.new)

この例では、CarBoatPlaneクラスは継承関係にありませんが、すべてがmoveメソッドを実装しています。travel_with関数は、渡されたオブジェクトが何であるかを気にせず、moveメソッドを呼び出すだけです。これがダックタイピングの考え方です。

5.2 Rubyにおけるインターフェース的な実装

Rubyには、JavaやC#のような明示的なインターフェース機能はありません。しかし、Rubyのモジュール(Module)を使って、インターフェースに似た機能を実現できます。

モジュールを使ったインターフェース的な実装

module Swimmable
  def swim
    raise NotImplementedError, "#{self.class}でswimメソッドを実装する必要があります"
  end
end

module Flyable
  def fly
    raise NotImplementedError, "#{self.class}でflyメソッドを実装する必要があります"
  end
end

class Duck
  include Swimmable
  include Flyable
  
  def swim
    "アヒルが泳ぎます"
  end
  
  def fly
    "アヒルが飛びます"
  end
end

class Fish
  include Swimmable
  
  def swim
    "魚が泳ぎます"
  end
end

class Airplane
  include Flyable
  
  def fly
    "飛行機が飛びます"
  end
end

# 使用例
def make_swim(obj)
  if obj.respond_to?(:swim)
    puts obj.swim
  else
    puts "#{obj.class}は泳げません"
  end
end

def make_fly(obj)
  if obj.respond_to?(:fly)
    puts obj.fly
  else
    puts "#{obj.class}は飛べません"
  end
end

duck = Duck.new
fish = Fish.new
airplane = Airplane.new

make_swim(duck)     # => アヒルが泳ぎます
make_swim(fish)     # => 魚が泳ぎます
make_swim(airplane) # => Airplaneは泳げません

make_fly(duck)      # => アヒルが飛びます
make_fly(fish)      # => Fishは飛べません
make_fly(airplane)  # => 飛行機が飛びます

この例では、SwimmableFlyableという二つのモジュールを定義し、それぞれにswimflyメソッドの「テンプレート」を用意しています。各クラスはこれらのモジュールをインクルードすることで、それらの機能を持つことを宣言し、実際のメソッドを実装します。

Railsにおけるインターフェース的な使用例

Railsでは、Active Supportのコンサーンという機能を使って、インターフェース的な実装パターンが広く使われています。

# app/models/concerns/searchable.rb
module Searchable
  extend ActiveSupport::Concern
  
  included do
    scope :search, ->(query) { where("name LIKE ?", "%#{query}%") }
  end
  
  def search_description
    "#{self.class.name} ##{id}: #{name}"
  end
  
  # クラスメソッドも定義可能
  class_methods do
    def search_by_id(id)
      find_by(id: id)
    end
  end
end

# app/models/product.rb
class Product < ApplicationRecord
  include Searchable
  # 追加の実装...
end

# app/models/user.rb
class User < ApplicationRecord
  include Searchable
  # 追加の実装...
end

この例では、Searchableモジュールが検索機能のインターフェースとして機能し、それをProductUserモデルにインクルードすることで、両方のモデルに同じ検索機能を提供しています。

5.3 Rubyにおける抽象クラス

Rubyには抽象クラスの明示的なサポートはありませんが、基底クラスから特定のメソッドの実装を要求することで、抽象クラスと同様の機能を実現できます。

抽象クラスの基本的な実装パターン

class AbstractDocument
  def initialize
    # 基底クラスの直接インスタンス化を防ぐ
    raise "#{self.class}は抽象クラスであり、インスタンス化できません" if self.class == AbstractDocument
  end
  
  def title
    raise NotImplementedError, "サブクラスでtitleメソッドを実装する必要があります"
  end
  
  def content
    raise NotImplementedError, "サブクラスでcontentメソッドを実装する必要があります"
  end
  
  def format
    raise NotImplementedError, "サブクラスでformatメソッドを実装する必要があります"
  end
  
  # 抽象クラスで実装を提供するメソッド
  def summary
    "#{title} (#{format}): #{content[0, 50]}..."
  end
  
  def word_count
    content.split.size
  end
end

class TextDocument < AbstractDocument
  attr_reader :title, :content
  
  def initialize(title, content)
    @title = title
    @content = content
    # super() は呼び出さない(抽象クラスのチェックをスキップ)
  end
  
  def format
    "TEXT"
  end
end

class HtmlDocument < AbstractDocument
  attr_reader :title
  
  def initialize(title, html_content)
    @title = title
    @html_content = html_content
  end
  
  def content
    # HTMLタグを除去してプレーンテキストを返す簡易実装
    @html_content.gsub(/<[^>]*>/, '')
  end
  
  def format
    "HTML"
  end
  
  def html
    @html_content
  end
end

# 使用例
begin
  # 抽象クラスのインスタンス化 - エラー
  # doc = AbstractDocument.new  # => エラー
  
  text_doc = TextDocument.new(
    "Rubyプログラミング入門",
    "Rubyは読みやすく書きやすいオブジェクト指向プログラミング言語です。"
  )
  
  html_doc = HtmlDocument.new(
    "HTMLの基礎",
    "<p>HTMLは<strong>ウェブページ</strong>を作成するための言語です。</p>"
  )
  
  puts text_doc.summary
  puts "単語数: #{text_doc.word_count}"
  
  puts html_doc.summary
  puts "単語数: #{html_doc.word_count}"
  puts "HTML: #{html_doc.html}"
  
rescue => e
  puts "エラー: #{e.message}"
end

この例では、AbstractDocumentが抽象基底クラスとして機能し、サブクラスに特定のメソッド(title, content, format)の実装を要求しています。また、基底クラスでは具体的な実装も提供しています(summary, word_count)。

この抽象クラスパターンには、以下の特徴があります:

  1. インスタンス化の防止: 基底クラスのコンストラクタで、直接インスタンス化されないようにチェックしています。
  2. 抽象メソッドの定義: サブクラスで実装すべきメソッドを定義し、呼び出されるとエラーを発生させます。
  3. 共通実装の提供: サブクラスで共通して使用できる具体的な実装も提供しています。

より洗練された抽象クラスパターン

より洗練された抽象クラスパターンを実現するには、モジュールと継承を組み合わせることもできます:

module AbstractMethod
  def abstract_methods(*methods)
    methods.each do |method|
      define_method(method) do |*args|
        raise NotImplementedError, "#{self.class}#{method}メソッドを実装する必要があります"
      end
    end
  end
end

class AbstractService
  extend AbstractMethod
  
  abstract_methods :process, :validate
  
  def execute
    raise "#{self.class}は抽象クラスです" if self.class == AbstractService
    
    if validate
      puts "サービスを実行します..."
      result = process
      puts "サービスが完了しました"
      result
    else
      puts "検証に失敗しました"
      nil
    end
  end
end

class EmailService < AbstractService
  def initialize(email, message)
    @email = email
    @message = message
  end
  
  def validate
    @email && @email.include?('@') && !@message.empty?
  end
  
  def process
    puts "#{@email}にメールを送信中: #{@message}"
    true
  end
end

class SmsService < AbstractService
  def initialize(phone, message)
    @phone = phone
    @message = message
  end
  
  def validate
    @phone && @phone.length >= 10 && !@message.empty?
  end
  
  def process
    puts "#{@phone}にSMSを送信中: #{@message}"
    true
  end
end

# 使用例
email_service = EmailService.new('user@example.com', 'こんにちは!')
email_service.execute

sms_service = SmsService.new('1234567890', 'こんにちは!')
sms_service.execute

# 検証に失敗する例
invalid_email = EmailService.new('invalid-email', '')
invalid_email.execute

この例では、AbstractMethodモジュールとabstract_methodsメソッドを使用して、抽象メソッドを宣言する簡潔な方法を提供しています。これにより、抽象クラスにおける抽象メソッドの定義がより明示的になります。

5.4 実践問題と解説(2問)

問題1: 支払い処理システムの実装

ECサイトの支払い処理システムを実装してください。以下の要件を満たす必要があります:

  1. PaymentProcessorという抽象クラスを作成し、支払い処理の共通インターフェースを定義する
  2. 異なる支払い方法(クレジットカード、銀行振込、電子マネー)を処理する具体的なクラスを実装する
  3. 各支払い方法には、認証、支払い処理、領収書生成の機能が必要
  4. 共通のエラー処理と支払いログ機能を実装する
  5. 複数の支払い方法を組み合わせた「分割支払い」機能も実装する

解答

# 支払い処理の抽象クラス
class PaymentProcessor
  attr_reader :amount, :description
  
  def initialize(amount, description)
    raise "PaymentProcessorは抽象クラスです" if self.class == PaymentProcessor
    
    @amount = amount
    @description = description
    @transaction_id = nil
  end
  
  # サブクラスで実装すべき抽象メソッド
  def authenticate
    raise NotImplementedError, "サブクラスでauthenticateメソッドを実装する必要があります"
  end
  
  def process_payment
    raise NotImplementedError, "サブクラスでprocess_paymentメソッドを実装する必要があります"
  end
  
  def generate_receipt
    raise NotImplementedError, "サブクラスでgenerate_receiptメソッドを実装する必要があります"
  end
  
  # 共通のエラー処理を含む支払い実行メソッド
  def execute
    begin
      log("支払い開始: #{@amount}円 - #{@description}")
      
      auth_result = authenticate
      unless auth_result
        log("認証失敗")
        return { success: false, error: "認証に失敗しました" }
      end
      
      log("認証成功")
      
      process_result = process_payment
      unless process_result[:success]
        log("支払い処理失敗: #{process_result[:error]}")
        return process_result
      end
      
      @transaction_id = process_result[:transaction_id]
      log("支払い処理成功: トランザクションID #{@transaction_id}")
      
      receipt = generate_receipt
      log("領収書生成: #{receipt[:receipt_number]}")
      
      return {
        success: true,
        transaction_id: @transaction_id,
        receipt: receipt
      }
    rescue => e
      log("エラー発生: #{e.message}")
      return { success: false, error: e.message }
    end
  end
  
  # 支払い状況をログに記録
  def log(message)
    puts "[#{Time.now}] [#{self.class.name}] #{message}"
  end
end

# クレジットカード支払い
class CreditCardPayment < PaymentProcessor
  attr_reader :card_number, :expiry_date, :cvv
  
  def initialize(amount, description, card_number, expiry_date, cvv)
    super(amount, description)
    @card_number = mask_card_number(card_number)
    @expiry_date = expiry_date
    @cvv = cvv
  end
  
  def authenticate
    # 実際にはカード情報の検証を行う
    log("クレジットカードの検証: #{@card_number}")
    valid_card? && !expired?
  end
  
  def process_payment
    # 実際には決済代行会社のAPIを呼び出す
    log("クレジットカード決済処理中: #{@amount}円")
    
    # 金額が100,000円以上の場合はエラーとする(デモ用)
    if @amount >= 100_000
      return { success: false, error: "高額決済はクレジットカードでは処理できません" }
    end
    
    # 成功したと仮定
    { success: true, transaction_id: "CC-#{Time.now.to_i}" }
  end
  
  def generate_receipt
    {
      receipt_number: "R-CC-#{Time.now.to_i}",
      date: Time.now,
      payment_method: "クレジットカード",
      last_4_digits: @card_number[-4..-1],
      amount: @amount,
      description: @description
    }
  end
  
  private
  
  def mask_card_number(number)
    "*" * (number.length - 4) + number[-4..-1]
  end
  
  def valid_card?
    # 実際にはLuhnアルゴリズムなどでカード番号の妥当性を検証
    true
  end
  
  def expired?
    # 実際には有効期限をチェック
    false
  end
end

# 銀行振込支払い
class BankTransferPayment < PaymentProcessor
  attr_reader :bank_name, :account_number
  
  def initialize(amount, description, bank_name, account_number)
    super(amount, description)
    @bank_name = bank_name
    @account_number = account_number
    @verified = false
  end
  
  def authenticate
    # 銀行口座の検証(本来はもっと複雑)
    log("銀行口座の検証: #{@bank_name}, #{@account_number}")
    @verified = true
  end
  
  def process_payment
    # 実際には銀行APIを呼び出すか、手動確認が必要
    log("銀行振込処理中: #{@amount}円 -> #{@bank_name}")
    
    # 処理中ステータスとして成功を返す(実際には非同期処理になる)
    { success: true, transaction_id: "BT-#{Time.now.to_i}" }
  end
  
  def generate_receipt
    {
      receipt_number: "R-BT-#{Time.now.to_i}",
      date: Time.now,
      payment_method: "銀行振込",
      bank_name: @bank_name,
      account_reference: "REF-#{Time.now.to_i}",
      amount: @amount,
      description: @description
    }
  end
end

# 電子マネー支払い
class DigitalWalletPayment < PaymentProcessor
  attr_reader :wallet_id, :provider
  
  def initialize(amount, description, wallet_id, provider)
    super(amount, description)
    @wallet_id = wallet_id
    @provider = provider
  end
  
  def authenticate
    # ウォレットの認証
    log("電子マネー認証: #{@provider}, #{@wallet_id}")
    wallet_exists? && sufficient_funds?
  end
  
  def process_payment
    # 実際には電子マネープロバイダのAPIを呼び出す
    log("電子マネー決済処理中: #{@amount}円 (#{@provider})")
    
    # 成功したと仮定
    { success: true, transaction_id: "DW-#{@provider}-#{Time.now.to_i}" }
  end
  
  def generate_receipt
    {
      receipt_number: "R-DW-#{Time.now.to_i}",
      date: Time.now,
      payment_method: "電子マネー (#{@provider})",
      wallet_id: @wallet_id,
      amount: @amount,
      description: @description
    }
  end
  
  private
  
  def wallet_exists?
    # ウォレットの存在確認
    true
  end
  
  def sufficient_funds?
    # 残高確認
    true
  end
end

# 分割支払い(複合支払い)
class SplitPayment
  def initialize(description, payment_processors)
    @description = description
    @payment_processors = payment_processors
    @total_amount = payment_processors.sum(&:amount)
  end
  
  def execute
    puts "===== 分割支払い開始 合計: #{@total_amount}円 ====="
    puts "説明: #{@description}"
    puts "支払い方法数: #{@payment_processors.length}"
    
    results = []
    all_success = true
    
    @payment_processors.each_with_index do |processor, index|
      puts "\n----- 支払い #{index + 1}/#{@payment_processors.length} (#{processor.amount}円) -----"
      result = processor.execute
      results << result
      
      if !result[:success]
        all_success = false
        puts "分割支払いの一部で失敗が発生しました。処理を中止します。"
        break
      end
    end
    
    if all_success
      puts "\n===== 分割支払い完了 ====="
      
      receipt = generate_combined_receipt(results)
      puts "合計領収書番号: #{receipt[:receipt_number]}"
      
      return {
        success: true,
        total_amount: @total_amount,
        individual_results: results,
        combined_receipt: receipt
      }
    else
      puts "\n===== 分割支払い失敗 - 払い戻し処理が必要 ====="
      
      # 実際には成功した支払いの払い戻し処理が必要
      
      return {
        success: false,
        error: "分割支払いの一部で失敗が発生しました",
        individual_results: results
      }
    end
  end
  
  private
  
  def generate_combined_receipt(results)
    {
      receipt_number: "R-SPLIT-#{Time.now.to_i}",
      date: Time.now,
      payment_method: "分割支払い",
      total_amount: @total_amount,
      description: @description,
      parts: results.map { |r| r[:receipt] }.compact
    }
  end
end

# 使用例
puts "===== クレジットカード支払いのテスト ====="
cc_payment = CreditCardPayment.new(
  50000,
  "ノートパソコン購入",
  "4111111111111111",
  "12/25",
  "123"
)
cc_result = cc_payment.execute
puts "結果: #{cc_result[:success] ? '成功' : '失敗'}"
puts

puts "===== 銀行振込支払いのテスト ====="
bank_payment = BankTransferPayment.new(
  150000,
  "家具一式",
  "三菱UFJ銀行",
  "1234567"
)
bank_result = bank_payment.execute
puts "結果: #{bank_result[:success] ? '成功' : '失敗'}"
puts

puts "===== 電子マネー支払いのテスト ====="
digital_payment = DigitalWalletPayment.new(
  8500,
  "書籍購入",
  "user123",
  "PayPay"
)
digital_result = digital_payment.execute
puts "結果: #{digital_result[:success] ? '成功' : '失敗'}"
puts

puts "===== 分割支払いのテスト ====="
# 高額商品を分割して支払う
cc_part = CreditCardPayment.new(
  80000,
  "高級時計(一部)",
  "5555555555554444",
  "06/26",
  "456"
)

bank_part = BankTransferPayment.new(
  70000,
  "高級時計(残り)",
  "みずほ銀行",
  "7654321"
)

split_payment = SplitPayment.new(
  "高級時計購入 (150,000円)",
  [cc_part, bank_part]
)

split_result = split_payment.execute
puts "最終結果: #{split_result[:success] ? '成功' : '失敗'}"
puts

解説

この実装では、支払い処理システムを抽象クラスとポリモーフィズムを使って設計しています。

  1. 抽象クラス PaymentProcessor

    • すべての支払い方法の基底クラスとなる抽象クラス
    • 実装すべき抽象メソッド(authenticate, process_payment, generate_receipt)を定義
    • 共通の処理フロー(executeメソッド)を提供
    • ログ記録機能を共通実装として提供
  2. 具体的な支払い方法クラス

    • CreditCardPayment: クレジットカード決済を処理
    • BankTransferPayment: 銀行振込を処理
    • DigitalWalletPayment: 電子マネー決済を処理
  3. 複合的な機能 SplitPayment

    • 複数の支払い方法を組み合わせた分割支払いを実現
    • 異なる支払い方法オブジェクトを組み合わせて使用(構成)
    • 各支払いが同じインターフェースを持つことで、統一的に扱える(ポリモーフィズム)

この設計では、以下のような利点があります:

  • 拡張性: 新しい支払い方法を追加する場合、PaymentProcessorを継承した新しいクラスを作成するだけで済みます。
  • 整合性: すべての支払い方法で同じ処理フローを強制できます。
  • コード再利用: 共通の処理(エラー処理、ロギングなど)は基底クラスで一度定義するだけで済みます。
  • 柔軟性: 異なる支払い方法を簡単に組み合わせられます(分割支払い)。

この例ではRubyの抽象クラスとポリモーフィズムを活用して、異なる支払い方法を共通のインターフェースで処理できるようにしています。エラー処理、ログ記録、領収書生成などの共通機能は基底クラスで提供しつつ、各支払い方法特有の処理はサブクラスで実装するという責任分担を実現しています。

問題2: データエクスポート/インポートシステムの実装

さまざまな形式(CSV、JSON、XML、Excelなど)のデータをエクスポート/インポートできるシステムを実装してください。以下の要件を満たす必要があります:

  1. データを扱うための抽象インターフェース(DataProcessor)を定義する
  2. 各形式に対応した具体的なエクスポーター/インポーターを実装する
  3. ファイル形式の検出と適切なプロセッサーの選択を行うファクトリーを実装する
  4. エラー処理とデータ検証の機能を備える
  5. 複数のデータ形式を一度に処理できるバッチ処理機能を実装する

解答

require 'json'
require 'csv'
require 'rexml/document'

# データ処理の抽象インターフェース(モジュール)
module DataProcessor
  def load(input)
    raise NotImplementedError, "#{self.class}でloadメソッドを実装する必要があります"
  end
  
  def dump(data)
    raise NotImplementedError, "#{self.class}でdumpメソッドを実装する必要があります"
  end
  
  def validate(data)
    raise NotImplementedError, "#{self.class}でvalidateメソッドを実装する必要があります"
  end
  
  def processor_name
    self.class.name
  end
end

# CSVプロセッサー
class CsvProcessor
  include DataProcessor
  
  def load(input)
    begin
      rows = CSV.parse(input, headers: true)
      data = rows.map(&:to_h)
      puts "CSVから#{data.length}件のレコードを読み込みました"
      data
    rescue CSV::MalformedCSVError => e
      puts "CSVの解析中にエラーが発生しました: #{e.message}"
      []
    end
  end
  
  def dump(data)
    return '' if data.empty?
    
    headers = data.first.keys
    CSV.generate do |csv|
      csv << headers
      data.each do |row|
        csv << row.values_at(*headers)
      end
    end
  end
  
  def validate(data)
    return { valid: false, errors: ["データが空です"] } if data.empty?
    
    errors = []
    
    # すべての行が同じキーを持っているか確認
    first_keys = data.first.keys
    data.each_with_index do |row, index|
      unless row.keys == first_keys
        errors << "行 #{index + 1} のキーが一致していません"
      end
    end
    
    { valid: errors.empty?, errors: errors }
  end
end

# JSONプロセッサー
class JsonProcessor
  include DataProcessor
  
  def load(input)
    begin
      data = JSON.parse(input)
      
      # 配列でない場合は配列に変換
      data = [data] unless data.is_a?(Array)
      
      puts "JSONから#{data.length}件のレコードを読み込みました"
      data
    rescue JSON::ParserError => e
      puts "JSONの解析中にエラーが発生しました: #{e.message}"
      []
    end
  end
  
  def dump(data)
    JSON.pretty_generate(data)
  end
  
  def validate(data)
    return { valid: false, errors: ["データが空です"] } if data.empty?
    
    errors = []
    
    # すべての要素がハッシュであることを確認
    data.each_with_index do |item, index|
      unless item.is_a?(Hash)
        errors << "アイテム #{index + 1} がハッシュではありません"
      end
    end
    
    { valid: errors.empty?, errors: errors }
  end
end

# XMLプロセッサー
class XmlProcessor
  include DataProcessor
  
  def load(input)
    begin
      doc = REXML::Document.new(input)
      
      # rootの下のレコード要素をすべて取得
      records = []
      root_element = doc.root
      
      root_element.elements.each do |record_element|
        record = {}
        
        record_element.elements.each do |field_element|
          record[field_element.name] = field_element.text
        end
        
        records << record
      end
      
      puts "XMLから#{records.length}件のレコードを読み込みました"
      records
    rescue REXML::ParseException => e
      puts "XMLの解析中にエラーが発生しました: #{e.message}"
      []
    end
  end
  
  def dump(data)
    doc = REXML::Document.new
    root = doc.add_element('records')
    
    data.each do |record|
      record_element = root.add_element('record')
      
      record.each do |key, value|
        field = record_element.add_element(key)
        field.text = value.to_s
      end
    end
    
    output = ''
    formatter = REXML::Formatters::Pretty.new
    formatter.write(doc, output)
    output
  end
  
  def validate(data)
    return { valid: false, errors: ["データが空です"] } if data.empty?
    
    # 簡易的な検証
    { valid: true, errors: [] }
  end
end

# プロセッサーを作成するファクトリークラス
class DataProcessorFactory
  def self.create_from_extension(file_path)
    extension = File.extname(file_path).downcase
    
    case extension
    when '.csv'
      CsvProcessor.new
    when '.json'
      JsonProcessor.new
    when '.xml'
      XmlProcessor.new
    else
      raise "未対応のファイル形式です: #{extension}"
    end
  end
  
  def self.create_from_content(content)
    # 内容に基づいて適切なプロセッサーを判定
    if content.strip.start_with?('{') || content.strip.start_with?('[')
      JsonProcessor.new
    elsif content.strip.start_with?('<')
      XmlProcessor.new
    elsif content.include?(',') && content.lines.first.include?(',')
      CsvProcessor.new
    else
      raise "内容からファイル形式を判断できません"
    end
  end
end

# データのエクスポート/インポートを管理するクラス
class DataManager
  def import_file(file_path)
    begin
      processor = DataProcessorFactory.create_from_extension(file_path)
      puts "#{processor.processor_name}を使用してインポートしています..."
      
      content = File.read(file_path)
      data = processor.load(content)
      
      validation = processor.validate(data)
      unless validation[:valid]
        puts "データの検証に失敗しました:"
        validation[:errors].each { |error| puts "- #{error}" }
        return nil
      end
      
      puts "インポート成功: #{data.length}件のレコード"
      data
    rescue => e
      puts "インポート中にエラーが発生しました: #{e.message}"
      nil
    end
  end
  
  def export_data(data, file_path)
    begin
      processor = DataProcessorFactory.create_from_extension(file_path)
      puts "#{processor.processor_name}を使用してエクスポートしています..."
      
      validation = processor.validate(data)
      unless validation[:valid]
        puts "データの検証に失敗しました:"
        validation[:errors].each { |error| puts "- #{error}" }
        return false
      end
      
      output = processor.dump(data)
      File.write(file_path, output)
      
      puts "エクスポート成功: #{file_path} (#{data.length}件のレコード)"
      true
    rescue => e
      puts "エクスポート中にエラーが発生しました: #{e.message}"
      false
    end
  end
  
  def convert_file(input_path, output_path)
    begin
      data = import_file(input_path)
      return false unless data
      
      export_data(data, output_path)
    rescue => e
      puts "変換中にエラーが発生しました: #{e.message}"
      false
    end
  end
end

# バッチ処理のためのクラス
class BatchProcessor
  def initialize(data_manager)
    @data_manager = data_manager
  end
  
  def process_directory(input_dir, output_dir, target_extension)
    puts "===== バッチ処理開始 ====="
    puts "入力ディレクトリ: #{input_dir}"
    puts "出力ディレクトリ: #{output_dir}"
    puts "出力形式: #{target_extension}"
    
    # 出力ディレクトリが存在しない場合は作成
    Dir.mkdir(output_dir) unless Dir.exist?(output_dir)
    
    success_count = 0
    failure_count = 0
    
    # サポートされている拡張子のファイルを検索
    files = Dir["#{input_dir}/*.csv", "#{input_dir}/*.json", "#{input_dir}/*.xml"]
    
    puts "処理対象ファイル: #{files.length}件"
    
    files.each do |input_file|
      base_name = File.basename(input_file, ".*")
      output_file = "#{output_dir}/#{base_name}#{target_extension}"
      
      puts "\n----- ファイル処理: #{input_file} -> #{output_file} -----"
      
      if @data_manager.convert_file(input_file, output_file)
        success_count += 1
      else
        failure_count += 1
      end
    end
    
    puts "\n===== バッチ処理完了 ====="
    puts "処理結果: 成功 #{success_count}件, 失敗 #{failure_count}件"
    
    { success: success_count, failure: failure_count }
  end
end

# 使用例

# サンプルデータの作成
sample_data = [
  { "id" => 1, "name" => "田中太郎", "email" => "tanaka@example.com", "age" => 30 },
  { "id" => 2, "name" => "鈴木花子", "email" => "suzuki@example.com", "age" => 25 },
  { "id" => 3, "name" => "佐藤一郎", "email" => "sato@example.com", "age" => 40 }
]

# 各形式へのエクスポート例
manager = DataManager.new

puts "===== CSVエクスポートのテスト ====="
manager.export_data(sample_data, "users.csv")
puts

puts "===== JSONエクスポートのテスト ====="
manager.export_data(sample_data, "users.json")
puts

puts "===== XMLエクスポートのテスト ====="
manager.export_data(sample_data, "users.xml")
puts

# ファイル間の変換
puts "===== 形式変換のテスト ====="
manager.convert_file("users.csv", "users_from_csv.json")
manager.convert_file("users.json", "users_from_json.xml")
puts

# バッチ処理のテスト
if Dir.exist?("data") && Dir.exist?("output")
  puts "===== バッチ処理のテスト ====="
  batch = BatchProcessor.new(manager)
  batch.process_directory("data", "output", ".json")
else
  puts "バッチ処理のテストをスキップします(data/outputディレクトリが必要です)"
end

解説

この実装では、モジュールを使ったインターフェースとファクトリーパターンを活用して、異なるデータ形式を処理するシステムを設計しています。

  1. DataProcessor モジュール(インターフェース)

    • データを読み込む(load)、書き出す(dump)、検証する(validate)メソッドを定義
    • これらのメソッドをすべての具体的なプロセッサーで実装することを要求
  2. 具体的なプロセッサークラス

    • CsvProcessor: CSV形式のデータを処理
    • JsonProcessor: JSON形式のデータを処理
    • XmlProcessor: XML形式のデータを処理
    • 各クラスはDataProcessorインターフェースを実装
  3. DataProcessorFactory(ファクトリークラス)

    • ファイル拡張子や内容に基づいて適切なプロセッサーを作成
    • 新しい形式を追加する際の中心的なポイント
  4. DataManager(管理クラス)

    • ファイルのインポート/エクスポート/変換を処理
    • プロセッサーを使ってデータを操作
    • エラー処理とデータ検証を管理
  5. BatchProcessor(バッチ処理クラス)

    • 複数のファイルを一括で処理
    • 異なる形式間の変換を自動化

この設計の主な利点は以下の通りです:

  • 拡張性: 新しいデータ形式に対応するには、新しいプロセッサークラスを追加し、ファクトリーを更新するだけで済みます。
  • 一貫性: すべてのプロセッサーが同じインターフェースを実装するため、統一的に扱えます。
  • 分離: データの読み込み、検証、変換、エラー処理などの責任が明確に分かれています。
  • 再利用性: 同じプロセッサーをインポート、エクスポート、変換など異なる目的に使用できます。

この例では、モジュールを使ったインターフェースの実装方法、ファクトリーパターンの活用、そして多態性(ポリモーフィズム)を生かした設計方法を示しています。各データ形式に対して専用のプロセッサーを用意しつつ、それらを統一的に扱えるようにすることで、柔軟性と保守性に優れたシステムを実現しています。

まとめ

この教材では、Rubyにおける継承とポリモーフィズムについて学びました。

継承とは

継承は既存のクラス(親クラス)の特性を新しいクラス(子クラス)が受け継ぐ仕組みです。Rubyでは<記号を使って継承関係を表現します。継承によってコードの再利用性が高まり、共通機能を一箇所で管理できるようになります。

superキーワード

superキーワードを使うと、子クラスから親クラスのメソッドを呼び出すことができます。これにより、親クラスの機能を拡張しつつ、既存の機能も活用できます。

メソッドのオーバーライド

子クラスで親クラスと同名のメソッドを定義することで、親クラスのメソッドを上書き(オーバーライド)できます。これにより、継承した機能をカスタマイズできます。

ポリモーフィズム

ポリモーフィズム(多態性)は、同じインターフェース(メソッド名)で異なる実装を持つことができる機能です。Rubyでは、クラスの継承関係やダックタイピングによってポリモーフィズムを実現します。

Rubyにおけるインターフェースと抽象クラス

Rubyには明示的なインターフェースや抽象クラスの機能がありませんが、モジュールや基底クラスを工夫して同様の機能を実現できます。モジュールを使ったインターフェース的な実装や、メソッドの実装を子クラスに要求する抽象クラスパターンが一般的です。

実践的な設計

実際のアプリケーション開発では、継承とポリモーフィズムを活用して、拡張性と保守性に優れた設計を行うことが重要です。支払い処理システムやデータ変換システムなどの実例からわかるように、適切な抽象化とインターフェースの設計により、柔軟で堅牢なシステムを構築できます。

Railsでの活用

Railsでは、継承とポリモーフィズムが随所に活用されています。例えば、ActiveRecordのモデル階層や、コンサーンを使った機能の共有などがその例です。これらの概念を理解することで、より効果的なRailsアプリケーションを開発できるようになります。

オブジェクト指向プログラミングの核心は「抽象化」と「多態性」にあります。適切な抽象化によってコードの再利用性と柔軟性を高め、多態性によって様々な状況に対応できる堅牢なシステムを構築することができます。Rubyは、これらの概念を直感的かつ柔軟に表現できる言語であり、Ruby on Railsはこれらの概念を活用した優れたフレームワークです。

これらの知識を活かして、より良いRuby/Railsアプリケーションを開発していきましょう!!!!

Discussion