オブジェクト志向設計実践ガイドを読んで継承を使ったソースコードの共有化手法を学んだので内容を整理してみましたφ(..)
はじめに
今回は、下記のような社員を表すクラスのコードを元に実際に継承によるコード共有を行ってみようと思いますφ(..)
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 = 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 = 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
@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
@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
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
@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
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 EmployeeInterfaceTest
def setup
@employee = @object = Employee.new(geade: :worker)
end
end
class Staff
include EmployeeInterfaceTest
def setup
@staff = @object = Staff.new
end
end
class Manager
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 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
class Staff
include EmployeeInterfaceTest
include EmployeeSubclassTest
def setup
@staff = @object = Staff.new
end
def test_default_grade_value_staff
assert_equal @manager.default_grade, :staff
end
end
class Manager
include EmployeeInterfaceTest
include EmployeeSubclassTest
def setup
@manager = @object = Manager.new
end
def test_default_grade_value_admin
assert_equal @manager.default_grade, :admin
end
end
おわりに
今回は少し長くなってしまいましたが、継承によるコードの共有化と、継承を使ったテンプレートメソッド
、フックメソッド
を使ったリファクタリングについて整理してみましたφ(..)
継承を使うとコードを共有してコード量を削減出来るだけでなく、
コードの変更にも強くなるので積極的に使っていきたいですが、安易に継承を使い過ぎると具象的な内容を残してしまったり、処理が各所に散らばり可読性が下がってしまう恐れもあるので、注意して使っていきたいですね・・・!