zdogma's diary

徒然なるエンジニアな日々。

ruby で継承と委譲の使い分けを考える

ことはじめ

継承と委譲に関して、それぞれなんとなく理解はしているものの、どう使い分けるのがよいのか、イマイチ実感が湧いていなかったので、整理がてらブログに書いてみます。

それぞれの定義

継承

既存の親クラスの構造(属性や振る舞い含む)をまるごと受け継ぎ、新しいクラスとして再定義する。

委譲

特定の振る舞いを別のクラスのメソッドに委ねる。

具体例で考える

継承、委譲の使い分けを、具体例を使いながら検討していきます。 ここでは、下記のようなエンジニアマネージャのクラスを考えます。

class EngineerManager
  def initialize
  end

  # エンジニアに共通する振る舞い
  def code
  end

  def debug
  end

  def test
  end

  # マネージャに共通する振る舞い(ただし assign は許可しない)
  def coach
  end

  def motivate
  end

  # 社員に共通する振る舞い
  def eat(type = 'lunch')
  end

  def attend(meeting_type)
  end
end

ここで想定するエンジニアマネージャは、下記のようなロールだとします。

  1. エンジニアとしての振る舞いをする
  2. マネージャとしての振る舞いをする
    ※ただし、他のマネージャ職のメンバーとは異なり、アサイン組み替えの権限はない
  3. 社員としての振る舞いをする

基本的な方針としては、下記の記事にもあるように、「is-a」の関係が綺麗に成り立つ場合は継承を使うのが良いと僕も思います。

osa.hatenablog.com

継承は安易に用いると容易に複雑化し、バグの温床になりえるものの、その系において "よく" 抽象化された親クラスのもとで継承関係を作ることができれば、とても強力に機能すると思います。

例えば今回の例だと、エンジニアマネージャは社員の部分集合であるため、継承を用いてよいケースだと考えます。 社員に対して認められる振る舞いや属性は、漏れなくエンジニアマネージャも備えるべきです。

逆にエンジニアやマネージャに対してはそうではなく、それぞれの一部の振る舞いがエンジニアマネージャにも備わっているという状態であるため、まるごと受け継ぐ継承は不向きです。 エンジニアマネージャは "(エンジニアやマネージャが担う振る舞いのうち)どの振る舞いができるか" にのみ焦点を当て、その処理の具体的な内容は関知しない、という状態にします。

上記の思想のもと、下記のように実装してみました。

class EngineerManager < Worker
  extend Forwardable
  def_delegators :@engineer, *%i( code debug test )
  def_delegators :@manager, *%i( coach motivate )

  def initialize(name)
    @engineer = Engineer.new # Engineer.find(name: name)
    @manager = Manager.new # Manager.find(name: name)
  end
end


class Worker
  def initialize
  end

  def eat(type = 'lunch')
    puts "#{type} を食べました"
  end

  def attend(meeting_type)
    puts "#{meeting_type} に出席しました"
  end
end


class Engineer
  def initialize
  end

  def code
    puts 'code を書きました'
  end

  def debug
    puts 'debug をしました'
  end

  def test
    puts 'test をしました'
  end
end


class Manager
  def initialize
  end

  def coach
    puts 'コーチングしました'
  end

  def motivate
    puts 'モチベートしました'
  end

  def assign
    puts 'アサイン組み換えしました'
  end
end
bob = EngineerManager.new('Bob')
puts bob.code # => code を書きました
puts bob.attend('朝会') # => 朝会 に出席しました
puts bob.coach # => コーチングしました
puts bob.assign # =>  undefined method `assign' for #<EngineerManager:0x007fe6ec8e3a70>

おわりに

なんとなく具体例を考えてみると思考が整理された気がします。
まだまだ思考実験の域を出ていないので、ツッコミなどありましたらぜひお願いします!

参考サイト