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)

上記コードは次のような流れになっています。

  1. キッチンマネージャー、シェフ、フロアスタッフの各インスタンスを初期化する
  2. キッチンマネージャーが和食、イタリアン、フレンチそれぞれのシェフを雇用する
  3. シェフの雇用状況に合わせて提供可能なコースメニューがキッチンマネージャーから公開される
  4. 公開されたメニューをもとにフロアスタッフがお客さんから注文を取る
  5. お客さんからの注文をもとに、キッチンマネージャーが担当シェフにコース料理の作成を指示する
  6. 完成した料理をフロアスタッフがお客さんに提供する

料理ジャンルごとに、コースの順番の名称まで出力しているところがこだわりポイントです。

さて、そもそも改善の余地が多く残るコードではありますが、その中でも特に大きな問題がありそうなのが`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アプリケーション開発に従事。コードのリファクタリング、コーヒーとドーナツが好き。

おすすめ記事

タグ

2020新卒バトンAdobe IllustratorBIツールCCDLab.CSVDockerDXECGoogleAnalyticsGoogleデータポータルKubernetesLT会MembersDinerOJTRubySDGsSEOSimilarWebSlackSNSSocial Art JapanプロジェクトSQLUXUXリサーチVitePressVSCodeWebディレクションWebディレクターWebマーケティングWeb解析アクセシビリティアナリティクスウェビナーウェビナー運用エシカルエシカルファッションエンジニアオウンドメディアオンラインイベントお悩み相談室キャリアクライアントワークコーディングコミュニケーションコンテンツマーケティングコンペサービスサステイナブルスウェーデンスキルアップセミナーソーシャルアーティストソーシャルクリエイターチームビルディングツールディレクションディレクターデザイナーデザインデンマークトンマナナレッジハックブームの裏側プランニングプログラミングフロントエンドマーケターマーケティングマシンラーニングマネジメントスキルミーティングメンタルハックメンバーズルーツカンパニーユーザーテストライティングラボ活動リサーチリモートワークショップワークスタイル事例仕事術仙台分析勉強会北欧医療業界営業地方金融企業学生向け広告運用数学新卒研修機械学習気候変動海洋プラスチック問題産学連携研修社会課題社会課題調査競技プログラミング脱炭素自動化ツール色彩検定製薬業界資格開発環境障がい者雇用