はじめに
「現場で役立つシステム設計の原則 変更を楽で安全にするオブジェクト指向の実践技法」という書籍を読んでいるのですが、その中でファーストクラスコレクションという考え方を知りました。
この書籍の中ではサンプルコードが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でプログラミングを書きはじめて、オブジェクト指向を学びたい方にはぜひオススメしたいです。