Duck Typing

最近コードと書く上で使っていきたいなと思ったので勉強。

Duck Typing (ダックタイピング)とは

  • 定義

結論から書くと、「あるオブジェクトがアヒルのように歩き、アヒルのように話すなら、Rubyインタプリタはそのオブジェクトをアヒル」とみなす。この考え方をダックタイピングといいます。

  • クラス≠型

ダックタイピングの背景には、「クラス≠型」という考え方があります。
Java 等、静的型付け言語では「オブジェクトの型=そのオブジェクトのクラス」という考えが一般的です。しかし、Ruby では(Javaであっても)、オブジェクトの型は、その「オブジェクトが何ができる」かによって定義されます。


説明の文を書いてもいまいちピンとこないと思いますので、コードを混ぜて考えていきます。

Duci Typing を用いたCSV パーサーのテスト

  • CSVParser のテスト

例えば、Rails 内で次のようなCSV パーサークラスがあるとします。

require 'csv'

class CSVParser
  def self.parse(str_or_readable)
    CSV::Reader.create(str_or_readable).collect
  end
end


これをテストするとなると、以下のようなコードになると思います。

require File.dirname(__FILE__) + '/../test_helper'

class CSVParserTest < Test::Unit::TestCase
  def test_parser
    expected = [["1","2","3"],
                ["4","5","6"]]
    file = fixture_file_upload('csv_test/duck_typing.csv', 'text/plain')
    assert_equal expected, CSVParser.parse(file)
  end
end


fixture_file_upload は、Rails のテストヘルパーで、ファイルのアップロードをシミュレートします。これをparse メソッドに渡せば、アップロードされたファイルをこのメソッドに渡すことのシミュレートになるというすんぽうです。
しかし、このテストはファイルが絡んでいるため、結構面倒です。このテストのために、CSV ファイルを生成する必要もあります。もし、エラーデータのテストをしたいと言う際にも、わざわざCSV ファイルを作るはめになります。

  • Duck Typing を用いる

ここで、Duck Typing の出番です。Duck Typing の考え方にそえば、テストにわざわざ本物のファイルを用意しなくてもいいということになります。その代わりに、「ファイルのように歩き、ファイルのように話す何か」があればいいのです。
ここで、テストを次のように書き換えました。

def test_parser
  file = StringIO.new(<<-DATA)
1,2,3
4,5,6
DATA
  
  expected = [["1","2","3"],
              ["4","5","6"]]
  assert_equal expected, CSVParser.parse(file)
end


テストは通過します。これでOK というのがDuck Typing です。StringIOクラスは、IOと「同じインターフェース」を持つ文字列クラスです。つまり、外から見ればFile と同じように振舞うのです。
これなら、テストデータをテスト内に書くことができ、わざわざファイルを作成する必要はありません。

Duck Typing を使ったコーディング

Duck Typing はテストだけでなく、実際のコードにも役立ちます。例えば、次のメソッドは、「<<」を持つオブジェクトなら、全てを引数として渡すことができます。

class User
  def initialize(name)
    @name = name
  end

  def add_name_to_list(list)
    list << @name
  end
end
  • テスト
require 'test/unit'
class UserTest < Test::Unit::TestCase
  def test_add_name_to_list
    user = User.new("bob")
    
    #listは配列
    list = []
    user.add_name_to_list(list)
    assert_eqaul ["#{user.name}"], list
    
    #listは文字列
    list = ""
    user.add_name_to_list(list)
    assert_eqaul user.name, list
 
    #listはファイル
    filepath = "hoge".txt
    File.open(filepath,'r') do |f|
      user.add_name_to_list(f)
    end
   
    File.open(filepath,'r') do |f|
      assert_eqaul user.name, f.gets
    end
  end
end


Duck Typing は、オブジェクト指向とマッチした概念だと思います。つまりこの場合、各クラス(Array, String, File)が「<<」というメソッドを「自身に何かを追加する」という操作として定義しておけば、User#add_name_to_list は、引数に関わらず、自身の名前をリストに追加するという定義どおりに動くということです。

RailsのソースにみるDuck Typing

  • activerecord-1.15.3\lib\active_record\connection_adapters\abstract\quoting.rb
module ActiveRecord
  module ConnectionAdapters # :nodoc:
    module Quoting
      # Quotes the column value to help prevent
      # {SQL injection attacks}[http://en.wikipedia.org/wiki/SQL_injection].
      def quote(value, column = nil)
        # records are quoted as their primary key
        return value.quoted_id if value.respond_to?(:quoted_id)

        case value
          when String, ActiveSupport::Multibyte::Chars


ここで、quote メソッドの2行目にrespond_to? がでてきます。これは、レシーバに引数に指定されたメソッドが定義されているかどうかを調べるメソッドです。
つまり、quoted_id という振る舞いをするならということです。Duck Typing じゃなく書けば、次のようになります。

return value.quoted_id if value.is_a?(ActiveRecord::Base)


しかし、これだと、ActiveRecord::Base クラスしか受け入れられない柔軟でないメソッドになってしまいます。respond_to? を使うほうがいいでしょう。次のように書けば、そのメリットが分かるかと思います。これなら、引数としてFile, StringIO などが渡せます。File に限定しないことで、先ほどのようなテストもかけますし、実際役に立つと思います。

def hoge(obj)
  if obj.respond_to?(:read)
    ・・・
  else
    raise 
  end
end


これだと、メソッドに制限がなくバグの原因にならないかという質問に対しては、大抵は問題にならないと述べられています。それ以上にメリットの方が多いと。


Let's Duck Typing!!

参考

プログラミングRuby 第2版 言語編

プログラミングRuby 第2版 言語編