Control Flow, Nil Safety และ Exceptions
โค้ด Ruby ใช้ร่วมจากโฟลเดอร์ en/ ของต้นฉบับ เพื่อให้สองภาษาผูกกับชุดทดสอบเดียวกัน
th/topic_05_control_nil_exceptions
ภาพรวม
ทำไมหัวข้อนี้จึงสำคัญ
แอปพลิเคชันจริงต้องมีพฤติกรรมที่ชัดเจนเวลาข้อมูลหาย หรือเวลาสถานะของระบบไม่ถูกต้อง Ruby ให้เครื่องมือที่กระชับ เช่น guard clauses และ safe navigation ก็จริง แต่บทเรียน ที่ลึกกว่านั้นคือการเลือก failure mode ที่เหมาะกับแต่ละสถานการณ์
สิ่งที่ผมอยากให้คุณทำได้เมื่อจบหัวข้อนี้
เมื่อจบหัวข้อนี้ คุณควรจะ:
- อธิบายได้ว่าเมื่อไร
nilเป็นผลลัพธ์ที่ยอมรับได้ และเมื่อไรควรใช้ exception - ใช้ guard clauses เพื่อลดความซับซ้อนของ control flow
- ใช้ safe navigation กับข้อมูลซ้อนที่เป็น optional ได้
- นิยามและ raise custom exception ที่สื่อความหมายได้ชัด
- อธิบาย trade-off ระหว่าง API ที่ยอมผ่อนปรน กับ API ที่ fail-fast ได้
จุดที่ผมใช้ดูความเข้าใจ
ผมอยากให้คุณอธิบายความต่างระหว่าง "หาไม่เจอ" กับ "ตั้งค่าผิด" ได้ชัด ไม่ใช่แค่ทำให้ test ผ่าน
โน้ตสั้น
nil ใน Ruby ใช้ง่ายก็จริง แต่ในเชิงการสอน คำถามที่สำคัญกว่า "จะไม่ให้โปรแกรมพังได้ อย่างไร" คือ "การไม่มีค่านี้ หมายความว่าอะไรใน domain นี้"
ข้อมูลบางอย่างอาจจะไม่มีได้:
- อีเมลใน profile ของผู้ใช้อาจไม่มีจริง
- การคืน
nilจึงอาจสมเหตุสมผล
ข้อมูลบางอย่างขาดไม่ได้:
- ค่า configuration สำหรับ integration สำคัญอาจต้องมีเสมอ
- การ raise exception จึงอาจเป็นทางเลือกที่ชัดกว่า
จุดที่ Ruby ทำได้ดีในหัวข้อนี้:
- guard clauses ช่วยให้กิ่งของเงื่อนไขตื้นลง
- safe navigation ช่วยจัดการข้อมูลซ้อนที่ไม่จำเป็นต้องมีได้สะอาด
- custom exceptions ช่วยสื่อความหมายของปัญหาในเชิง domain
จุดที่ต้องระวังในหัวข้อนี้:
- ถ้าคืน
nilบ่อยเกินไป จะเริ่มซ่อน bug - ถ้า raise ทุกอย่างหมด API จะเริ่มใช้งานเหนื่อยเกินไป
คำถามชวนคิด:
- ความต่างเชิงความหมายระหว่าง "ไม่มีอีเมลของผู้ใช้" กับ "ไม่มี config ของแอป" คืออะไร
ตัวอย่างแบบลงมือดู
Example 1: ข้อมูลใน profile ของผู้ใช้ที่อาจไม่มี
ข้อมูลใน profile ของผู้ใช้มักไม่ครบเสมอ การคืน nil จากการค้นหาแบบปลอดภัยจึงสมเหตุ สมผล เมื่อคนเรียกสามารถตัดสินใจต่อเองได้ว่าจะเอาไปแสดงอย่างไร
user&.dig(:profile, :email)ruby
วิธีนี้ดีกว่าการซ้อนเงื่อนไขลึก ๆ เพื่อเช็กข้อมูล optional ที่มีงานแค่นิดเดียว
Example 2: configuration ที่จำเป็นต้องมี
การดึง configuration เป็นตัวอย่างคู่เปรียบที่ชัดมาก เพราะ config ที่หายไปมักเป็นปัญหา ด้าน deployment หรือการตั้งค่า ไม่ใช่เรื่องปกติที่ยอมรับได้ในทุกวัน
เหตุผลที่ตัวอย่างนี้มีประโยชน์:
- ผู้เรียนได้ฝึกแยกประเภทของ error
- custom exception ทำให้ความล้มเหลวมีชื่อที่ผูกกับ domain
- ความต่างระหว่างข้อมูล optional กับข้อมูล required จับต้องได้ขึ้นมาอย่างชัดเจน
โพยสั้น
safe navigation
user&.dig(:profile, :email)ruby
guard clause
return 0 unless userruby
custom exception
class MissingConfigError < StandardError; endruby
fail-fast pattern
raise MissingConfigError, "missing config: #{key}"ruby
คำถามที่ควรถาม
- การไม่มีค่านี้เป็นเรื่องปกติหรือไม่
- คนที่เรียก method มีทาง fallback ที่สมเหตุสมผลหรือไม่
- ถ้าคืน
nilกลับไป จะทำให้ bug ร้ายแรงถูกซ่อนหรือไม่
คู่มือการเรียน
จุดประสงค์ของหัวข้อนี้
หัวข้อนี้อยากให้คุณแยกให้ออกว่า "ข้อมูลไม่มี" กับ "ระบบตั้งค่าไม่ถูก" เป็นปัญหาคนละ แบบกัน หัวข้อนี้จึงเกี่ยวกับ judgment พอ ๆ กับเรื่องไวยากรณ์
ลำดับที่ผมแนะนำ
- อ่าน
overview.md - อ่าน
shortnote.mdแล้วมองให้ออกว่าการไม่มีค่ามีอยู่สองแบบ - อ่าน
worked_examples.md - เปิด
cheatsheet.mdไว้ตอนอ่านexample.rb - ทำแบบฝึกหัดพื้นฐานเรื่องการตัดสินส่วนลด
- ทำแบบฝึกหัดขั้นสูงเรื่อง configuration ที่จำเป็นต้องมี
สิ่งที่ผมอยากให้คุณสังเกต
- guard clauses ช่วยลดการซ้อนของเงื่อนไข
- safe navigation เหมาะเมื่อการไม่มีข้อมูลเป็นเรื่องปกติ
- custom exception สื่อสารได้แรงกว่า เมื่อการไม่มีข้อมูลคือปัญหาระดับระบบ
คำถามที่อยากให้คุณพกไว้
- ทำไมการคืน
nilถึงเหมาะกับการหา profile lookup แต่ไม่เหมาะกับ required config - ถ้า
fetch!คืนnilเงียบ ๆ ไป bug แบบไหนจะถูกซ่อน - guard clauses ช่วยให้ method เล็ก ๆ อ่านง่ายขึ้นอย่างไร
Source Files and Tests
โค้ด Ruby ใช้ร่วมจากโฟลเดอร์ en/ ของต้นฉบับ เพื่อให้สองภาษาผูกกับชุดทดสอบเดียวกัน
# EXAMPLE CODE
# Topic: topic_05_control_nil_exceptions
#
# Purpose:
# - This file demonstrates reference implementation for the concept.
# - It should pass tests from the beginning.
# - Read and understand it before solving exercises.
class ProfileEmail
def extract(user)
user&.dig(:profile, :email)
end
end
Ruby course source
# STUDENT TASK (BASIC)
# Topic: topic_05_control_nil_exceptions
#
# What to do:
# - Implement or improve the class/methods in this file.
# - Read tests in tests/topic_05_control_nil_exceptions_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 DiscountPolicy
def discount_for(user)
return 0 unless user
return 20 if user[:vip]
5
end
end
Ruby course source
# STUDENT TASK (ADVANCED)
# Topic: topic_05_control_nil_exceptions
#
# Academic purpose:
# - Distinguish optional data access from mandatory configuration lookup.
# - Practice encoding domain meaning in an exception class rather than raising generic errors.
#
# Real-world use case:
# - Application config often includes API keys, region names, feature flags, or service endpoints.
# - If one of those settings is missing, the system should fail loudly and explain why.
# - This is a useful counterexample to the earlier nil-safe lookup behavior.
#
# Why Ruby is beautiful here:
# - A custom exception is easy to define and gives the failure a clear name.
# - The method can stay short while still communicating a strong failure policy.
# - Guard-style control flow keeps the successful path obvious.
#
# What to do:
# - Complete the challenge behavior requested by the guide.
# - Be able to explain why this case should not silently return `nil`.
# - Use tests in tests/topic_05_control_nil_exceptions_spec.rb under the "advanced exercise" examples.
#
# Expected outcome:
# - Advanced tests pass and you can justify the fail-fast design.
class MissingConfigError < StandardError; end
class ConfigFetcher
def fetch!(config, key)
return config[key] if config.key?(key)
raise MissingConfigError, "missing config: #{key}"
end
end
Ruby course source
# ANSWER KEY (BASIC)
# Topic: topic_05_control_nil_exceptions
#
# Solution idea:
# - Use guard clauses to separate cases clearly.
# - No user means no discount.
# - VIP users get the larger discount.
# - Everyone else gets the standard fallback.
class DiscountPolicy
def discount_for(user)
return 0 unless user
return 20 if user[:vip]
5
end
end
Ruby course source
# ANSWER KEY (ADVANCED)
# Topic: topic_05_control_nil_exceptions
#
# Solution idea:
# - Missing required configuration is treated as an error, not a normal nil case.
# - A custom exception gives the failure a domain-specific meaning.
class MissingConfigError < StandardError; end
class ConfigFetcher
def fetch!(config, key)
return config[key] if config.key?(key)
raise MissingConfigError, "missing config: #{key}"
end
end
Ruby course source
# This spec is your learning companion for topic_05_control_nil_exceptions.
#
# 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_05_control_nil_exceptions" do
describe "EXAMPLE purpose: understand the reference implementation" do
it "extracts profile emails safely" do
e = ProfileEmail.new
expect(e.extract({ profile: { email: "a@b.com" } })).to eq("a@b.com")
expect(e.extract(nil)).to eq(nil)
end
end
describe "BASIC EXERCISE purpose: implement the comparable task" do
it "applies discount rules" do
d = DiscountPolicy.new
expect(d.discount_for(nil)).to eq(0)
expect(d.discount_for(vip: true)).to eq(20)
expect(d.discount_for(vip: false)).to eq(5)
end
end
describe "ADVANCED EXERCISE purpose: solve challenge and edge cases" do
it "raises custom error for missing config" do
f = ConfigFetcher.new
expect(f.fetch!({ timeout: 30 }, :timeout)).to eq(30)
expect { f.fetch!({}, :timeout) }.to raise_error(MissingConfigError, /timeout/)
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 แล้วจดว่าคำตอบต่างจากวิธีคิดแรกของคุณตรงไหน