LoginSignup
11
4

More than 5 years have passed since last update.

ダックタイピング、あるいは動的型付けのすゝめ(「オブジェクト指向設計実践ガイド」より)

Last updated at Posted at 2018-12-21

はじめに

こんにちは!Tama.rbによくお邪魔をしておりますWebエンジニア一年生のしおいです。
Tama.rbでは、現在月二回のペースで書籍「オブジェクト指向設計実践ガイド」の読書会を行なっています。
読書会は、各章持ち回りで担当者がまとめた内容をたたき台に、当日メンバーでディスカッションを行うという進め方をしています。
今回わたしは第5章「ダックタイピングでコストを削減する」を担当することとなりました。
そこでまとめた内容を、この機会に公開したいと思います。
(内容についてのご指摘どんどんいただけますと嬉しいです!)

前提

書籍「オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方
Sandi Metz (著), 髙山泰基 (翻訳)
第5章を自分なりにまとめたものになります。

問題

ダックタイピングとは?

解答

複数のクラスに同じ名前のメソッドを作ること
(※しおいの見解です)

型のお話

  • 文字列
  • 数値
  • 配列   …etc

ex.

1.to_s
# => "1" 
# => 「数値型はto_sメソッドというパブリックインターフェースを持っている」という信頼の上に成り立ったコード

[1,2,3].to_s
# => "[1, 2, 3]"
# => 配列型もまたto_sメソッドというパブリックインターフェースを持っている

_人人人人人人人人人人_
> ダックタイピング <
 ̄Y^Y^Y^Y^Y^Y^Y^Y^Y ̄

5.1 ダックタイピングを理解する

駄目な例:ダックタイピングを使わない場合

class Trip
  attr_reader :bicycles, :customers, :vehicle

  def prepare(preparers)  
    preparers.each do |preparer|

      case preparer  # preparer.classでは?という思い…
      # 以下、各classの中で実装されている、各メソッドを実行する
      when Mechanic    
        preparer.prepare_bicycles(bicycles)
      when TripCoodinator
        preparer.buy_foods(customers)
      when Driver
        preparer.gas_up(vehicle)
        preparer.fill_water_tank(vehicle)
      end

    end
  end
end

class Mechanic
  def prepare_bicycles(bicycles)
    # (略)
  end
end

class TripCoodinator
  def buy_foods(customers)
    # (略)
  end
end

class Driver
  def gas_up(vehicle)
    # (略)
  end

  def fill_water_tank(vehicle)
    # (略)
  end
end

このコードの何が駄目なのか

  • prepareが、一つ一つのpreparerを具体的に知りすぎている (引数に何が渡されるのかを暗示的に想定してしまっている)
  • preparerクラスに実装されているメソッドが変更された場合、 それに引きずられてTripクラスのprepareメソッドも変更しなければいけない
  • preparerクラスの種類が増えるたびに分岐が増えていく

良き例:ダックタイピングを使う場合

class Trip
  attr_reader :bicycles, :customers, :vehicle

  def prepare(preparers)  
    preparers.each do |preparer|
      # 自分自身を引数に渡す => 必要になりそうな属性(bicycles, customers, vehicle)をまるっと渡している
      preparer.prepare_trip(self)
    end
  end
end

class Mechanic
  def prepare_trip(trip)
    # 渡されたtripインスタンスから必要な要素(bicycles)を取り出している
    trip.bicycles.each do |bicycle|
      prepare_bicycle(bicycle)
    end
  end

  def  prepare_bicycle(bicycle)
    # 略
  end
end

class TripCoodinator
  def prepare_trip(trip)
    trip.customers.each do |customer|
      buy_foods(customer)
    end
  end

  def  buy_foods(customer)
    # 略
  end
end

class Driver
  def prepare_trip(trip)
    vehicle = trip.vehicle
    gas_up(vehicle)
    fill_water_tank(vehicle)
  end

  def gas_up(vehicle)
    # (略)
  end

  def fill_water_tank(vehicle)
    # (略)
  end
end

このコードの何が嬉しいのか

  • preparerクラスのメソッドに変更があった場合
  • preparerクラスが増えた場合

などにTripクラスを変更する必要がない
(オープン・クローズドの原則!)

ポリモーフィズム is 何

同じメソッドをいろんなクラスで使えることだよ(╹◡╹)
ダックタイピングの他に
- 継承(第6章)
- モジュール(第7章)

なんかで実装されるよ(╹◡╹)

5.2 ダックを信頼するコードを書く

隠れたダックを見つけよう

ダックはこんなところに隠れているぞ!🦆

  • クラスで分岐するcase文
  • kind_of? / is_a?
  • respond__to?

クラスで分岐するcase文

※さっき見たよね

def prepare(preparers)  
    preparers.each do |preparer|
      case preparer
      when Mechanic    
        preparer.prepare_bicycles(bicycles)
      when TripCoodinator
        preparer.buy_foods(customers)
      when Driver
        preparer.gas_up(vehicle)
        preparer.fill_water_tank(vehicle)
      end
    end
  end

kind_of? / is_a?

さっきのcase文を

def prepare(preparers)  
    preparers.each do |preparer|
      if preparer.kind_of?(Mechanic)    
        preparer.prepare_bicycles(bicycles)
      elsif preparer.kind_of?(TripCoodinator)
        preparer.buy_foods(customers)
      elsif preparer.kind_of?(Driver)
        preparer.gas_up(vehicle)
        preparer.fill_water_tank(vehicle)
      end
    end
  end

kind_of? ( またはis_a?)に書き換えても駄目🙅‍♀️

responds_to?

さらにさっきのkind_of?

def prepare(preparers)  
    preparers.each do |preparer|
      if preparer.responds_to?(:prepare_bictycles)    
        preparer.prepare_bicycles(bicycles)
      elsif preparer.responds_to?(:buy_food)
        preparer.buy_foods(customers)
      elsif preparer.responds_to?(:gas_up)
        preparer.gas_up(vehicle)
        preparer.fill_water_tank(vehicle)
      end
    end
  end

responds_to?に書き換えても駄目🙅‍♀️
(※responds_to?はオブジェクトが引数のメソッドに対応しているか調べるためのメソッド)

なぜ駄目なのか

  • クラスで分岐するcase文
  • kind_of? / is_a?
  • respond__to?

はいずれも、オブジェクトが何なのか(あるいはどんなメソッドを持っているか)を暗示的に規定しています。
(密結合になってしまっている…)

大事なこと👍

  • 「オブジェクトが何なのか」を規定せず、アヒル(抽象的な概念)として扱うこと
  • 複数のクラスで同じ名前のメソッドを共有すること

is
_人人人人人人人人人人_
> ダックタイピング <
 ̄Y^Y^Y^Y^Y^Y^Y^Y^Y ̄

※オブジェクトを抽象的に扱うことでコードから明白性が消えてしまうため、テストはしっかり書きましょう。

例外もある

ex: Railsのfirstメソッド

def first(*args)
  if args.any?

    # ↓kind_of? 使ってるじゃん!
    if args.first.kind_of?(Integer) || (loaded? && !args.first.kind_of?(Hash))
      to_a.first(*args)
    else
      apply_finder_options(args.first).first
    end

  else
    find_first
  end
end

IntegerクラスやHashクラスはRubyのコアクラス
=> どんな風に動くのかは大体想定できるでしょ。。

つまり

ダックタイプを行うかどうかについては、コスト対効果を考えることも大事。

ちなみに

先ほどのコードではRubyのfirstメソッドをモンキーパッチで書き変えています。
ダックタイプを行うためにモンキーパッチを行うのには常に危険が伴いますので重々ご注意ください🚫
大いなる力には大いなる責任が(略😎

5.3 ダックタイピングへの恐れを克服する

ここから議論を呼ぶ内容へ…

静的型付け言語って何?

変数を定義するときにあらかじめデータ型を指定しておく必要がある言語
- Java
- Go
- C# などが有名

これに対して、変数定義の段階では型を指定せず、プログラムを実行しながら型を確認していくのがRubyなどの動的型付け言語
- JavaScript
- PHP
- Python などが有名

静的型付け言語
=> どんな型が来るのか事前に分かっているので、予期せぬ型エラーが起こりにくいことがメリット、との認識です…
(ご指摘ありましたらよろしくお願いします)

つまり

静的型付け言語的には

def prepare(preparers)

# preparersに何が入ってるのかがわからないと、
# 安心して使えないよ…😱

動的型付け言語的には

def prepare(preparers)

# preparers何でも来い来い😎

子(サンディさん)、曰く

「(動的型付けに)慣れましょう」

心配事は起こらない(多分)

動的型付けのメリット

  • コンパイルが無い = すぐに実行できる
  • ソースコードに型情報を書かなくて良い = 簡潔に書ける
  • 型が動的に変わるのでメタプログラミングが簡単

動的型付けのデメリット(と言われているもの)

  • コンパイルが無い = コンパイル時に型チェックしてくれない
  • ソースコードに型情報を書けない = わかりにくくない?
  • コンパイルが無い(二回め) = 実行速度が遅い

子、曰く

(※全体的に超訳)
「優秀なプログラマがメタプログラミングを手放すのは人類の損失。
 いかに型エラーを起こさないように実装するかがプログラマの腕の見せ所。
 コンパイルで得られるメリットより柔軟なプログラミングで得られるメリットの方が大きいんじゃない?😎」

まとめ

ダックタイピングで処理を抽象化して、依存を減らそう! 🦆 🦆 🦆

余談

🦆←どう見てもカモ

11
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
4