API ที่ยืดหยุ่นด้วย Keyword Arguments และ Blocks
โค้ด Ruby ใช้ร่วมจากโฟลเดอร์ en/ ของต้นฉบับ เพื่อให้สองภาษาผูกกับชุดทดสอบเดียวกัน
th/topic_02_methods_keywords_blocks
ภาพรวม
ทำไมหัวข้อนี้จึงสำคัญ
method ของ Ruby จะสั้นก็ได้ แต่ยังรองรับรูปแบบการเรียกที่ยืดหยุ่นได้ด้วย Keyword arguments ช่วยให้จุดเรียก method สื่อความหมาย ส่วน blocks ช่วยให้คนเรียกส่ง พฤติกรรมเล็ก ๆ เข้ามาได้โดยไม่ต้องสร้าง callback interface ที่เต็มไปด้วยพิธีรีตอง
สิ่งที่ผมอยากให้คุณทำได้เมื่อจบหัวข้อนี้
เมื่อจบหัวข้อนี้ คุณควรจะ:
- อธิบายความต่างระหว่าง positional arguments กับ keyword arguments ได้
- ใช้ค่า default เพื่อให้ API ใช้ง่าย โดยไม่ต้องแตก overload ออกไปเรื่อย ๆ
- ส่งพฤติกรรมเข้ามาผ่าน blocks เพื่อปรับแต่งงานเล็ก ๆ ได้
- อธิบายได้ว่าเมื่อไร block ชัดกว่าการสร้าง class ใหม่
- เทียบสไตล์ของ Ruby ที่ใช้ blocks กับ callback patterns ที่พบได้บ่อยใน Java ได้
จุดที่ผมใช้ดูความเข้าใจ
ผมอยากให้คุณสร้าง API เล็ก ๆ ที่ยังอ่านง่ายทั้งสำหรับคนเขียนและคนเรียกใช้ ไม่ใช่แค่ ทำให้ชุดทดสอบผ่านเท่านั้น
โน้ตสั้น
API ที่ดีใน Ruby มักอ่านแล้วเกือบจะเหมือนประโยคหนึ่งประโยค Keyword arguments ช่วย ให้คนเรียกเข้าใจความหมายได้โดยไม่ต้องเปิดเข้าไปดูใน method body ส่วน blocks ช่วยให้ คนเรียกส่งพฤติกรรมเล็ก ๆ เข้ามาได้ โดยไม่ต้องสร้างโครงหนัก ๆ มารองรับ
สำหรับคนที่มาจาก Java นี่เป็นความต่างที่สำคัญ ใน Java ความยืดหยุ่นมักพาไปสู่ type ที่มากขึ้น overload ที่มากขึ้น หรือโครงประกอบที่มากขึ้น แต่ใน Ruby ภาษาจะชวนให้เรา ทำจุดเรียก method ให้เล็ก กระชับ และสื่อความหมาย
จุดที่ Ruby ทำได้ดีในหัวข้อนี้:
- keyword arguments ทำให้เจตนามองเห็นได้
- ค่า default ช่วยลดความซ้ำซ้อนตอนเรียกใช้
- blocks ช่วยปรับแต่งพฤติกรรมได้แบบเบา ๆ
จุดที่ต้องระวังในหัวข้อนี้:
- ถ้า keyword arguments เยอะเกินไป method จะเริ่มคลุมเครือ
- ถ้า block ทำงานหนักเกินไป โค้ดจะเริ่มคิดตามยาก
คำถามชวนคิด:
- เมื่อไร block ทำให้โค้ดสื่อความหมายขึ้น และเมื่อไรควรย้ายพฤติกรรมไปอยู่ใน object
ตัวอย่างแบบลงมือดู
Example 1: API สำหรับจัดรูปแบบที่อ่านจุดเรียกได้ทันที
formatter เป็นตัวอย่าง API เล็ก ๆ ที่ดี เพราะคนเรียกมักสนใจความอ่านง่ายมากกว่าราย ละเอียดข้างใน
formatter.wrap("warning", left: "(", right: ")")ruby
แบบนี้ดีกว่าการส่งเครื่องหมายวรรคตอนแบบ positional arguments เพราะแค่มองที่จุด เรียกก็เข้าใจได้เลยว่าค่าไหนทำหน้าที่อะไร
Example 2: notification hook ที่ใช้ block
blocks เหมาะมากเมื่อ framework เป็นคนควบคุมการวนซ้ำ แต่คนเรียกเป็นคนกำหนดผล ข้างเคียงที่อยากให้เกิดขึ้น
notifier.notify_all(users) do |message|
audit_log << message
endruby
เหตุผลที่ตัวอย่างนี้มีประโยชน์:
- notifier เป็นคนควบคุมการเดินข้อมูล
- คนเรียกเป็นคนกำหนดว่าจะทำอะไรกับแต่ละข้อความ
- โค้ดไม่ต้องสร้าง interface เพิ่ม เพื่อรองรับพฤติกรรมที่มีแค่ช่องเล็ก ๆ ช่องเดียว
โพยสั้น
Keyword arguments
def greet(name, punctuation: "!")
"Hello, #{name}#{punctuation}"
endruby
ค่า default
greet("Mina")
greet("Mina", punctuation: ".")ruby
ส่งงานต่อให้ block
def notify_all(users)
users.each { |user| yield "Notify #{user}" }
endruby
คำถามที่ควรถาม
- คนที่เรียก method ควรเห็นความหมายของ argument ได้ตั้งแต่จุดเรียกหรือไม่
- พฤติกรรมที่ส่งเข้ามาเล็กและอยู่เฉพาะจุดพอจะใช้ block หรือไม่
คู่มือการเรียน
จุดประสงค์ของหัวข้อนี้
หัวข้อนี้อยากให้คุณเห็นว่า Ruby ทำให้ method ยืดหยุ่นได้ โดยไม่จำเป็นต้องยาวหรือ หนักเกินไป สิ่งที่ผมอยากสอนไม่ใช่การท่องไวยากรณ์ แต่คือความอ่านง่ายของ API และการส่ง พฤติกรรมเล็ก ๆ เข้าไปผ่าน block
ลำดับที่ผมแนะนำ
- อ่าน
overview.md - อ่าน
shortnote.mdแล้วเทียบสไตล์ของ Ruby กับ Java overloads หรือ callback types - อ่าน
worked_examples.mdให้จบก่อนแตะแบบฝึกหัด - เปิด
cheatsheet.mdไว้ตอนอ่านexample.rb - ทำแบบฝึกหัดพื้นฐานที่เน้น keyword arguments
- ทำแบบฝึกหัดขั้นสูงที่เน้น blocks
สิ่งที่ผมอยากให้คุณสังเกต
- keywords ช่วยอธิบายจุดเรียก method ไปในตัว
- ค่า default ช่วยให้โค้ดยังเรียกใช้ง่าย
- blocks จะงามที่สุดเมื่อพฤติกรรมที่ส่งเข้ามาเล็กและอยู่ใกล้จุดใช้งาน
คำถามที่อยากให้คุณพกไว้
- ทำไม
left:กับright:ถึงเป็น API ที่ดีกว่าการส่งเครื่องหมายวรรคตอนหลายตำแหน่ง - ในแบบฝึกหัดนี้ block สื่ออะไรได้ดีกว่า callback class
- เมื่อไรคุณควรเลิกใช้ block แล้วเปลี่ยนเป็น object ที่เข้ามาร่วมงานแทน
Source Files and Tests
โค้ด Ruby ใช้ร่วมจากโฟลเดอร์ en/ ของต้นฉบับ เพื่อให้สองภาษาผูกกับชุดทดสอบเดียวกัน
# EXAMPLE CODE
# Topic: topic_02_methods_keywords_blocks
#
# Purpose:
# - This file demonstrates reference implementation for the concept.
# - It should pass tests from the beginning.
# - Read and understand it before solving exercises.
class Greeter
def greet(name, punctuation: "!")
"Hello, #{name}#{punctuation}"
end
def greet_many(names)
names.map { |name| greet(name) }
end
end
Ruby course source
# STUDENT TASK (BASIC)
# Topic: topic_02_methods_keywords_blocks
#
# What to do:
# - Implement or improve the class/methods in this file.
# - Read tests in tests/topic_02_methods_keywords_blocks_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 Formatter
def wrap(text, left: "[", right: "]")
"#{left}#{text}#{right}"
end
end
Ruby course source
# STUDENT TASK (ADVANCED)
# Topic: topic_02_methods_keywords_blocks
#
# Academic purpose:
# - Study blocks as a lightweight form of behavior injection.
# - Contrast Ruby's block style with heavier callback abstractions common in Java.
#
# Real-world use case:
# - Notification systems often own the list of users but let the caller decide what to do
# with each message: log it, enqueue it, send it, or collect it for later.
# - A block is useful when the framework controls iteration and the caller controls the side effect.
#
# Why Ruby is beautiful here:
# - The method can stay tiny while remaining flexible.
# - Callers can customize behavior without defining a new class.
# - The code reads close to the problem statement: "for each user, do this".
#
# What to do:
# - Complete the challenge behavior requested by the guide.
# - Focus on what the block buys you in API design, not just how `yield` works.
# - Use tests in tests/topic_02_methods_keywords_blocks_spec.rb under the "advanced exercise" examples.
#
# Expected outcome:
# - Advanced tests pass and you can explain when a block is the right abstraction.
class Notifier
def notify_all(users)
return [] unless block_given?
users.map { |u| yield "Notify #{u}" }
end
end
Ruby course source
# ANSWER KEY (BASIC)
# Topic: topic_02_methods_keywords_blocks
#
# Solution idea:
# - Use keyword arguments so the call site explains the meaning of each wrapper.
# - Keep defaults in the method signature instead of branching inside the method body.
class Formatter
def wrap(text, left: "[", right: "]")
"#{left}#{text}#{right}"
end
end
Ruby course source
# ANSWER KEY (ADVANCED)
# Topic: topic_02_methods_keywords_blocks
#
# Solution idea:
# - The notifier owns the iteration over users.
# - The caller owns what to do with each generated message.
# - Returning an empty array when no block is given keeps the method safe for the exercise.
class Notifier
def notify_all(users)
return [] unless block_given?
users.map { |user| yield "Notify #{user}" }
end
end
Ruby course source
# This spec is your learning companion for topic_02_methods_keywords_blocks.
#
# 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_02_methods_keywords_blocks" do
describe "EXAMPLE purpose: understand the reference implementation" do
it "greets with default and custom punctuation" do
g = Greeter.new
expect(g.greet("Mina")).to eq("Hello, Mina!")
expect(g.greet("Mina", punctuation: ".")).to eq("Hello, Mina.")
end
end
describe "BASIC EXERCISE purpose: implement the comparable task" do
it "wraps text with keyword defaults" do
f = Formatter.new
expect(f.wrap("ruby")).to eq("[ruby]")
expect(f.wrap("ruby", left: "<", right: ">")) .to eq("<ruby>")
end
end
describe "ADVANCED EXERCISE purpose: solve challenge and edge cases" do
it "yields notifications and handles empty list" do
n = Notifier.new
out = n.notify_all(%w[a b]) { |msg| msg.upcase }
expect(out).to eq(["NOTIFY A", "NOTIFY B"])
expect(n.notify_all([]) { |msg| msg }).to eq([])
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 แล้วจดว่าคำตอบต่างจากวิธีคิดแรกของคุณตรงไหน