あかね
親クラスで例外を投げる?継承ってそういうことかと腹落ちした話
2026年07月19日
見出しはありません
要約を生成中...
私がたまに書くRailsの内容もShiftB内ではニッチかもですが需要があるんだなって伊藤さんのブログで知ったので、結構前の学びなんですがオブジェクト指向らしい設計について書いてみます🚀
感謝されてるなんて🥹🥹🥹
最近自己満な内容が多かったので反省しつつ今日はがっつり技術的なことを書きます!!
Railsのコードを読んでいて、親クラスにこんなメソッドが定義されているのを見つけました。
def destination
raise NotImplementedError
end最近の話ではないのですが最初に見たときは、「親クラスにメソッドはあるのに、呼ばれたら例外になる、、?どういうこと?」と思いました。
現在のプラットフォームで実装されていない機能が呼び出されたときに発生する例外
Rubyでは本来「未実装の機能」を表す例外ですが、子クラスで実装してほしいという意図を表現するために使われることもあります。
子クラスと親クラスで同じメソッドが定義していた時は、子クラスのメソッドが優先して実行されます。
そのルールを利用し、親クラスのメソッドが実行されると例外が発生されるように実装されてました。
つまり、子クラスでの定義を強制するということ
継承ってこういうふうに使うこともあるんですね〜🫣
デフォルト設定とか継承するクラスに共通する処理を親クラスのメソッドにするくらいの感覚でいたので、新パターン知って感動しました。
実際に見たクラスは、外部サービスへ通知を送るためのジョブでした。
通知の種類によって、
通知内容(本文)の作り方
通知の送信先(slackでいうチャンネルIDなど)
が異なります。
一方で、処理全体の流れはほとんど同じです。
データを受け取る
↓
通知用のデータに整形する
↓
指定された送信先へ通知するそこで親クラスが、この共通する処理の流れを持っていました。
イメージとしては、こんな形です。
class BaseNotificationJob
def perform(content)
payload = serializer_class.new(content).to_hash
notifier.post(**payload)
end
private
def serializer_class
raise NotImplementedError
end
def destination
raise NotImplementedError
end
def notifier
Notifier.new(destination:)
end
end
親クラスは、
シリアライザー(成形を担うクラス)を使って通知内容を作る
通知先を指定して送信する
という全体の流れを知っています。
ただし、
どのシリアライザーを使うのか
どこに通知するのか
については知りません。
そこを子クラスに任せています。
子クラス側では、通知ごとに異なる部分だけを定義します。
class OrderNotificationJob < BaseNotificationJob
private
def serializer_class
OrderNotificationSerializer
end
def destination
Settings.notification.order_channel
end
end
別の通知を追加する場合も、同じように子クラスを追加します。
class ErrorNotificationJob < BaseNotificationJob
private
def serializer_class
ErrorNotificationSerializer
end
def destination
Settings.notification.error_channel
end
end
通知を送る処理自体を、各クラスで毎回書く必要はありません。
子クラスは、その通知固有の情報だけを返せばよくなっています。
親クラスのメソッドは、次のようになっています。
def destination
raise NotImplementedError
end子クラスで実装し忘れた状態で処理を実行すると、例外が発生します。
単に親クラスにメソッドを定義しない場合でも、実行時にはエラーになります。
ただ、あえて親クラス側にメソッドを定義しておくことで、「このクラスを継承するなら、このメソッドを実装してくれ」という設計上の意図がコードから読み取れます。
このように、親クラスが処理全体の流れを定義して、処理の一部を子クラスに任せるという設計は、Template Methodパターンと呼ばれるらしいです。
今回の例では、perform が処理全体の流れを決めています。
def perform(content)
payload = serializer_class.new(content).to_hash
notifier.post(**payload)
endその中で使われる、
serializer_class
destinationだけを子クラスが実装します。
処理の骨格は変えず、通知ごとに異なる部分だけを差し替えている形です。
この設計を使わない場合、ひとつのクラスの中で通知の種類を判定する方法も考えられます。
case notification_type
when :order
serializer = OrderNotificationSerializer
destination = Settings.notification.order_channel
when :error
serializer = ErrorNotificationSerializer
destination = Settings.notification.error_channel
end最初はこれでも問題ないかもしれません。
ただ、通知の種類が増えるたびに条件分岐も増えていきます。
良い設計のパターン知らないとやっちゃいますよねぇ
今回の設計では、新しい通知を追加するときに、新しい子クラスを作って差分を定義できます。
既存の通知処理に条件を追加し続けるのではなく、それぞれの通知を別のオブジェクトとして扱っています。
オブジェクト指向って何が嬉しいんやろ?と思ってたし難しいな関数でええやんって思ってたけど、実際のいい設計に出会うと、変更される部分を閉じ込めて変更されない部分を守るために使うこともできて、そうすると便利なんだなと感じました。
これまで継承というと、同じコードを親クラスにまとめて、重複を減らすために使うイメージが強くありました。
もちろん今回も共通処理はまとめられています。
ただ、それだけではなく、
親クラスが処理の流れを決める
子クラスが変更される部分だけを担当する
子クラスに必要なメソッドの実装を要求する
という役割もあります。
単に同じコードを親クラスへ移動したというより、「ここは共通で、ここから先は通知ごとに変わる」という境界が、クラスの構造として表現されています。
今回のコードを読んで学んだのは、継承を使って、
共通する処理の流れを親クラスに持たせる
変化する部分だけを子クラスに任せる
NotImplementedErrorによってオーバーライドが必要なことを表現する
という設計でした。
コードとしてはそこまで複雑ではありません。
でも、共通部分と差分がきれいに分かれていて、通知を追加するときも既存の処理を大きく変更せずに済みます。
こういうコードを見ると、「オブジェクト指向って概念の話だけではなく、実務ではこういう形で使われるんやな」と腹落ちします。
継承の使いどころとして、かなり勉強になるコードでした。
最近私のことをふざけた腹黒BBAって思ってる人がいる気がして(被害妄想)、まともな記事書いてみました。
for 伊藤さん!Railsのキャッチアップともに頑張りましょう!!
昨日、Netflix契約してとあるドラマ見てたんですが、これやっぱハマるといくら時間あっても足りないので見るもんじゃないなって思いました笑
スクール生はこういうの目標達成するまで完全にやめた方がいいと再認識!
見る時間を勉強に充てるべき!断定しちゃいけんけど断定するw
私もハマるってわかってるので今月は見て(見るんかい)即解約して時間の使い方は考えようと思いました🐈
要約
コメント
こういう記事本当に助かります!
キャッチアップ頑張ります🔥