オブジェクト指向
ポエム
アンチパターン

君の継承の使い方は間違っている

オブジェクト指向はプログラミングの基本です。そして、継承はオブジェクト指向の基本的な操作ですから、プログラマーは呼吸をするように継承をできなくてはならないはずです1

しかし、その割に業務ではダメな継承の使い方をして、スパゲッティコードになるのをしばしば見かけます。継承の「良い使い方」はデザインパターンとしてリストアップされているのに、「悪い使い方」はまとまっていないからかもしれません。

そこで、自分だったらコードレビューで :thumbsdown: をつけるような「悪い継承の例」をリストにしました2

(なお、この記事は個人的な経験によるもので、裏付けがあるものではありません。ご意見やオススメ本があれば、コメントをお願いします。)

TL;DR

  • 継承は禁止
  • Mix-inも禁止
  • インターフェースの実装(implements) は使ってもいい(こともある)
  • TemplateMethod パターンは使ってもいい(こともある)

悪い継承

:thumbsdown: メソッド共有のための継承

共通して使うメソッドだから、基底クラスに定義しよう!と思ってはいけません。

# 悪い例

# 基底クラス
class Base
   # 何かのAPIを呼び出せるメソッド
   # どのサブクラスでも使うから、基底クラスで定義するのが便利だよね
   def call_some_api(params)
      ...
   end
end

# 子クラス
class Foo < Base
   def method
      ...
      call_some_api(params) # APIを呼び出そう
      ...
   end
end

# 別のクラス
class Bar < Base
   ...
   def method
      ...
      call_some_api(params) # このクラスでもAPIを呼び出そう
      ...
   end
end

基底クラスに共通メソッドを定義すると、基底クラスと派生クラスの間、(基底クラスを介して)派生クラス同士に依存関係が生じてしまいます。

  • 共通メソッドを変更・機能追加しづらくなる(複数の派生クラスへの影響を気にしなければならないので)
  • 逆に、共通メソッドが無駄に多機能になる(様々な派生クラスでの使い方に対応するために)

などの問題が起きます。

また、上記の例では共通メソッドが1つですが、実際の開発では2個3個10個と共通メソッドが増えていき 基底クラスが肥大化します(共通メソッドが別の共通メソッドを呼び出していたりすると悲惨です)。

単にクラス間で処理を共有したい場合は、適当な static メソッドを作りましょう(もちろん、ある程度複雑な処理ならクラスにします)。

module SomeAPI
   # 何かのAPIを呼び出せるメソッド
   def self.call_some_api(params)
      ...
   end
end

# 子クラス
class Foo < Base
   def method
      ...
      SomeAPI.call_some_api(params) # APIを呼び出そう
      ...
   end
end

# 別のクラス
class Bar < Base
   ...
   def method
      ...
      SomeAPI.call_some_api(params) # このクラスでもAPIを呼び出そう
      ...
   end
end

:thumbsdown: メンバ変数の共有のための継承

たまたま同じ型・名前のメンバ変数だからと言って、共通の基底クラスを作ったりしてはいけません。

# 悪い例
# UserとCarに共通のメンバ変数があるので基底クラスを設けているが、
# UserとCarは全く別種のクラスなので、共通の基底クラスがあるのはおかしい。
# (本当にユーザーと自動車を同列に扱うアプリケーションなら話は別ですが)

# 基底クラス
class Base
   # UserとCarで共通のメンバ変数を定義しよう
   def initialize(name, age)
      @name = name
      @age = age 
   end
end

# ユーザークラス
class User < Base
  def initialize(name, age, gender)
     super(name, age) # ユーザーには名前と年齢(age)があるから、親のコンストラクタを呼ぼう
     @gender = gender
  end
end

# 自動車クラス
class Car
   def initialize(name, age, vehicle_type)
     super(name, age) # 自動車には名前と年数(age)があるから、親のコンストラクタを呼ぼう
     @vehicle_type = vehicle_type
   end
end

「メソッド共有のための継承」に似ていますが、派生クラスへの影響があるため、メンバ変数を変更しにくくなります。

また、superの呼び出しは不吉な匂いでもあります。

別なクラスは、メンバ変数が同じであっても、別々に定義しましょう。

:thumbsdown: 機能追加・機能変更のための継承

元のクラスをカスタマイズするために継承するのもダメです。

# 配列
class Array
  def push(item)
    # 要素を追加するメソッド
  end

  def sort!
    # ソートするメソッド
  end

  # 他にも色々なメソッドがある
end

# 自動ソートされる配列を作ったぜ!
class AutoSortedArray(Array)
  def push(item)
    super(item)
    sort! # sort
  end
end

派生クラスでオーバーライドされる可能性があると、基底クラスを変更しにくくなります。

クラスをカスタマイズしたい時は継承ではなく委譲をします。つまり、内部に元クラスのインスタンスを持つ、ラッパーを作ります。

# 自動ソートされる配列を作ったぜ!
class AutoSortedArray(Array)
  def initialize
    @array = []  # 内部に元クラスのインスタンスを持つ
  end

  # カスタマイズしたメソッド
  def push(item)
    array.push(item)
    array.sort!
  end

  # 他にも必要なメソッドを元クラスのインスタンスに委譲する
  def [](index)
    array[index]
  end

  def each(&block)
    array.each(&block)
    self
  end
end

また、カスタマイズが頻繁に必要なのであれば、基底クラスをカスタマイズ前提で実装する(Template Method パターン)べきかもしれません。

良い継承

:ok_hand: インターフェースの継承(implements継承)

インターフェースはメソッドの名前・シグニチャのリストなので、継承しても問題は生じません(多分、大抵の場合は)3

:ok_hand: Template Method パターン

Template Method パターン では、子クラスは親クラスで指定されたメソッドを実装するだけなので安全です。

ただし、以下のようなごとが起きていれば、それは悪い兆候です。設計を考え直しましょう。

  • オーバーライドするメソッドの数が多い
  • オーバーライドするメソッドで、super (基底クラスの同名メソッド)を呼んでいる
  • オーバーライドするメソッドで、基底クラスの別のメソッドを呼んでいる
  • コンストラクタをオーバーライドしている(i.e. 親クラスにメンバ変数がある)
  • 派生クラスが、基底クラスのメンバ変数を参照している

また、Template Methodパターンではなく、Strategy パターン や高階関数を使うという手もあります。

Ruby の Mixin について

Ruby の Mixin も継承の一種です。そのため、「悪い継承」のパターンは Mixin にも当てはまります。

例えば、

  • 共通で使うメソッドを提供するMixinを定義する
  • (Rails)でControllerで共通で使うメソッドをApplicationControllerに定義する

といったことは避けましょう。

特にRuby on Railsでは、フックやValidatorなどのMixin以外のコード共有の手段が提供されているので、Mixinは基本的に禁止くらいに思ってもいいかもしれません。


  1. オブジェクト指向では無い言語・継承が無いオブジェクト指向言語もありますし、オブジェクト指向は禁止するべきなんて話もあります。とは言え、JavaやRubyで仕事をするなら継承は必須スキルです。 

  2. 「神クラス」「ドメインモデル貧血症」のようなアンチパターンもありますが、この記事では継承に関係するものだけを書きました。 

  3. Java系のプロジェクトでDIのためにインターフェースが量産されるのは、それはそれで問題だとは思いますが。