7

การออกแบบที่ยึดพฤติกรรมด้วย Duck Typing และ Mixins

โค้ด Ruby ใช้ร่วมจากโฟลเดอร์ en/ ของต้นฉบับ เพื่อให้สองภาษาผูกกับชุดทดสอบเดียวกัน

th/topic_07_duck_typing_polymorphism

ภาพรวม

ทำไมหัวข้อนี้จึงสำคัญ

หัวข้อนี้คือหนึ่งในจุดที่ชัดที่สุดว่าความเป็น Ruby อยู่ตรงไหน แทนที่จะยึด class hierarchy หรือ interface แบบเป็นทางการ ผู้เรียนจะเริ่มออกแบบจากพฤติกรรมที่แชร์กัน และความคาด หวังเล็ก ๆ ระหว่าง objects

สิ่งที่ผมอยากให้คุณทำได้เมื่อจบหัวข้อนี้

เมื่อจบหัวข้อนี้ คุณควรจะ:

  • อธิบาย duck typing ในความหมายว่า "รับ object จากสิ่งที่มันทำได้"
  • มองออกว่าข้อตกลงโดยนัยเล็ก ๆ อย่าง export(rows) คืออะไร
  • ขยาย service ด้วย object ร่วมงานแบบใหม่ โดยไม่ต้องแก้ service เดิม
  • ใช้ module mixin เพื่อแชร์พฤติกรรมได้
  • อธิบายทั้งจุดแข็งและความเสี่ยงของ interface โดยนัยได้

จุดที่ผมใช้ดูความเข้าใจ

ผมอยากให้คุณอธิบายได้ว่าทำไม service จึงพึ่งพาพฤติกรรม ไม่ใช่ exporter class แบบ เฉพาะเจาะจง

โน้ตสั้น

Duck typing คือหนึ่งในไอเดียการออกแบบที่ทรงอิทธิพลที่สุดของ Ruby แทนที่จะถามว่า "object นี้เป็น class อะไร" Ruby มักถามว่า "object นี้ทำสิ่งที่ผมต้องการได้ไหม"

สำหรับคนที่มาจาก Java เรื่องนี้อาจให้ความรู้สึกทั้งโล่งและเสี่ยง เป้าหมายของหัวข้อนี้ ไม่ใช่การปฏิเสธ interfaces แบบไม่ลืมหูลืมตา แต่คือการเห็นว่าเมื่อไร contract ด้าน พฤติกรรมที่เล็กมาก ก็ชัดพอโดยไม่ต้องเพิ่มโครงสร้างเกินจำเป็น

จุดที่ Ruby ทำได้ดีในหัวข้อนี้:

  • service เปิดรับ object ร่วมงานแบบใหม่ได้ง่าย
  • contract มีขนาดเล็กและใช้งานจริง
  • mixins ทำให้พฤติกรรมร่วมกันเดินทางข้าม class ได้ โดยไม่บังคับให้เกิด hierarchy

จุดที่ต้องระวังในหัวข้อนี้:

  • ข้อตกลงโดยนัยจะเริ่มมองไม่เห็น ถ้าการตั้งชื่อกับชุดทดสอบอ่อนเกินไป
  • inheritance ไม่ควรถูกใช้เป็นเครื่องมือขยายระบบแบบอัตโนมัติ

คำถามชวนคิด:

  • อะไรทำให้ข้อตกลงโดยนัยอ่านออกและปลอดภัยพอจะใช้ร่วมกันในทีม

ตัวอย่างแบบลงมือดู

Example 1: Export service

การส่งออกรายงานเดียวกันเป็น CSV, JSON, XML-like markup หรือ payload สำหรับ API ภายนอก เป็นกรณีใช้งาน duck typing ที่สมจริงมาก

ReportService.new(exporter: CsvExporter.new).call(rows)
ReportService.new(exporter: JsonExporter.new).call(rows)
worked_examples.md
ruby

service นี้ไม่สนใจว่ามันได้รับ exporter class อะไร มันสนใจแค่ว่า object ตอบ export(rows) ได้หรือไม่

Example 2: พฤติกรรม logging ที่ใช้ร่วมกัน

mixins มีประโยชน์เมื่อ service หลายตัวต้องการพฤติกรรมเล็ก ๆ ร่วมกัน แต่ถ้าใช้ inheritance จะพูดความจริงของ domain ผิดไป

เหตุผลที่ตัวอย่างนี้มีประโยชน์:

  • service ยังโฟกัสงานหลักของตัวเองอยู่
  • การขยายระบบเกิดผ่าน object ร่วมงานตัวเล็กและ modules
  • โค้ดนี้แสดงให้เห็นความชอบของ Ruby ที่จะต่อพฤติกรรมเข้าหากันได้

โพยสั้น

object ร่วมงานแบบ duck-typed

class ReportService
  def initialize(exporter:)
    @exporter = exporter
  end

  def call(rows)
    @exporter.export(rows)
  end
end
cheatsheet.md
ruby

mixin

module Loggable
  def log(message)
    "LOG: #{message}"
  end
end
cheatsheet.md
ruby

สิ่งที่ควรจำเรื่องการออกแบบ

  • พึ่ง method ที่ต้องใช้ ไม่ใช่ชื่อ class ที่คาดหวัง
  • ชุดทดสอบเป็นส่วนหนึ่งของการอธิบายข้อตกลงโดยนัย

คู่มือการเรียน

จุดประสงค์ของหัวข้อนี้

หัวข้อนี้อยากให้คุณเห็นหนึ่งในท่าการออกแบบที่เป็น Ruby มากที่สุด: พึ่งพาพฤติกรรม แทน ที่จะผูกกับชนิดข้อมูลแบบตายตัวโดยตรง ถ้าคุณเข้าใจจุดนี้ได้ดี คุณจะเริ่มเห็นว่าทำไม Ruby ถึง ทั้งทรงพลังและดูสง่างามในงานที่ต้องขยายต่อ

ลำดับที่ผมแนะนำ

  1. อ่าน overview.md
  2. อ่าน shortnote.md ช้า ๆ เพราะนี่คือหัวข้อเปลี่ยนวิธีคิดจาก Java ไป Ruby ที่สำคัญ
  3. อ่าน worked_examples.md
  4. เปิด cheatsheet.md ไว้ตอนอ่าน example.rb
  5. ทำแบบฝึกหัดพื้นฐานเรื่อง exporter
  6. ทำแบบฝึกหัดขั้นสูงเรื่อง logging แล้วอธิบายว่าทำไมจึงเลือกออกแบบแบบนั้น

สิ่งที่ผมอยากให้คุณสังเกต

  • service นี้พึ่ง export ไม่ได้พึ่ง class hierarchy
  • เราสามารถเพิ่ม exporters ใหม่ได้โดยไม่ต้องแก้ service
  • mixins ช่วยแชร์พฤติกรรม โดยไม่ต้องแปลว่าความสัมพันธ์ใน domain เป็นแบบ "is-a"

คำถามที่อยากให้คุณพกไว้

  • ทำไม export(rows) ถึงเป็นข้อตกลงโดยนัยที่ดีในที่นี้
  • เอกสารหรือชุดทดสอบแบบไหนทำให้ duck typing ปลอดภัยพอสำหรับทีม
  • เมื่อไร interface ที่เป็นทางการ หรือขอบเขตที่เข้มกว่านี้ ยังเหมาะอยู่

Source Files and Tests

โค้ด Ruby ใช้ร่วมจากโฟลเดอร์ en/ ของต้นฉบับ เพื่อให้สองภาษาผูกกับชุดทดสอบเดียวกัน

# EXAMPLE CODE
# Topic: topic_07_duck_typing_polymorphism
#
# Purpose:
# - This file demonstrates reference implementation for the concept.
# - It should pass tests from the beginning.
# - Read and understand it before solving exercises.

require "json"

class CsvExporter
  def export(rows)
    rows.map { |r| r.join(",") }.join("\n")
  end
end

class JsonExporter
  def export(rows)
    rows.to_json
  end
end

class ReportService
  def initialize(exporter:)
    @exporter = exporter
  end

  def call(rows)
    @exporter.export(rows)
  end
end
en/topic_07_duck_typing_polymorphism/example.rb
Ruby course source
# STUDENT TASK (BASIC)
# Topic: topic_07_duck_typing_polymorphism
#
# What to do:
# - Implement or improve the class/methods in this file.
# - Read tests in tests/topic_07_duck_typing_polymorphism_spec.rb under the "basic exercise" examples.
# - Make tests pass without breaking the example/advanced sections.
#
# Expected outcome:
# - You can run this topic tests and see all examples green after implementation.

class XmlLikeExporter
  def export(rows)
    body = rows.map { |r| "<row><c1>#{r[0]}</c1><c2>#{r[1]}</c2></row>" }.join
    "<rows>#{body}</rows>"
  end
end
en/topic_07_duck_typing_polymorphism/basic_exercise.rb
Ruby course source
# STUDENT TASK (ADVANCED)
# Topic: topic_07_duck_typing_polymorphism
#
# Academic purpose:
# - Show that Ruby extension is often about adding behavior, not building hierarchies.
# - Use a mixin to discuss reuse that is smaller and more local than inheritance.
#
# Real-world use case:
# - Service objects often need cross-cutting behavior such as logging, tracing, or simple auditing.
# - A logging mixin is useful when several services need that ability without pretending they are all
#   the same kind of object.
# - This complements the exporter example by showing two forms of extension in one topic.
#
# Why Ruby is beautiful here:
# - Duck typing keeps the main service open to new exporters.
# - A module can add behavior with very little ceremony.
# - The result is flexible, but still readable when the shared behavior is small.
#
# What to do:
# - Complete the challenge behavior requested by the guide.
# - Be able to explain why a mixin was chosen instead of deeper inheritance.
# - Use tests in tests/topic_07_duck_typing_polymorphism_spec.rb under the "advanced exercise" examples.
#
# Expected outcome:
# - Advanced tests pass and you can discuss the value and risk of implicit contracts.

module Loggable
  def log(message)
    "LOG: #{message}"
  end
end

class LoggedReportService < ReportService
  include Loggable
end
en/topic_07_duck_typing_polymorphism/adv_exercise.rb
Ruby course source
# ANSWER KEY (BASIC)
# Topic: topic_07_duck_typing_polymorphism
#
# Solution idea:
# - Implement the same `export(rows)` behavior contract used by the example exporters.
# - The service only cares that the object responds to `export`.
# - This exporter turns each row into a small XML-like structure.

class XmlLikeExporter
  def export(rows)
    body = rows.map { |row| "<row><c1>#{row[0]}</c1><c2>#{row[1]}</c2></row>" }.join
    "<rows>#{body}</rows>"
  end
end
en/topic_07_duck_typing_polymorphism/answer_basic_exercise.rb
Ruby course source
# ANSWER KEY (ADVANCED)
# Topic: topic_07_duck_typing_polymorphism
#
# Solution idea:
# - A mixin is a good fit for small shared behavior such as logging.
# - `LoggedReportService` inherits the report behavior and mixes in logging.
# - This keeps the shared concern separate from exporter logic.

module Loggable
  def log(message)
    "LOG: #{message}"
  end
end

class LoggedReportService < ReportService
  include Loggable
end
en/topic_07_duck_typing_polymorphism/answer_adv_exercise.rb
Ruby course source
# This spec is your learning companion for topic_07_duck_typing_polymorphism.
#
# How to use this file:
# 1) Run tests and observe failures/successes.
# 2) Keep EXAMPLE specs green from the beginning.
# 3) Implement BASIC exercise until BASIC specs pass.
# 4) Implement ADVANCED exercise and pass edge cases.
#
# Expected final result:
# - All examples in this file pass.
# - You understand both the concept and the implementation tradeoffs.

require_relative "../example"
require_relative "../basic_exercise"
require_relative "../adv_exercise"

RSpec.describe "topic_07_duck_typing_polymorphism" do
  describe "EXAMPLE purpose: understand the reference implementation" do
    it "exports with duck-typed services" do
      rows = [["name", "A"], ["role", "admin"]]

      expect(ReportService.new(exporter: CsvExporter.new).call(rows)).to include("name,A")
      expect(ReportService.new(exporter: JsonExporter.new).call(rows)).to include(%(["name","A"]))
    end
  end

  describe "BASIC EXERCISE purpose: implement the comparable task" do
    it "supports xml-like exporter with same interface" do
      rows = [["name", "A"], ["role", "admin"]]

      out = ReportService.new(exporter: XmlLikeExporter.new).call(rows)
      expect(out).to include("<rows>")
      expect(out).to include("<row>")
    end
  end

  describe "ADVANCED EXERCISE purpose: solve challenge and edge cases" do
    it "mixes loggable behavior into service" do
      service = LoggedReportService.new(exporter: CsvExporter.new)
      expect(service.log("ok")).to eq("LOG: ok")
    end
  end
end
en/topic_07_duck_typing_polymorphism/tests/topic_07_duck_typing_polymorphism_spec.rb
Ruby course source
#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"

exec "${ROOT_DIR}/run_tests.sh" "$(basename "${SCRIPT_DIR}")"
en/topic_07_duck_typing_polymorphism/run_topic_tests.sh
Ruby course source

Study Prompts

  1. อ่าน test ก่อน แล้วบอกให้ได้ว่าพฤติกรรมใดเป็น example, basic exercise และ advanced exercise

  2. ลองทำแบบฝึกหัดก่อนเปิด answer files แล้วจดว่าคำตอบต่างจากวิธีคิดแรกของคุณตรงไหน