Madogiwa Blog

主に技術系の学習メモに使っていきます。

Ruby:ダックタイピングを使って抽象度の高い処理を実装するメモ

オブジェクト志向設計実践ガイドを読んでダックタイピングを使って抽象度を上げる方法を学んだので、整理してみるφ(..)

はじめに

突然ですが以下のようなコードを見て、どのように思いますか?

class EmploymentHandler
  def work(employees)
    employees.each do |employee|
      case employee
      when Staff   then employee.do_clean_up
      when Manager then employee.do_check
      end
    end
  end
end

class Staff
  def do_clean_up
    # ...
  end
end

class Manager
  def do_check
    # ...
  end
end

渡された引数によってcase文使い、引数に渡されたクラスによって実行する処理を分けています。

一見良さそうに見えますが、このコードには以下のような問題が含まれています・・・。

  • EmploymentHandler.workメソッドに渡されるクラスが増える度にwhen...thenを追加しなければならない。
  • EmploymentHandler.workメソッドを修正する場合に、呼び出すStaff,Managerのメソッドについても理解しなければならない。

今回のようなケースでは、ダックタイピングを用いることで、EmploymentHandler.workの抽象度を上げ、他Classの修正から守り、よりわかりやすいコードとすることが出来ます!

ダックタイピングとは

ダックタイピングは、下記のような考え方です(._.)

「もしオブジェクトがダック(アヒル)のように鳴き、アヒルのように歩くならば、そのクラスが何であれ、それはダックである」
オブジェクト指向実践ガイドより抜粋

つまり、渡された引数がなんであれ、同様のメソッドを持っていれば同じものとして扱い、中身の処理は関知しないということでしょうか。

ダックタイピングを使ってみる

では実際にダックタイピングを使ってコードをリファクタリングしてみようと思います!

今回のケースであれば、Staff.do_clean_upManager.do_checkというメソッドをEmploymentHandler.workの中で呼び出していますが、社員または管理者がどのような働きをするかを定義しています。

なので、まずは既存のメソッドをラップするようなStaff.workManager.workというメソッドをつくってみましょう。

class EmploymentHandler
  def work(employees)
    employees.each do |employee|
      case employee
      when Staff   then employee.work
      when Manager then employee.work
      end
    end
  end
end

class Staff
  def work
    do_clean_up
  end
  def do_clean_up
    # ...
  end
end

class Manager
  def work
    do_check
  end
  def do_check
    # ...
  end
end

どうでしょうか、case文内でClassによって処理は分岐していますが実行されるメソッドはemployee.workとなっています!

引数と実行されるメソッドが同じになったので、ダックタイピングが使えそうですね!!

さっそくダックタイピングを使ってリファクタリングしたコードが下記ですφ(..)

class EmploymentHandler
  def work(employees)
    employees.each do |employee|
      employee.work
    end
  end
end

class Staff
  def work
    do_clean_up
  end
  def do_clean_up
    # ...
  end
end

class Manager
  def work
    do_check
  end
  def do_check
    # ...
  end
end

リファクタリング前後でコードを比較してみると、リファクタリング後の方が抽象度が上がり変更に強くなっていますね!!

修正前 修正後
EmploymentHandler.workメソッドに渡されるクラスが増える度にwhen...thenを追加しなければならない。 workメソッドを持つクラスを渡すというルールを守れば、どのクラスでもEmploymentHandler.workを使うことが出来る。
EmploymentHandler.workメソッドを修正する場合に、呼び出すStaff,Managerのメソッドについても理解しなければならない。 EmploymentHandler.workは、workメソッドが実装されていることを信頼し、呼び出し元のクラスのことは知る必要がない。

ダックタイピングをテストする

最後にダックタイピングのテストについて考えてみようと思います。 ダックタイピングのテストに必要なことは、それぞれのクラスについて特定の同名メソッドが実装されているかどうかを検証する必要があります。
今回は、Staff及びManagerworkメソッドが実装されているかを検証します。※テストはminitestを使用しています。

require 'minitest/autorun'

# workが実装されているか検証するモジュール
module WorkInterfaceTest
  def test_implements_the_work_inmterface
    assert_respond_to(@object, :work)
  end
end

# 社員用のテスト
class StaffTest < MiniTest::Unit::TestCase
  include WorkInterfaceTest
  def setup
    @staff = @object = Staff.new
  end
end

# 管理者用のテスト
class ManagerTest < MiniTest::Unit::TestCase
  include WorkInterfaceTest
  def setup
    @manager = @object = Manager.new
  end
end

WorkInterfaceTestモジュールをStaffTest及びManagerTestにincludeすることにより、それぞれのクラスにworkが実装されていることを検証しています。

またworkの実装有無をWorkInterfaceModuleTestで検証することにより、新しいクラスが追加されても容易にテストを追加出来るようになっていますφ(..)

まとめ

私もまだまだ理解が不足している点もあるのですが、簡単にダックタイピングについてまとめて見ました!

今回のケースではcase...whenを使っている箇所をダックタイピングでリファクタリングしてみましたが、responds_to?kind_of?等を使っている場合も使えるそうですφ(..)

ダックタイピングを上手く活用出来ると綺麗で、変更に強いコードを書けそうなので、使えそうな部分では積極的に活用して身に付けていきたいと思いました。