2

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: ")")
worked_examples.md
ruby

แบบนี้ดีกว่าการส่งเครื่องหมายวรรคตอนแบบ positional arguments เพราะแค่มองที่จุด เรียกก็เข้าใจได้เลยว่าค่าไหนทำหน้าที่อะไร

Example 2: notification hook ที่ใช้ block

blocks เหมาะมากเมื่อ framework เป็นคนควบคุมการวนซ้ำ แต่คนเรียกเป็นคนกำหนดผล ข้างเคียงที่อยากให้เกิดขึ้น

notifier.notify_all(users) do |message|
  audit_log << message
end
worked_examples.md
ruby

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

  • notifier เป็นคนควบคุมการเดินข้อมูล
  • คนเรียกเป็นคนกำหนดว่าจะทำอะไรกับแต่ละข้อความ
  • โค้ดไม่ต้องสร้าง interface เพิ่ม เพื่อรองรับพฤติกรรมที่มีแค่ช่องเล็ก ๆ ช่องเดียว

โพยสั้น

Keyword arguments

def greet(name, punctuation: "!")
  "Hello, #{name}#{punctuation}"
end
cheatsheet.md
ruby

ค่า default

greet("Mina")
greet("Mina", punctuation: ".")
cheatsheet.md
ruby

ส่งงานต่อให้ block

def notify_all(users)
  users.each { |user| yield "Notify #{user}" }
end
cheatsheet.md
ruby

คำถามที่ควรถาม

  • คนที่เรียก method ควรเห็นความหมายของ argument ได้ตั้งแต่จุดเรียกหรือไม่
  • พฤติกรรมที่ส่งเข้ามาเล็กและอยู่เฉพาะจุดพอจะใช้ block หรือไม่

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

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

หัวข้อนี้อยากให้คุณเห็นว่า Ruby ทำให้ method ยืดหยุ่นได้ โดยไม่จำเป็นต้องยาวหรือ หนักเกินไป สิ่งที่ผมอยากสอนไม่ใช่การท่องไวยากรณ์ แต่คือความอ่านง่ายของ API และการส่ง พฤติกรรมเล็ก ๆ เข้าไปผ่าน block

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

  1. อ่าน overview.md
  2. อ่าน shortnote.md แล้วเทียบสไตล์ของ Ruby กับ Java overloads หรือ callback types
  3. อ่าน worked_examples.md ให้จบก่อนแตะแบบฝึกหัด
  4. เปิด cheatsheet.md ไว้ตอนอ่าน example.rb
  5. ทำแบบฝึกหัดพื้นฐานที่เน้น keyword arguments
  6. ทำแบบฝึกหัดขั้นสูงที่เน้น 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
en/topic_02_methods_keywords_blocks/example.rb
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
en/topic_02_methods_keywords_blocks/basic_exercise.rb
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
en/topic_02_methods_keywords_blocks/adv_exercise.rb
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
en/topic_02_methods_keywords_blocks/answer_basic_exercise.rb
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
en/topic_02_methods_keywords_blocks/answer_adv_exercise.rb
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
en/topic_02_methods_keywords_blocks/tests/topic_02_methods_keywords_blocks_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_02_methods_keywords_blocks/run_topic_tests.sh
Ruby course source

Study Prompts

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

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