3
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

posted at

updated at

Rubyで作るファーストクラスコレクション

はじめに

 「現場で役立つシステム設計の原則 変更を楽で安全にするオブジェクト指向の実践技法」という書籍を読んでいるのですが、その中でファーストクラスコレクションという考え方を知りました。

 この書籍の中ではサンプルコードがJavaで書かれているのですが、私は普段Rubyを使っているため、Rubyでの実装方法について考えて見ようと思います。

ファーストクラスコレクションとは

 配列をラップしたクラスのことで、対象の配列に対する処理を全て集約したクラスになります。別名コレクションオブジェクトとも呼ばれるそうです。

 配列周りは、以下のように複雑な処理が行われることが多いです。

  • for文などのループ処理
  • 配列やコレクションの要素の数が変換する(可能性がある)
  • 個々の要素の内容が変化する(可能性がある)
  • 0件の場合の処理
  • 要素の最大数の制限

 これらの処理を全てファーストクラスコレクションの中へ集約し、コードを整理します。

サンプルコード
 storesという配列に関する処理を、全てStoresクラスの中にまとめています。

class Stores
  def initialize(stores)
    @stores = stores
  end

  def size
    return @stores.size
  end

  def show_all
    if @stores.empty?
       puts '店舗が存在しません。'
       return
    end

    @stores.each do |store|
      puts store.name
    end
  end
end

メリット

 配列の複雑さを専用の小さなクラスに閉じ込めることで、対象の配列に関する処理がプログラムの至る所に散らばらず、整理することができます。

その結果

  • メンテナンス性が向上する
  • プログラムが分かりやすくなる

などの効果が得られます。

注意点

 コンストラクタで与えた配列のオブジェクトに対して、Setterを作らないようにする方が望ましいです。要素の追加や削除が外部から行えるようになってしまうため、ラップした意味がなくなってしまいます。

実装方法

 それでは早速実装していきたいと思います。

その1: Arrayを継承させたクラスを作る

 まず単純にArrayクラスを継承させた方法を考えてみます。

class Stores < Array
  def output_store
    self.each { |store|
      puts "店名: #{store}"
    }
  end
end

stores = Stores.new(['A店', 'B店', 'C店'])
puts stores.size
# => 3
stores.output_store
# => 店名: A店
# => 店名: B店
# => 店名: C店

 これで、固有のビジネスロジックを持った上で、Arrayが備える機能も保持したクラスを作ることができました。

  • この実装の問題点

 しかし、このクラスには以下の様な問題点もあります。

stores1 = Stores.new(['A店', 'B店', 'C店'])
stores2 = Stores.new(['D店', 'E店', 'F店'])
puts stores1.class
=> Stores
puts stores2.class
=> Stores

stores3 = stores1 + stores2
puts stores3.class
=> Array
# => 3

stores3.output_store()
# => NoMethodError

 Storesのオブジェクト同士を+で連結することによって、元のArrayクラスに戻ってしまいます。
 Arrayクラス自体機能が多いため、全ての機能を継承させることで、不要な機能まで搭載してしまう不利益の方が多いという印象です。

その2: Arrayから必要なメソッドのみラップする

 今度はArrayから必要なメソッドだけラップする形を考えます。

class Stores
  def initialize(stores)
    @stores = stores
  end

  def size
    @stores.size
  end

  def output_store
    @stores.each { |store|
      puts "店名: #{store}"
    }
  end
end

stores = Stores.new(['A店', 'B店', 'C店'])
puts stores.size
# => 3
stores.output_store
# => 店名: A店
# => 店名: B店
# => 店名: C店

 これで対象のクラスにとって必要な機能のみを搭載した、最小構成クラスを作ることができました。

  • このクラスの問題点

 このパターンの場合だと、Arrayクラスからラップしたいメソッドが増えるたびに、新たなメソッドを定義することになります。

class Stores
  def initialize(stores)
    @stores = stores
  end

  def size
    @stores.size
  end

  def each
    @stores.each { |store| yield(store) }
  end

  def select
    @stores.select{ |store| yield(store) }
  end

  def index(obj)
    @stores.index(obj)
  end

  .
  .
  .

end

ラップ用のクラスを都度定義するのは面倒ですし、コードの量も増えて見にくくなりそうです。

その3: 委譲を使用したやり方

 そこで今度は委譲という考え方を使ってみます。Rubyの場合、Forwardableモジュールを使うことで実現できるみたいです。

require 'forwardable'
class Stores
  extend Forwardable
  def_delegators :@stores, :size

  def initialize(stores)
    @stores = stores
  end

  def output_store
    @stores.each { |store|
      puts "店名: #{store}"
    }
  end
end

stores = Stores.new(['A店', 'B店', 'C店'])
puts stores.size
# => 3
stores.output_store
# => 店名: A店
# => 店名: B店
# => 店名: C店

これでコードが大分スッキリしましたね。

実装部分で参考にした書籍

 「オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方」の第8章を参考に書いてみました。

 オブジェクト指向関連の書籍で、Rubyを題材にしてくれているものは少ない気がするので、Rubyでプログラミングを書きはじめて、オブジェクト指向を学びたい方にはぜひオススメしたいです。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
3
Help us understand the problem. What are the problem?