オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方を読んで、フックメッセージについて学習したので解説します。
フックメッセージとは?
サブクラスがそれに合致するメソッドを実装することで情報を提供できるようにするための専門のメソッド
フックメッセージを使う場面の例
抽象的な説明をされても全く理解できないと思うので、実際の例を使って解説していきます。
悪い例
ありえそうな悪い例から解説を始めていきます。
継承関係は図のようになっています。
class Bicycle
attr_reader :size, :chain, :tire_size
def initialize(args={})
@size = args[:size]
@chain = args[:chain] || default_chain
@tire_size = args[:tire_size] || default_tire_size
end
def spares
{
tire_size: tire_size,
chain: chain
}
end
def default_chain
'10-speed'
end
def default_tire_size
raise NotInmplementedError
end
end
class RoadBike < Bicycle
attr_reader :tape_color
def initialize(args)
@tape_color = args[:tape_color]
# 親クラスがinitializeに反応するということを知っている -> 依存
super(args)
end
def spares
# 親クラスでHashを返すsparesが実装されているといことを知っている -> 依存
super.merge({ tape_color: tape_color })
end
def default_tire_size # <- サブクラスの初期値
'23'
end
end
class MountainBike < Bicycle
attr_reader :front_shock, :rear_shock
def initialize(args)
@front_shock = args[:front_shock]
@rear_shock = args[:rear_shock]
# 親クラスがinitializeに反応するということを知っている -> 依存
super(args)
end
def spares
# 親クラスでHashを返すsparesが実装されているといことを知っている -> 依存
super.merge({ rear_shock: rear_shock })
end
def default_tire_size # <- サブクラスの初期値
'2.1'
end
end
spares
メソッドとinitialize
メソッドに注目してください。
コードを見るとわかるように親クラスに依存していることがわかると思います。
もし新しいサブクラスRecumbentBike
が追加された時に、プログラマーがinitialize
内でsuper
を送り忘れたとしましょう。
class RecumbentBike < Bicycle
attr_reader :flag
def initialize(args)
@flag = args[:flag]
# ここでsuperを送るのを忘れていた
end
def spares
super.merge({ flag: flag })
end
def default_tire_size
'28'
end
end
bent = RecumbentBike.new(flag: 'tall and orange')
bent.spares
# => {tire_size => nil, <- 初期化されていない
# chain => nil,
# flag => 'tall and orange'
# }
super
の送り忘れによってエラーの原因とは遠く離れたところで起こる可能性があります。その時のデバッグは大変なものになります。
他にもspares
メソッド内でsuper
を送り忘れると、ハッシュの形が間違ってしまい、エラーが起こります。
Bicycle
クラスとそのサブクラスの設計に携わったプログラマーならこのようなミスを起こすことは少ないかもしれませんが、全く設計に携わっていないプログラマーが今回のようにRecumbentBike
を追加した時にエラーが起こる可能性が上がります。
どんなプログラマーでもsuper
を送り忘れることは十分に考えられるのでこの問題に対処しなければいけません。
いい例
悪い例のようなミスを避けるためにフックメッセージを使います。
サブクラスがそれに合致するメソッドを実装することで情報を提供できるようにするための専門のメソッド
フックメッセージとは上記のような意味でしたが、実際にはどのように使われるのか下のコードを見てください。
class Bicycle
attr_reader :size, :chain, :tire_size
def initialize(args={})
@size = args[:size]
@chain = args[:chain] || default_chain
@tire_size = args[:tire_size] || default_tire_size
+ post_initialize(args) # フックメッセージを送る
end
+ def post_initialize(args)
+ nil # サブクラスでオーバライドするためのメソッドを定義する
+ end
def spares
{
tire_size: tire_size,
chain: chain
}
+ .merge(local_spares)
end
+ def local_spares
+ # サブクラスでオーバーライドするためのメソッドを定義
+ {}
+ end
def default_chain
'10-speed'
end
def default_tire_size
raise NotInmplementedError
end
end
RoadBike
とMountainBike
でコードは変わらないのでRoadBike
だけを書きます。
class RoadBike < Bicycle
attr_reader :tape_color
+ def post_initialize(args)
+ # サブクラス側でフックメッセージを受け取りオーバーライドする
+ @tape_color = args[:tape_color]
+ end
+ def local_spares
+ # さっきまでsuper.mergeしていた部分を置き換える
+ # サブクラスでオーバライドする
+ { tape_color: tape_color }
+ end
def default_tire_size # <- サブクラスの初期値
'23'
end
end
このようにすることでサブクラスでsuper
しなくてもよくなりました。
- サブクラスの
Bicycle
への結合度が減った - サブクラスが
super
を送らなくてもよくなった - サブクラスが「スーパークラスが
spares
メソッドを実装していること」を知らないようになった
まとめ
ここまでくるとフックメッセージの
サブクラスがそれに合致するメソッドを実装することで情報を提供できるようにするための専門のメソッド
という意味がわかったのではないでしょうか?
今回の例では、post_initialize
とlocal_spares
をスーパークラスに定義だけして置いて、merge
などの処理はスーパークラスに記述して起きます。
同時にサブクラスでは具体的な処理をオーバーライドすることで、実現したい実装を行なっています。
フックメッセージを使うことによって、コードの見通しも良くなり、後からサブクラスを追加するプログラマーにとっても親切なものとなりました。