オブジェクト志向設計実践ガイドを読んで継承を使ったソースコードの共有化手法を学んだので内容を整理してみましたφ(..)
オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方
- 作者: Sandi Metz,?山泰基
- 出版社/メーカー: 技術評論社
- 発売日: 2016/09/02
- メディア: 大型本
- この商品を含むブログ (1件) を見る
はじめに
今回は、下記のような社員を表すクラスのコードを元に実際に継承によるコード共有を行ってみようと思いますφ(..)
class Staff attr_reader :name, :group def initialize(args) @name = args[:name] @group = args[:group] end def work do_preparation do_clean_up end # ... end
社員だけでなく管理者も追加して、独自のプロパティとメソッドを定義したいんだけど。。。
あまり考えたくないですが、このような仕様変更は結構ありそうですね・・・!! このような要求に関してどのようにコードを修正しますか??
ぱっと思いつくのはgrade
を追加して、その値によって振る舞いを定義する方法でしょうか?
class Staff attr_reader :name, :group, :grade, :assistant def initialize(args) @name = args[:name] @group = args[:group] @grade = args[:grade] # 管理者の場合、assistantを持つ @assistant = args[:assistant] end def work def do_preparation if grade == :admin # 管理者の場合はチェックを行う do_check else do_clean_up end end # ... end
一見良さそうに見えますが、このコードは下記のような問題を含んでいます。
- 新しい
grade
が追加された場合、work
にif文が追加されメンテナンス負荷が高い。 - 新しい
grade
が追加される度に、プロパティが追加され、使用するプロパティと使用しないプロパティが混在する恐れがある。
クラスは原則として単一の役割を持つ(単一責任)であることが良いと言われています。しかし今回の改修では、Staff
クラスが純粋なスタッフとしての定義と管理者としての定義という2つの役割を持ってしまっているのが原因のようですね。。。
継承とはなんぞや
継承を使ってコードを整理する前に、そもそも継承とは何なのかを少しおさらいしましょう。
継承とは、根本的に「メッセージの自動移譲」の仕組みにほかなりません。
オブジェクト指向実践ガイドより抜粋
継承とはスーパークラス(継承元クラス)からサブクラス(継承先クラス)へ、プロパティやメソッドを共有化する仕組みです。
サブクラスでは、スーパークラスのメソッドとプロパティが使えるだけでなく、スーパークラスへ定義したメソッドの上書き(オーバーライド)及び、独自のメソッドも定義することも出来ます!!
イメージとしては師匠(スーパークラス)から弟子(サブクラス)へ、礼儀や技を受け継ぎ、弟子は師匠から受け継いだ技だけでなく、独自に編み出した技も使えるイメージでしょうか(._.)
継承を使ってコードを整理してみる
では本題の継承を使ってコードを整理してみますφ(..)
現状のコードは、Staff
クラスの中に一般社員と管理者、2つの役割が存在してしまっているために煩雑になってしまっていましたね。
より抽象的なクラスを作成し、継承させてみる。
まずは、一般社員や管理者よりも抽象的な労働者Employee
クラスを作成してみましょう。その後、管理者Manager
のクラスを作成し、労働者クラスを管理者や一般社員に継承させてみましょう。
class Employee end class Staff < Employee attr_reader :name, :group, :grade, :assistant def initialize(args) @name = args[:name] @group = args[:group] @grade = args[:grade] # 管理者の場合、assistantを持つ @assistant = args[:assistant] end def work do_preparation if grade == :admin # 管理者の場合はチェックを行う do_check else do_clean_up end end # ... end class Manager < Employee end
一見この状態では、なにも変わっていないように見えますが、ここからStaff
クラスの抽象化可能な処理をEmployee
に移譲していきます。
※処理を全てEmployee
に移譲してから、Staff
、Manager
に振り分けるよりも、Staff
から抽象化出来そうな処理を吟味して、Employee
に移譲していった方が、抽象的で無い処理がEmployee
に残るリスクを回避することが出来ます。
まずは、プロパティから労働者Employee
に移譲してみましょう。
@name
、@group
、@grade
は、労働者であれば誰でも持っていると言えそうなので、Employee
に移譲しても良さそうです。また、@assistant
は管理者のみが持つ項目なので、Manager
に移します。
class Employee attr_reader :name, :group, :grade def initialize(args) @name = args[:name] @group = args[:group] @grade = args[:grade] end end class Staff < Employee def work do_preparation if grade == :admin # 管理者の場合はチェックを行う do_check else do_clean_up end end # ... end class Manager < Employee attr_reader :assistant initialize(args) # superを書くことで、サブクラス(Employee)の同名のメソッドを呼び出せる super @assistant = args[:assistant] end end
次にStaff
のwork
メソッドに取り組んでみます。メソッド内の作業前の準備do_preparation
は、Employee
に移して、他は個別にサブクラスに実装した方が良さそうですね。
class Employee attr_reader :name, :group, :grade def initialize(args) @name = args[:name] @group = args[:group] @grade = args[:grade] end def work do_preparation end # ... end class Staff < Employee def work super do_clean_up end # ... end class Manager < Employee attr_reader :assistant def initialize(args) super # Employeeのinitializeメソッドを実行 @assistant = args[:assistant] end def work super do_check end # ... end
最初に比べるとStaff
が純粋な社員としての役割となっていて大分良い感じになってきた気がします!!
しかし、この段階でも問題を含んでいます。。。
- Staffなのに
grade
がadmin
、Manager
なのにgrade
がstaff
等のチグハグな状態が起こる。 - 新しいサブクラスを追加する際に
super
を記載忘れると上手く動作しなくなってしまう。
このような問題を解決するために「テンプレートメソッド」、「フックメソッド」を使って更にリファクタリングしていきます。
テンプレートメソッドをつかってサブクラスにメソッド実装を促す。
「Staffなのにgrade
がadmin
、Manager
なのにgrade
がstaff
等のチグハグな状態が起こる。」をテンプレートメソッドというデザインパターンで対応してみたいと思います!!
テンプレートメソッドは、スーパークラスにメソッドを作っておいて、サブクラスでのオーバーライドを促すというものです。
実際にテンプレートメソッドを用いてリファクタリングしたのが下記コードです。Employee
にdefault_grade
メソッドを実装し、その中にERRORを発生処理を実装し、initialize
メソッドで@grade
の設定に使用することでサブクラスにdefault_grade
の実装を促しています。
これによって、Staff
、Manager
にて適切なgrade
が設定されるようになりましたφ(..)
class Employee attr_reader :name, :group, :grade def initialize(args) @name = args[:name] @group = args[:group] @grade = default_grade end def default_grade # ERRORを投げるメソッドを定義 raise NotImprementedError, "#{self.class}では、default_gradeメソッドを実装してください。" end def work do_preparation end # ... end class Staff < Employee def work super do_clean_up end def default_grade :staff end # ... end class Manager < Employee attr_reader :assistant def initialize(args) super # Employeeのinitializeメソッドを実行 @assistant = args[:assistant] end def default_grade :admin end def work super do_check end # ... end
フックメソッドを使ってスーパークラスからsuperを除外する。
「新しいサブクラスを追加する際にsuper
を記載忘れると上手く動作しなくなってしまう。」をフックメソッドを使って対応していきます。
フックメソッドを使うとサブクラスでsuper
を実装させずに、サブクラスの処理を引き継ぐことが出来ます!!
実際のコードを見たほうがわかりやすいと思います、フックメソッドを用いてリファクタリングしたのが下記コードです。
Employee
にpost_initialize
、subsequent_work
を実装し、initialize
、work
内で呼び出すことで、Staff
、Manager
内でpost_initialize
、subsequent_work
メソッドを実装すれば、super
を使わなくも自動的に元メソッド内で実行されるようになりました!!
class Employee attr_reader :name, :group, :grade def initialize(args) @name = args[:name] @group = args[:group] @grade = default_grade post_initialize(args) # フックメソッド end def post_initialize(args) nil end def default_grade # ERRORを投げるメソッドを定義 raise NotImprementedError, "#{self.class}では、default_gradeメソッドを実装してください。" end def work do_preparation subsequent_work # フックメソッド end # ... end class Staff < Employee def subsequent_work do_clean_up end def default_grade :staff end # ... end class Manager < Employee attr_reader :assistant def post_initialize(args) @assistant = args[:assistant] end def default_grade :admin end def subsequent_work do_check end # ... end
継承を使ったコードをテストする
最後に継承を使ったコードのテストについて整理していこうとおもいますφ(..)
検証のポイント
継承を使ったコードの検証のポイントは下記の通りです。
スーパークラス
- メソッドが定義されているか
- テンプレートメソッドが適切にエラーを吐くか
- サブクラスで
default_grade
未実装時にエラーとなるか
- サブクラスで
サブクラス
- スーパークラスのメソッドが定義されているか
- サブクラス独自の振る舞いが定義されているか
post_initialize
、subsequent_work
、default_grade
がサブクラスに実装されているか- サブクラスで定義した
default_grade
の設定値の確認
テストコード
それでは実際にテストコードを書いて検証していきます!(._.)
メソッドが定義されているか、スーパークラスのメソッドが定義されているか
スーパークラスにメソッドが定義されており、それが適切にサブクラスに継承されているかを検証するにはassert_respond_to
を使います。また、メソッド定義の確認は汎用性を高めるために、module
を使ってまとめておきます。
moduleにまとめておくことにより、新規のサブクラスが追加になっても容易にスーパークラスのメソッド追加出来ますφ(..)
require 'minitest/autorun' # スーパークラスのメソッド定義を検証するモジュール module EmployeeInterfaceTest def test_respond_to_name assert_respond_to(@object, :name) end def test_respond_to_group assert_respond_to(@object, :group) end def test_respond_to_grade assert_respond_to(@object, :grade) end def test_respond_to_work assert_respond_to(@object, :work) end def test_respond_to_default_grade assert_respond_to(@object, :default_grade) end def test_respond_to_do_preparation assert_respond_to(@object, :do_preparation) end end # スーパークラスの検証 class EmployeeTest # モジュールをincludeし、メソッド定義の検証を実施 include EmployeeInterfaceTest def setup @employee = @object = Employee.new(geade: :worker) end end # サブクラス(Staff)の検証 class Staff # モジュールをincludeし、メソッド定義の検証を実施 include EmployeeInterfaceTest def setup @staff = @object = Staff.new end end # サブクラス(Manager)の検証 class Manager # モジュールをincludeし、メソッド定義の検証を実施 include EmployeeInterfaceTest def setup @manager = @object = Manager.new end end
テンプレートメソッドが適切にエラーを吐くか
スーパークラスに定義されたテンプレートメソッドが適切にエラーを出力するかを検証するのは、単純です。assert_raise
を使用して、NotImprementedError
が発生するかどうかを検証します。
また、エラーが発生することだけではなくオーバーライドされていたらエラーとならず、サブクラスで定義した値が設定されることをスタブを使い確認しています。今回は、サブクラスとしてStaff
とManager
が定義されていますが、実際の業務等ではスーパークラスだけで検証する必要がある場合は、このようにスタブを作成することでスーパークラスの検証を行うことが出来ますφ(..)
# 検証用のサブクラス(スタブ) class StubbedEmployee < Employee def default_grade :stubbed end end # スーパークラスの検証 class EmployeeTest # モジュールをincludeし、メソッド定義の検証を実施 include EmployeeInterfaceTest def setup @employee = @object = Employee.new(geade: :worker) # スタブ用のインスタンスを作成 @stubbed_employee = StubbedEmployee.new end # スーパークラスで呼び出したときにエラーとなることを検証 def test_forces_subclasses_to_implement_default_grade assert_raises(NotImprementedError){ @employee.default_grade } end # スタブを用いて、オーバーライド時に値が設定されることを検証 def test_default_grade_overraid_success assert_equal @stubbed_employee.default_grade, :stubbed end end
サブクラス独自の振る舞いが定義されているか
これも最初に説明したケースと同様にサブクラス固有音メソッド定義を検証するmodule
を作成して検証していきます。また、固有の固有の振る舞いdefalt_grade
の設定値については、テストメソッドとして、個別のテストクラスに記載していきますφ(..)
こうすることによって、新しいサブクラスが発生したときにEmployeeSubclassTest
をincludeすれば、サブクラスに必要な役割の検証を行うことができ、あとは個別の振る舞いのみの検証に集中することが出来ます!
module EmployeeSubclassTest def test_respond_to_post_initialize assert_respond_to(@object, :post_initialize) end def test_respond_to_subsequent_work assert_respond_to(@object, :subsequent_work) end def test_respond_to_default_grade assert_respond_to(@object, :default_grade) end end # サブクラス(Staff)の検証 class Staff # モジュールをincludeし、メソッド定義の検証を実施 include EmployeeInterfaceTest # moduleをinclude include EmployeeSubclassTest def setup @staff = @object = Staff.new end # default_gradeの値の検証 def test_default_grade_value_staff assert_equal @manager.default_grade, :staff end end # サブクラス(Manager)の検証 class Manager include EmployeeInterfaceTest # モジュールをincludeし、サブクラスのメソッド定義の検証を実施 include EmployeeSubclassTest def setup @manager = @object = Manager.new end # default_gradeの値の検証 def test_default_grade_value_admin assert_equal @manager.default_grade, :admin end end
おわりに
今回は少し長くなってしまいましたが、継承によるコードの共有化と、継承を使ったテンプレートメソッド
、フックメソッド
を使ったリファクタリングについて整理してみましたφ(..)
継承を使うとコードを共有してコード量を削減出来るだけでなく、 コードの変更にも強くなるので積極的に使っていきたいですが、安易に継承を使い過ぎると具象的な内容を残してしまったり、処理が各所に散らばり可読性が下がってしまう恐れもあるので、注意して使っていきたいですね・・・!