8

Dynamic Ruby Features และ Metaprogramming แบบมีวินัย

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

th/topic_08_ruby_specific_features

ภาพรวม

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

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

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

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

  • อธิบายได้ว่า public_send ทำอะไร และทำไมมันปลอดภัยกว่า send
  • ใช้ define_method กับ API ที่มีรูปแบบซ้ำอย่างชัดเจนได้
  • มองออกว่าเมื่อไรโค้ดแบบ dynamic ช่วยให้ชัดขึ้น และเมื่อไรทำให้ดูแลยากขึ้น
  • อธิบายได้ว่าทำไม open classes ถึงทั้งทรงพลังและเสี่ยง
  • ประเมิน metaprogramming จากความอ่านง่าย ไม่ใช่จากความแปลกใหม่

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

ผมอยากให้คุณอธิบายได้ว่าพฤติกรรมแบบ dynamic นี้ตอบโจทย์จริง ไม่ใช่ถูกใช้เพียงเพื่อ โชว์สไตล์

โน้ตสั้น

หัวข้อนี้คือจุดที่หลายคนเริ่มรู้สึกถึง "มนตร์" ของ Ruby แต่บทเรียนสำคัญไม่ใช่แค่ว่า Ruby ทำท่า dynamic ได้ บทเรียนสำคัญคือฟีเจอร์เหล่านี้มีค่าที่สุดเมื่อมันช่วยตัดโค้ดซ้ำ หรือช่วย ทำให้ DSL เล็ก ๆ อ่านออกชัด

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

  • public_send ทำให้ dynamic dispatch ถูกเขียนออกมาอย่างชัดเจน
  • define_method ช่วยลดโค้ดประกอบที่ซ้ำกัน
  • internal DSL ขนาดเล็กอ่านใกล้เคียงกับภาษาของ domain ได้

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

  • open classes อาจสร้างความประหลาดใจให้ส่วนอื่นของระบบ
  • methods ที่ถูกสร้างขึ้นมาอาจเริ่มมองไม่เห็นสำหรับคนอ่านและเครื่องมือ
  • metaprogramming ที่ประหยัดโค้ดห้าบรรทัด แต่ทำให้เสียเวลาทำความเข้าใจสิบ นาที มัก

เป็นการแลกที่ไม่คุ้ม

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

  • อะไรทำให้กรณีใช้ metaprogramming ดูสง่างาม ไม่ใช่แค่ฉลาดเกินจำเป็น

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

Example 1: Metrics APIs

ระบบ metrics มักเปิดชุด method ที่มีทรงเหมือนกันเป็นครอบครัว เช่น:

  • track_cpu
  • track_memory
  • track_latency

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

Example 2: DSL สำหรับสร้าง query

โค้ดสร้าง query เป็นตัวอย่างที่ดีของการใช้ Ruby แบบ dynamic เพื่อความอ่านง่าย ไม่ใช่ เพื่อความแปลกใหม่

QueryBuilder.new.where(:status, "active").where(:role, "admin")
worked_examples.md
ruby

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

  • API อ่านเหมือนภาษาเล็ก ๆ ภายใน domain
  • method chaining ตั้งใจคืน receiver กลับมา
  • ผลลัพธ์แสดงให้เห็นว่า Ruby ทำให้การคุยกับ object แบบธรรมดา ดูงามขึ้นได้อย่างไร

โพยสั้น

dynamic dispatch

public_send(operation, a, b)
cheatsheet.md
ruby

สร้าง method แบบอัตโนมัติ

[:cpu, :memory].each do |name|
  define_method("track_#{name}") do |value|
    "tracking #{name}=#{value}"
  end
end
cheatsheet.md
ruby

API ที่ต่อ chain ได้

def where(key, value)
  @query[key] = value
  self
end
cheatsheet.md
ruby

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

  • ใช้เทคนิคแบบ dynamic เพื่อเอาความซ้ำซ้อนออก หรือเพื่อทำให้ภาษาของ domain ชัดขึ้น
  • ถ้า method ธรรมดาอ่านง่ายกว่า ก็อย่าใช้เทคนิคพวกนี้

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

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

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

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

  1. อ่าน overview.md
  2. อ่าน shortnote.md ก่อนแตะแบบฝึกหัด
  3. อ่าน worked_examples.md แล้วถามตัวเองว่าเทคนิคแต่ละชิ้นคุ้มกับต้นทุนของมันไหม
  4. เปิด cheatsheet.md ไว้ตอนอ่าน example.rb
  5. ทำแบบฝึกหัดเรื่อง metrics
  6. ทำแบบฝึกหัดขั้นสูงเรื่อง query-builder

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

  • public_send เป็นเครื่องมือ runtime dispatch ที่ใช้ด้วยความตั้งใจ
  • define_method จะเด่นที่สุดเมื่อรูปแบบของ methods ซ้ำกันชัดเจน
  • API ที่ต่อ chain กันได้จะสวยมาก เมื่อมันยังเล็กและไม่โกหกคนอ่าน

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

  • ทำไมตัวอย่าง query builder ถึงสอนได้ดีกว่า metaprogramming demo แบบสุ่ม ๆ
  • อะไรทำให้ define_method ดูสมเหตุสมผลในแบบฝึกหัด metrics
  • เมื่อไร method ที่เขียนตรง ๆ ธรรมดา ๆ จะเป็นคำตอบที่ดีกว่า

Source Files and Tests

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

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

class CalculatorOps
  def add(a, b) = a + b
  def sub(a, b) = a - b

  def apply(operation, a, b)
    public_send(operation, a, b)
  end
end
en/topic_08_ruby_specific_features/example.rb
Ruby course source
# STUDENT TASK (BASIC)
# Topic: topic_08_ruby_specific_features
#
# What to do:
# - Implement or improve the class/methods in this file.
# - Read tests in tests/topic_08_ruby_specific_features_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 Metrics
  [:cpu, :memory].each do |name|
    define_method("track_#{name}") do |value|
      "tracking #{name}=#{value}"
    end
  end
end
en/topic_08_ruby_specific_features/basic_exercise.rb
Ruby course source
# STUDENT TASK (ADVANCED)
# Topic: topic_08_ruby_specific_features
#
# Academic purpose:
# - Demonstrate a dynamic Ruby pattern that feels close to domain language rather than mere syntax trickery.
# - Evaluate metaprogramming by readability and usefulness.
#
# Real-world use case:
# - Query builders appear in ORMs, search interfaces, filtering layers, and internal DSLs.
# - Chaining methods such as `where(:status, "active")` can make intent very readable.
# - This is a better teaching example than abstract runtime dispatch because students can imagine using it.
#
# Why Ruby is beautiful here:
# - Returning `self` makes the API feel fluid.
# - The method names become part of a tiny language for expressing intent.
# - The resulting object can stay simple while the call site becomes expressive.
#
# What to do:
# - Complete the challenge behavior requested by the guide.
# - Be able to explain why this is a good DSL-style example and where the pattern should stop.
# - Use tests in tests/topic_08_ruby_specific_features_spec.rb under the "advanced exercise" examples.
#
# Expected outcome:
# - Advanced tests pass and you can discuss both the beauty and the risk of dynamic APIs.

class QueryBuilder
  def initialize
    @query = {}
  end

  def where(key, value)
    @query[key] = value
    self
  end

  def to_h
    @query.dup
  end
end
en/topic_08_ruby_specific_features/adv_exercise.rb
Ruby course source
# ANSWER KEY (BASIC)
# Topic: topic_08_ruby_specific_features
#
# Solution idea:
# - The methods all share the same shape, so `define_method` removes repetition.
# - This is a reasonable use of metaprogramming because the generated API is small and obvious.

class Metrics
  [:cpu, :memory].each do |name|
    define_method("track_#{name}") do |value|
      "tracking #{name}=#{value}"
    end
  end
end
en/topic_08_ruby_specific_features/answer_basic_exercise.rb
Ruby course source
# ANSWER KEY (ADVANCED)
# Topic: topic_08_ruby_specific_features
#
# Solution idea:
# - Build a chainable API by storing filter pairs in an internal hash.
# - Return `self` from `where` so multiple calls can read like a tiny query DSL.
# - Return a duplicated hash so callers do not mutate internal state accidentally.

class QueryBuilder
  def initialize
    @query = {}
  end

  def where(key, value)
    @query[key] = value
    self
  end

  def to_h
    @query.dup
  end
end
en/topic_08_ruby_specific_features/answer_adv_exercise.rb
Ruby course source
# This spec is your learning companion for topic_08_ruby_specific_features.
#
# 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_08_ruby_specific_features" do
  describe "EXAMPLE purpose: understand the reference implementation" do
    it "applies dynamic operation via public_send" do
      c = CalculatorOps.new
      expect(c.apply(:add, 2, 3)).to eq(5)
      expect(c.apply(:sub, 7, 4)).to eq(3)
    end
  end

  describe "BASIC EXERCISE purpose: implement the comparable task" do
    it "tracks dynamically defined metrics" do
      m = Metrics.new
      expect(m.track_cpu(70)).to eq("tracking cpu=70")
      expect(m.track_memory(256)).to eq("tracking memory=256")
    end
  end

  describe "ADVANCED EXERCISE purpose: solve challenge and edge cases" do
    it "builds chainable query hashes" do
      q = QueryBuilder.new.where(:status, "active").where(:role, "admin")
      expect(q.to_h).to eq({ status: "active", role: "admin" })
    end
  end
end
en/topic_08_ruby_specific_features/tests/topic_08_ruby_specific_features_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_08_ruby_specific_features/run_topic_tests.sh
Ruby course source

Study Prompts

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

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