2021.10.7
Rubyで学ぶダックタイピング
はじめに
こんにちは、メンバーズエッジカンパニー所属の大和です。
突然ですが、皆さんは「ダックタイピング」をご存知でしょうか。オブジェクト指向プログラミングの文脈でよく見かけるこの言葉。しかし、実際にどういったものなのかよく知らないという方も多いと思います。
今回はそんなダックタイピングについて、Rubyのコードを用いながら紹介していきたいと思います。
ダックタイピングとは
ダックタイピングについて学んでいると必ず目にするフレーズがあります。それがこちらです。
「もしもそれがアヒルのように歩き、アヒルのように鳴くのなら、それはアヒルに違いない」
これはもともとダックテストというアナロジーの一つらしいのですが、これをオブジェクト指向プログラミングの世界の話に置き換えてみると、
「それがどんなクラスのオブジェクトであれ、アヒルのように振る舞うのならそのオブジェクトはアヒルである」
このように表現できるかなと思います。
つまり「オブジェクトは、それ自体が何かということではなく、どのように振る舞うかによって定義される」という「振る舞い重視」の考え方がダックタイピングの根幹にあるということですね。
ところで、Rubyでは数値や文字列などのあらゆる値がオブジェクトであり、私たちはそのオブジェクトに対してメソッドの呼び出しを行うことで特定の「ふるまい」を実行させます。
また、あるオブジェクトが反応できるメソッドは、そのオブジェクトが所属するクラスによって一意に決定されます。
つまりダックタイピング的な考え方をした場合、あるオブジェクトがアヒルとしてみなされるためには、そのオブジェクトのクラスにアヒルとして振る舞うためのメソッド(パブリックインターフェース)が備わっていればいい、ということになります。
文字だけでは伝わりにくいと思いますので、ここまでの内容を一度Rubyのコードで表現してみます。
class ShibaInu
def bark
puts 'bow wow!'
end
def paw # お手
puts ('(pon)')
end
# ...(※以下略)
end
class SiberianHusky
def bark
puts 'howl!'
end
def paw
puts '(pohu)'
end
# ...(※以下略)
end
taro = ShibaInu.new
jiro = SiberianHusky.new
taro.bark
taro.paw
jiro.bark
jiro.paw
上のコードでは、`ShibaInu`クラス、`SiberianHusky`クラスという異なる2つの犬種が表現されています。
二つの犬種クラスは名称も実装内容も根本的に違うものですが、名称のみ共通するメソッドとして`bark`メソッド、`paw`メソッドが定義されているのが見てとれるかと思います。
さて、ここで呼び出し側が求めているオブジェクトを、
「番犬のように吠え、番犬のようにお手をする番犬オブジェクト」
だと仮定してみましょう。
上記二種のクラスはそのどちらの振る舞いも備えているので、`taro`や`jiro`は各々の所属クラスがなんであれ「番犬オブジェクト」と見なすことができるというわけです。
このように、複数のオブジェクトが、同じメッセージに応答する能力を持ち、それぞれ異なる振る舞いをすることを、オブジェクト指向プログラミングでは多態性(ポリモーフィズム)なんて呼んだりします。
そしてこの多態性を活かして「特定のクラスと結びつかない、クラスを跨ぐパブリックインターフェースを取り決め、実装する」ことが最も単純なダックタイピングなのです。
ダックタイピングを使用するメリット
ダックタイピングの基本を述べたところで、次にこれがどんな時に役に立つのかを紹介しようと思います。
以下のコードは、あるレストランにおける料理のオーダーから提供までを簡単にシミュレートしたものです。
Rubyの実行環境がある方は、適当なファイルに保存した上で実行してみてください。
(動作確認時の環境はRuby3.0ですが、最近のバージョンであれば問題なく動くはずです)
class FloorStaff
def recieve_order(cource_menu)
if cource_menu.empty?
puts 'ただいま準備中です'
return
end
puts 'いらっしゃいませ。メニューからコースをお選びください'
order = nil
loop do
cource_menu.each { |course| puts "・#{course}" }
order = gets.chomp.capitalize
break if cource_menu.include?(order)
puts '申し訳ありませんが、そちらはメニューにございません。再度お選びください'
end
puts 'かしこまりました。少々お待ちください'
order
end
def serve_dishes(dishes)
puts 'お待たせいたしました。こちらから'
dishes.each do |course_order, dish|
puts "・#{course_order}: #{dish}"
end
puts 'になります'
end
end
class KitchenManager
def initialize
@chefs = {}
end
def prepare_dishes(course_name)
chef = @chefs[course_name]
course_dishes = {}
case chef
when ItalianChef
course_dishes['アンティパスト'] = chef.make_ahijo
course_dishes['メイン'] = chef.make_pasta
course_dishes['ドルチェ'] = chef.make_gelato
when FrenchChef
course_dishes['オードブル'] = chef.make_terrine
course_dishes['ヴィヤンド'] = chef.make_rossini_steak
course_dishes['デセール'] = chef.make_millefeuille
when JapaneseChef
course_dishes['先付'] = chef.make_goma_dofu
course_dishes['焼物'] = chef.make_saikyoyaki
course_dishes['菓子'] = chef.make_mizu_yokan
end
course_dishes
end
def employee_chef(chef_part, cook)
@chefs[chef_part] = cook
end
def open_menu
@chefs.keys
end
end
class ItalianChef
def make_pasta
'トマトとベーコンのパスタ'
end
def make_ahijo
'マッシュルームのアヒージョ'
end
def make_gelato
'イチジクのジェラート'
end
end
class FrenchChef
def make_terrine
'夏野菜のテリーヌ'
end
def make_rossini_steak
'牛ヒレ肉のロッシーニ風'
end
def make_millefeuille
'桃のミルフィーユ'
end
end
class JapaneseChef
def make_goma_dofu
'胡麻豆腐'
end
def make_saikyoyaki
'鰆の西京焼き'
end
def make_mizu_yokan
'水羊羹'
end
end
donald= KitchenManager.new
taro = JapaneseChef.new
alonzo = ItalianChef.new
alice = FrenchChef.new
takeo = FloorStaff.new
donald.employee_chef('Japanese', taro)
donald.employee_chef('Italian', alonzo)
donald.employee_chef('French', alice)
available_menu = donald.open_menu
customer_order = takeo.recieve_order(available_menu)
cource_dishes = donald.prepare_dishes(customer_order)
takeo.serve_dishes(cource_dishes)
上記コードは次のような流れになっています。
- キッチンマネージャー、シェフ、フロアスタッフの各インスタンスを初期化する
- キッチンマネージャーが和食、イタリアン、フレンチそれぞれのシェフを雇用する
- シェフの雇用状況に合わせて提供可能なコースメニューがキッチンマネージャーから公開される
- 公開されたメニューをもとにフロアスタッフがお客さんから注文を取る
- お客さんからの注文をもとに、キッチンマネージャーが担当シェフにコース料理の作成を指示する
- 完成した料理をフロアスタッフがお客さんに提供する
料理ジャンルごとに、コースの順番の名称まで出力しているところがこだわりポイントです。
さて、そもそも改善の余地が多く残るコードではありますが、その中でも特に大きな問題がありそうなのが`KitchenManager`クラスのこちらの箇所です。
def prepare_dishes(course_name)
chef = @chefs[course_name]
course_dishes = {}
case chef
when ItalianChef
course_dishes['アンティパスト'] = chef.make_ahijo
course_dishes['メイン'] = chef.make_pasta
course_dishes['ドルチェ'] = chef.make_gelato
when FrenchChef
course_dishes['オードブル'] = chef.make_terrine
course_dishes['ヴィヤンド'] = chef.make_rossini_steak
course_dishes['デセール'] = chef.make_millefeuille
when JapaneseChef
course_dishes['先付'] = chef.make_goma_dofu
course_dishes['焼物'] = chef.make_saikyoyaki
course_dishes['菓子'] = chef.make_mizu_yokan
end
course_dishes
end
お客さんのオーダーを受け、担当シェフにコース料理のセットアップを指示しているしているところですね。
case文では担当シェフがどのシェフクラスに属するか検証し、そのクラスが持つ料理作成メソッドを実行。最終的に、コースの順番名をキーとしたハッシュを返しています。
ここで一番の問題となるのが`KitchenManager`クラスが各シェフクラスの実装を「知っている」ことです。
「各シェフクラスの実装に深く依存したコードになっている」と言い換えることもできるでしょう。このようなコードは、変更にとても弱いという欠点があります。
例えば季節によってコース料理の構成変更があったり、新しくスペイン料理を担当するシェフが雇用されたりしたとしたらどうでしょうか。その度に、このメソッドに追加修正を加えなければならなくなることが容易に想像できると思います。
実のところ`KitchenManager`クラスは各シェフクラスが何ができるか、どのように調理を行うかについて知っておく必要はないのです。
実際のレストランの厨房を想像してみましょう。
お客さんの注文を受けたフロアスタッフは、厨房に「和食コース1つお願いします」などとオーダーを通すでしょう。
またそれを受けたキッチンマネージャーも、担当シェフに「和食コース一つね」と丸投げするでしょう。内容の決まったコースについて、いちいち「最初は胡麻豆腐を作って、次に……」などと指示することはないと思います。
つまりここで`KitchenManager`クラスは、各シェフクラスが「担当する料理を作る」振る舞いを備えていることのみを期待すればいいのです。
ここで満を辞して登場するのが、そう、ダックタイピングです!「特定のクラスと結びつかない、クラスを跨ぐパブリックインターフェースを取り決め、実装する」ことで、今回の問題を改善してみましょう。
class KitchenManager
def initialize
@chefs = {}
end
def prepare_dishes(course_name)
chef = @chefs[course_name]
chef.cook
end
# ※以下略
end
class ItalianChef
def cook
{
'アンティパスト': make_ahijo,
'メイン': make_pasta,
'ドルチェ': make_gelato
}
end
# ※以下略
end
class FrenchChef
def cook
{
'オードブル': make_terrine,
'ヴィヤンド': make_rossini_steak,
'デセール': make_millefeuille
}
end
# ※以下略
end
class JapaneseChef
def cook
{
'先付': make_goma_dofu,
'焼物': make_saikyoyaki,
'菓子': make_mizu_yokan
}
end
# ※以下略
end
各シェフクラスを跨いだパブリックインターフェースとして`cook`メソッドを実装し、その中でクラスごとに異なるコースメニューを組み立てることにしました。
`KitchenManager`クラスの`prepare_dishes`メソッドから煩わしかったcase文が消え、コードも大分シンプルになったことが分かると思います。
これなら、コースメニューの変更に伴う修正は各シェフクラスの実装についてのみ生じるため、`KitchenManager`クラスはそれを気にする必要がありません。また新たにスペイン料理のシェフが加わったとしても、同じように`cook`メソッドを実装すれば、呼び出し側を変更することなく調理を依頼できます。
ダックタイピングを用いることで、変更や変更に柔軟に対応できるコードへとリファクタリングすることができました。
このように、ダックタイピングを用いることで変更に強く柔軟なコードを記述することができるようになるのです。
Strategyパターン
今回取り上げたコードの改修には、実は「Strategyパターン」というオブジェクト指向プログラミングにおけるデザインパターンを用いています。
これは、課題に対する戦略(アルゴリズム)をクラスとして定義、分離し、委譲とインターフェースを介してそれを利用するデザインパターンです。
戦略クラスはその中にアルゴリズムを閉じ込めるため、仮に戦略に変更があったとしても、インターフェースさえ変更しなければ呼び出し元に変更を加える必要がありません。
また共通のインターフェースを規定することで、目的に合わせて戦略を簡単に切り替えることができます。
今回取り上げたコードの内では、各シェフクラスが戦略クラス、`cook`メソッドが共通のインターフェースとしての役割を担っていました。ダックタイピングは、このインターフェースの規定に一役買っていたというわけなのです。
ダックタイピングの注意点
良いことずくめに思えるダックタイピングにも、いくつか注意点があります。
まず、ダックタイピングで規定したインターフェースは酷く暗黙的だということです。Javaのように明示的に型を宣言するわけでもなく、ただ共通のパブリックメソッドを定義するだけであるため、コード理解の点では難易度が上昇するでしょう。テストコードを用いて規定の文書化はできると思いますが、一定の抽象度は残ってしまうと思います…。
また、型宣言がない、オブジェクトを信頼したコーディングだという点に違和感を覚える方もいるでしょう。特に静的型付け言語を多用されてきた方々は、気持ち悪ささえ感じるかもしれません。
ただ、ダックタイピングのような「ユーザーを信頼する」プログラミングは、それに伴う責任と引き換えに私たちにに大きな力を与えてくれるものでもあります。それを忘れなければ、こうしたプログラミングも決して忌避されるものではないでしょう。
まとめ:変更に強いプログラミングをしよう
Strategyパターンをはじめ、オブジェクト指向における各種デザインパターンの原則として「変わるものを変わらないものから分離する」というものがあります。変更に強く柔軟なプログラムを作成する上で、非常に重要な考え方です。今回取り上げたコードでは、コースの内容や雇用されるシェフ達(変わるもの)を、調理を依頼するキッチンマネージャー(変わらないもの)から分離することで実現しました。
コース料理が季節とともにその内容を変えるように、現代において変更の生じないプログラムなどほとんどありません。だからこそ、いつどこで変更が起きてもいいような、柔軟なプログラムを記述することが大切なのです。
ダックタイピングはそうしたコードを書くための強力な武器の一つです。明るく素敵なコーディングライフを目指す中で、ぜひ積極的にコードに取り入れてみてください。
この記事を書いた人
大和 拓朗(おおわ たくろう)
メンバーズエッジカンパニー所属。Webエンジニア。
現在はSNS関連のWebアプリケーション開発に従事。コードのリファクタリング、コーヒーとドーナツが好き。