普段何気なく使っている技術や言葉について、表面的な知識にせず、しっかりとイメージできるまで自分なりに調べて整理し、理解するシリーズ。
抽象を見つけ、クラスを分ける
どのようにして抽象的なスーパークラスを見つけ、抽象と具象を分けていくかを考えて整理してみます。
車を例に抽象を見つける方法を考えてみます。例えば、全ての車が自家用車しかないアプリケーションにおいてはクラス名はCarで問題ありませんが、自家用車とバスの2つがある場合は変わってきます。
自家用車とバスはどちらも車の一種であり、それぞれ共通化できる抽象と特化できる具象に分けることができます。そしてこの自家用車とバスを車が特化したものとして捉えるのは、継承のルールである凡化-特化の関係として正しいと考えます。
こうしてスーパークラスである車と、そのサブクラスである自家用車とバスに分けることができます。
抽象的なスーパークラスをつくる
先程の継承から、以下のクラス図のようにCarがPrivateCarとBusのスーパークラスとなっています。
この継承構造によってCarが共通の振る舞いを持ち、PrivateCarとBusがそれぞれ特化した振る舞いを追加できます。
以下のコードでは、スーパークラスであるCarは共通のsizeを含み、サブクラスであるBusは特有のpaymentを含みます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | class Car attr_reader :size def initialize(args={}) @size = args[:size] end end class PrivateCar < Car def initialize(args) super(args) end end class Bus < Car attr_reader :payment_style def initialize(args) @payment_style = args[:payment_style] super(args) end end |
テンプレートメソッドパターン
Carのinitializeメソッドにdefault_seat_colorとdefault_seat_numberというデフォルト値となる2つのメッセージを追加してみます。
これらのメッセージを用意しオーバーライドすることで、サブクラスに特化した状態や振る舞いを定義する機会を与えることができます。
スーパークラス内で基本の構造を定義し、サブクラスにメッセージを送る手法は、テンプレートメソッドパターンとして知られています。
以下のコードでは、サブクラス両方ともdefault_seat_numberを実装し、default_seat_colorはスーパークラスにのみ実装しています。それぞれのサブクラスは座席数に独自の初期値を用意するものの、座席の色はスーパークラスの共通の初期値を継承します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | class Car attr_reader :size def initialize(args={}) @size = args[:size] @seat_color = args[:seat_color] | default_seat_color @seat_number= args[:seat_number] | default_seat_number end def default_seat_color ‘black’ end end class PrivateCar < Car def default_seat_number 4 end end class Bus < Car attr_reader :payment_style def initialize(args) @payment_style = args[:payment_style] super(args) end def default_seat_number 24 end end |
これによって、全ての車において座席の色は同じ初期値が使われ、座席数はサブクラスによって異なる初期値が使われるようになりました。
全てのテンプレートメソッドを実装する
Carによるinitializeメソッドではdefault_seat_numberをサブクラスに送りますが、Car自体には実装れていません。そのため例えば新しくトラックを追加した場合、以下のような問題を引き起こす場合があります。
1 2 3 4 5 6 7 8 9 10 | Class Truck < Car def default_seat_color ‘gray’ end end truck = Truck.new(size: ‘M’) # undefined local variable or method ‘default_seat_number’ |
これは、今後Carから新しくサブクラスを作っていくタイミングで発生しうるエラーです。この問題は一見コードを見るだけではわからない要件をサブクラスに課していることです。
Carにdefault_seat_numberが書かれている限り、サブクラスはdefault_seat_numberを必ず実装している必要があります。
この気付きにくい要件を前もって知らせるためには、テンプレートメソッドパターンを使うどのクラスにおいても、送信するメッセージの全てに実装を用意します。
以下のようにサブクラスがメッセージを実装する必要があるとスーパークラスに明示的に示すことで、開発者は要件に気付くことができます。例え実装時に気づかなくても明確なエラーメッセージを用意することで、実行時に開発者はそのエラーメッセージから気付くことができます。
1 2 3 4 5 6 7 8 9 10 | Class Car … def default_seat_number raise NoImplementedError “This #{self.class} cannot respond to:” end end |
参考: