はじめに
こんにちは!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何でも来い来い😎
子(サンディさん)、曰く
「(動的型付けに)慣れましょう」
心配事は起こらない(多分)
動的型付けのメリット
- コンパイルが無い = すぐに実行できる
- ソースコードに型情報を書かなくて良い = 簡潔に書ける
- 型が動的に変わるのでメタプログラミングが簡単
動的型付けのデメリット(と言われているもの)
- コンパイルが無い = コンパイル時に型チェックしてくれない
- ソースコードに型情報を書けない = わかりにくくない?
- コンパイルが無い(二回め) = 実行速度が遅い
子、曰く
(※全体的に超訳)
「優秀なプログラマがメタプログラミングを手放すのは人類の損失。
いかに型エラーを起こさないように実装するかがプログラマの腕の見せ所。
コンパイルで得られるメリットより柔軟なプログラミングで得られるメリットの方が大きいんじゃない?😎」
まとめ
ダックタイピングで処理を抽象化して、依存を減らそう! 🦆 🦆 🦆
余談
🦆←どう見てもカモ