Menu
CoddyTech

Forwardableによる委譲

CoddyのRubyジャーニー「オブジェクト指向プログラミング」セクションの一部。レッスン 46/57。

委譲とは、あるオブジェクトが別のオブジェクトを継承するのではなく、そのオブジェクトに仕事の一部を任せることです。曲の配列を持つプレイリストは、配列そのものであるプレイリストより安全です。外部のコードが使えるのは、転送するように選んだメソッドだけだからです。

class Playlist
  def initialize
    @songs = []
  end

  def add(song)
    @songs << song
    self
  end

  def size
    @songs.size
  end

  def first
    @songs.first
  end
end

list = Playlist.new.add("Intro").add("Dune")
puts list.size
puts list.first

出力:

2
Intro

転送メソッドを一つずつ手書きするのは繰り返しの作業です。標準ライブラリのForwardableを使えば、それを実現できます。extend Forwardableを呼び出し、続いてdef_delegators :@songs, :size, :firstを記述すると、これらの呼び出しは@songs内のオブジェクトに転送されます。

require 'forwardable'

class Playlist
  extend Forwardable
  def_delegators :@songs, :size, :first, :last, :empty?, :include?

  def initialize
    @songs = []
  end

  def add(song)
    @songs << song
    self
  end
end

list = Playlist.new.add("Intro").add("Dune")
puts "#{list.size} #{list.first} #{list.last}"
puts list.include?("Dune")
puts list.empty?

出力:

2 Intro Dune
true
false

def_delegator :@songs, :push, :add は、1つのメソッドを新しい名前で転送します。転送しないすべての処理(たとえば delete や clear)は、オブジェクトが引き続き完全に制御します。

require 'forwardable'

class Queue2
  extend Forwardable
  def_delegator :@jobs, :push, :enqueue
  def_delegator :@jobs, :shift, :next_job
  def_delegators :@jobs, :size

  def initialize
    @jobs = []
  end
end

q = Queue2.new
q.enqueue("a")
q.enqueue("b")
puts q.next_job
puts q.size
puts q.respond_to?(:clear)

出力:

a
1
false

クラスが別のクラスの振る舞いの一部だけを必要とする場合は、委譲(「has-a」)を優先しましょう。Arrayを継承すると、プレイリストのルールを壊す可能性のあるclearやdelete_atなど、数十ものメソッドが公開されてしまいます。

class Playlist < Array      # clear を含む Array のすべてのメソッドが public になる
end

class Playlist              # 委譲したものだけが public になる
  extend Forwardable
  def_delegators :@songs, :size, :each
end
challenge icon

チャレンジ

簡単

Forwardableを使ってPlaylistクラスを完成させてください。@songsに曲を保持します。各曲は[title, seconds]のペアです。def_delegatorsでsize、empty?、firstを委譲し、def_delegatorでpushをaddという名前で委譲してください。独自のメソッドdurationは合計時間をm:ss形式で返し、titlesはタイトルを, で連結して返します。

用意されたコードは、入力の各行(title,seconds)についてaddで曲を追加し、その後、曲数、最初のタイトル、タイトル一覧、再生時間、およびプレイリストがclearに応答するかどうかを出力します。

コードはplaylist.rbに記述します。main.rbには用意された入出力コードが含まれており、編集できません。

自分で試してみよう

require_relative 'playlist'

# 提供された入出力コード:そのままにしてください
input = []
while (line = gets)
  input << line.chomp
end
list = Playlist.new
input.each do |line|
  title, seconds = line.split(",")
  list.add([title, seconds.to_i])
end
puts "songs: #{list.size}"
puts "first: #{list.empty? ? "none" : list.first[0]}"
puts "titles: #{list.titles}"
puts "duration: #{list.duration}"
puts "can clear: #{list.respond_to?(:clear)}"
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン

自分で練習してみよう: Rubyオンラインコンパイラ