5

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

วิธีนี้ดีกว่าการซ้อนเงื่อนไขลึก ๆ เพื่อเช็กข้อมูล optional ที่มีงานแค่นิดเดียว

Example 2: configuration ที่จำเป็นต้องมี

การดึง configuration เป็นตัวอย่างคู่เปรียบที่ชัดมาก เพราะ config ที่หายไปมักเป็นปัญหา ด้าน deployment หรือการตั้งค่า ไม่ใช่เรื่องปกติที่ยอมรับได้ในทุกวัน

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

  • ผู้เรียนได้ฝึกแยกประเภทของ error
  • custom exception ทำให้ความล้มเหลวมีชื่อที่ผูกกับ domain
  • ความต่างระหว่างข้อมูล optional กับข้อมูล required จับต้องได้ขึ้นมาอย่างชัดเจน

โพยสั้น

safe navigation

user&.dig(:profile, :email)
cheatsheet.md
ruby

guard clause

return 0 unless user
cheatsheet.md
ruby

custom exception

class MissingConfigError < StandardError; end
cheatsheet.md
ruby

fail-fast pattern

raise MissingConfigError, "missing config: #{key}"
cheatsheet.md
ruby

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

  • การไม่มีค่านี้เป็นเรื่องปกติหรือไม่
  • คนที่เรียก method มีทาง fallback ที่สมเหตุสมผลหรือไม่
  • ถ้าคืน nil กลับไป จะทำให้ bug ร้ายแรงถูกซ่อนหรือไม่

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

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

หัวข้อนี้อยากให้คุณแยกให้ออกว่า "ข้อมูลไม่มี" กับ "ระบบตั้งค่าไม่ถูก" เป็นปัญหาคนละ แบบกัน หัวข้อนี้จึงเกี่ยวกับ judgment พอ ๆ กับเรื่องไวยากรณ์

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

  1. อ่าน overview.md
  2. อ่าน shortnote.md แล้วมองให้ออกว่าการไม่มีค่ามีอยู่สองแบบ
  3. อ่าน worked_examples.md
  4. เปิด cheatsheet.md ไว้ตอนอ่าน example.rb
  5. ทำแบบฝึกหัดพื้นฐานเรื่องการตัดสินส่วนลด
  6. ทำแบบฝึกหัดขั้นสูงเรื่อง 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
en/topic_05_control_nil_exceptions/example.rb
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
en/topic_05_control_nil_exceptions/basic_exercise.rb
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
en/topic_05_control_nil_exceptions/adv_exercise.rb
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
en/topic_05_control_nil_exceptions/answer_basic_exercise.rb
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
en/topic_05_control_nil_exceptions/answer_adv_exercise.rb
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
en/topic_05_control_nil_exceptions/tests/topic_05_control_nil_exceptions_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_05_control_nil_exceptions/run_topic_tests.sh
Ruby course source

Study Prompts

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

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