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

しかし、その割に業務ではダメな継承の使い方をして、スパゲッティコードになるのをしばしば見かけます。

これは、継承の「良い使い方」はデザインパターンのようなリスト化されているのに、継承の「悪い使い方」はリスト化されていないからかもしれません。

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

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

TL;DR

継承は基本的に禁止(Mix-inも)

以下の2つだけはOK
- インターフェースの実装(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

派生クラスでオーバーライドされる可能性があると、基底クラスをうかつに変更できなくなります(push メソッドが基底クラスから無くなったら何が起きる?)。

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

# 自動ソートされる配列を作ったぜ!
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. 親クラスにメンバ変数がある)
  • 派生クラスが、基底クラスのメンバ変数を参照している

といった場合は、悪い兆候です。設計を考え直しましょう。

また、Strategy パターン (や高階関数)なら、継承を使わずにTemplate Methodパターンと実現できることがあります。

Ruby の Mixin について

Ruby の Mixin も継承の一種です。そのため、「悪い継承」のパターンは Mixin にも当てはまります。例えば「共通で使うメソッドを提供するだけの Mixin」は避けましょう

また、Ruby on Railsの場合は、フックやValidatorなどのMixin以外の手段も提供されているので、Mixinは基本的に禁止(Mixinを使う前に他の方法がないか調べる)くらいに思ってもいいかもしれません。

まとめ

継承は基本的に禁止(Mix-inも)

以下の2つだけはOK
- インターフェースの実装(implements)
- TemplateMethod パターン

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

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

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

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.