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_cputrack_memorytrack_latency
แบบนี้ทำให้ define_method เป็นตัวอย่างสอนที่สมเหตุสมผล เพราะความซ้ำซ้อนนั้นมีอยู่ จริง ไม่ได้ถูกสร้างขึ้นมาเพื่อโชว์เทคนิค
Example 2: DSL สำหรับสร้าง query
โค้ดสร้าง query เป็นตัวอย่างที่ดีของการใช้ Ruby แบบ dynamic เพื่อความอ่านง่าย ไม่ใช่ เพื่อความแปลกใหม่
QueryBuilder.new.where(:status, "active").where(:role, "admin")ruby
เหตุผลที่ตัวอย่างนี้มีประโยชน์:
- API อ่านเหมือนภาษาเล็ก ๆ ภายใน domain
- method chaining ตั้งใจคืน receiver กลับมา
- ผลลัพธ์แสดงให้เห็นว่า Ruby ทำให้การคุยกับ object แบบธรรมดา ดูงามขึ้นได้อย่างไร
โพยสั้น
dynamic dispatch
public_send(operation, a, b)ruby
สร้าง method แบบอัตโนมัติ
[:cpu, :memory].each do |name|
define_method("track_#{name}") do |value|
"tracking #{name}=#{value}"
end
endruby
API ที่ต่อ chain ได้
def where(key, value)
@query[key] = value
self
endruby
สิ่งที่ควรจำเรื่องการออกแบบ
- ใช้เทคนิคแบบ dynamic เพื่อเอาความซ้ำซ้อนออก หรือเพื่อทำให้ภาษาของ domain ชัดขึ้น
- ถ้า method ธรรมดาอ่านง่ายกว่า ก็อย่าใช้เทคนิคพวกนี้
คู่มือการเรียน
จุดประสงค์ของหัวข้อนี้
หัวข้อนี้อยากให้คุณเห็นว่าความเป็น dynamic ของ Ruby ทำให้โค้ด expressive ขึ้นได้ แต่ ต้องใช้ด้วยวินัย ผมอยากให้หัวข้อนี้สอนทั้งความชื่นชมและความระแวงไปพร้อมกัน
ลำดับที่ผมแนะนำ
- อ่าน
overview.md - อ่าน
shortnote.mdก่อนแตะแบบฝึกหัด - อ่าน
worked_examples.mdแล้วถามตัวเองว่าเทคนิคแต่ละชิ้นคุ้มกับต้นทุนของมันไหม - เปิด
cheatsheet.mdไว้ตอนอ่านexample.rb - ทำแบบฝึกหัดเรื่อง metrics
- ทำแบบฝึกหัดขั้นสูงเรื่อง 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
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
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
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
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
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
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}")"
Ruby course source
Study Prompts
อ่าน test ก่อน แล้วบอกให้ได้ว่าพฤติกรรมใดเป็น example, basic exercise และ advanced exercise
ลองทำแบบฝึกหัดก่อนเปิด answer files แล้วจดว่าคำตอบต่างจากวิธีคิดแรกของคุณตรงไหน