継承について

問題の定義

  • データ構造を共通化したい
  • 振る舞いを共通化したい

継承の定義

wikipedia - # 継承 (プログラミング) より

コンピュータプログラミングにおける継承(けいしょう、英: inheritance)とは、任意のオブジェクトの特性を、他のオブジェクトの特性の基礎にするためのメカニズムと定義されている。

基礎にされる継承元は親、その継承先は子と呼ばれて、状態と機能と定数と注釈などが引き継がれるが、コンストラクタとデストラクタは対象外になる。その親と子の関係を、クラスベースOOPはスーパークラスとサブクラスの関係で、プロトタイプベースOOPはプロトタイプとクローンの関係で導入している[1]。

is-a関係

wikipedia - # is-a より

知識表現、オブジェクト指向プログラミング、オブジェクト指向設計では、is-aとは、あるクラスBはもう一つのクラスAのサブクラスである(また、AはBのスーパークラスである)という関係である。

言い換えれば、"BはAである"は通常、概念Bは概念Aの特化であり、概念Aは概念Bの汎化であることを意味する。例として、「フルーツ」は「リンゴ」、「オレンジ」、「マンゴー」などの汎化である。リンゴはフルーツである(is-a) (Apple is a fruit.)と言える。

オブジェクト指向プログラミングではis-a関係は継承という概念の中で使われる。たとえばリンゴは、「果肉に種が入った植物」に属するというような、フルーツすべてに共通するプロパティをすべて継承するといって差し支えない。

is-a関係とは、異なる種類の階層の性質をもつ関係にhas-aがある。 オブジェクトと従属するオブジェクトの論理関係がis-aか、それともhas-aなのか、いつもはっきりと決定できるものではない。この曖昧さが、is-aのようなメタ言語的な用語を生み出した。

has-a関係

wikipedia - has-aより

has-aとは、データベース設計やオブジェクト指向プログラミングアーキテクチャにおいて、(複合型と呼ばれる)もう一つのオブジェクト(構造体の一部またはメンバ)に属するオブジェクトの関係[1]であり、所有者のルールに準じて振る舞うものである。

多数のhas-a関係は相まって所有階層を形成する。これは異なる種類の階層(派生型)を構成するis-a関係と対比される。あるオブジェクトとそれに従属するオブジェクトとの最も論理的な関係がhas-aであるのか、それともis-aであるのかは必ずしも明確に決められない。そのような判断上の混乱があるため、これらのメタ言語的用語が生まれたとも言える。日本語では集約やコンポジション集約関係を意味する。

結論

悩んだら継承を使わない

  • is-a関係が長期的に保たれるという確信がなければ継承の出番ではない
  • 継承でしかできないことはあまりない割に、問題を複雑化させる性質があるのでコスパが悪い
  • 特にデータの構造・振る舞いの共通化を目的とした継承は全く割に合わない

継承を使う場合

is-a関係が長期的に保たれる(=リスコフの置換原則に違反しない)と確認できる場合

  • つまり、リスコフの置換原則に違反しない:
    • コード内のスーパークラス(基本型/基底型/スーパータイプ)のオブジェクトはすべてサブクラス(派生型/サブタイプ)のオブジェクトへ置換可能であること
    • サブクラスの事前条件はスーパークラスと同一か、それより弱い
    • サブクラスの事前条件はスーパークラスと同一か、それより強い
    • 不変条件はサブクラスでも維持されなければならない
    • サブクラスで独自の例外を投げてはならない
  • ざっくりいうと、実装の大部分がスーパークラスにあり、サブクラスにわずかな差異のみを表現する実装がある場合

例外

  • ライブラリによって提供されている利用手段が継承だった場合
  • (Railsの) ActiveRecordによるORM機能を利用するため ※
    • ActiveRecord::Base.table_name=メソッドで回避できるかもしれない
      • やってみないとわからないが
    • STI(Single Table Inheritance)を利用する場合
  • 使うにしても親子以上の継承をしない(孫サブクラスが出来てはならない)

リスコフの置換原則(LSP)に違反するのはどうしてダメなのか

A.オープンクローズドの原則(OCP)に違反してしまうから

class Item
	def buyable?
		#...
	end
end

def DiscountingItem < Item
	# buyable?実行前に必要(スーパークラスにはないメソッド)
	def keep_stock!
		#...
	end
end

class LimitedItem < Item
	# buyable?実行前に必要(スーパークラスにはないメソッド)
	def keep_stock!
		#...
	end
end

module ItemFactory
	def self.create(param)
		case param.type
		when :discounting
			DiscountingItem.new(param)
		when :limited
			LimitedItem.new(param)
		else
			Item.new(param)
		end
	end
end

class Controller


	def buyable?(item)
		# OCP違反1: ItemFactory.createメソッドでもこのメソッドでも出し分けが必要になっている
		# OCP違反2: サブタイプが増えるとItemFactory.createメソッドも修正する必要がある
		if item.type == :discounting || item.type == :limited
			item.keep_stock
		end
		# LSP違反: itemがサブクラスのオブジェクトである場合、スーパークラスのオブジェクトに比べて、buyable?を実行するための事前条件が強くなっている
		item.buyable?
	end
end

controller = Controller.new
item = controller.create_item
cont

継承の難しさ

品質の低下を招く構造になっている

flowchart BT
sp[superClass]
sb1[SubClass1]
sb2[SubClass2]
sb3[SubClass3]

sb1-->sp
sb2-->sp
sb3-->sp
  • リスコフの置換原則を満たしていればスーパークラスを安全に変更することができるか、違反しないためには細心の注意が必要(ここがそもそも努力頼みで難しい)
    • リスコフの置換原則の違反に気がつくのが難しい
      • いつの間にか違反する => サブクラスがどんどん太る => スーパークラスの特殊化とは呼べるものではなくなる =>サブクラスの区別して利用する圧が強くなる => サブクラスの都合でスーパークラスを変更する時がやってくる => 兄弟サブクラスを壊さないように注意が必要となる => 以下無限ループ
    • スーパークラスにはある振る舞いが、サブクラスによっては不要な振る舞いが登場した瞬間にサブクラスには余分な責務が発生(凝集度の低下)したことになる
      • 回避するには継承を最初から使わない、という方法しかない
      • よかれと思って共通化したロジックが返って保守性の低下を招く

代替手段

詳しくは長くなるので別途ドキュメントにしようと思います

  • データ構造を共通化したい
    • => 委譲
  • 振る舞いを共通化したい
    • => 委譲orミックスイン

参考

  • リスコフの置換原則(LSP)をしっかり理解する https://qiita.com/yuki153/items/142d0d7a556cab787fad
  • 君の継承の使い方は間違っている https://qiita.com/tonluqclml/items/c0110098722763caa556