1

พื้นฐาน Ruby และวงจร TDD รอบแรก

โค้ด Ruby ใช้ร่วมจากโฟลเดอร์ en/ ของต้นฉบับ เพื่อให้สองภาษาผูกกับชุดทดสอบเดียวกัน

th/topic_01_ruby_basics

ภาพรวม

ผมเริ่มคอร์สด้วยการขยับเล็ก ๆ อย่างหนึ่ง ซึ่งดูเล็กในรูป แต่ส่งผลไกลมากในทางปฏิบัติ: โค้ด Ruby มักสร้างจาก expressions มากกว่าพิธีรีตองรอบนอก และ method หนึ่งก็มักพูด สิ่งที่มันต้องการทำได้ตรงขึ้น ขณะเดียวกัน test ก็สามารถบอกพฤติกรรมของโค้ดได้ก่อนที่ ตัวโค้ดจริงจะมีอยู่เสียอีก

ฟังดูเหมือนเรื่องเล็ก แต่ความรู้สึกของโค้ดจะเปลี่ยนไปทันที ถ้าคุณมาจาก Java คุณคุ้นกับ โครงประกอบที่มองเห็นได้ชัดกว่า แม้ในพฤติกรรมง่าย ๆ ผมอยากให้คุณเห็นว่าใน Ruby โครงประกอบบางส่วนหายไปได้ โดยที่ความคิดหลักไม่ได้อ่อนลงเลย

สิ่งที่ผมอยากให้คุณได้จากหัวข้อนี้

เมื่อจบหัวข้อนี้ ผมอยากให้คุณ:

  • อธิบายได้ว่า Ruby ที่เป็น expression-oriented หมายถึงอะไร
  • เขียน class เล็ก ๆ ที่มีพฤติกรรมชัด โดยไม่ต้องมีพิธีรีตองเกินจำเป็น
  • ใช้ RSpec เพื่ออธิบายพฤติกรรมก่อน implementation
  • แยกพฤติกรรมปกติออกจากพฤติกรรมด้าน validation ได้
  • เห็นว่าทำไมชื่อที่ชัดจึงสำคัญ แม้ในตัวอย่างที่เล็กมาก

สิ่งที่คุณควรมีก่อนเริ่ม

คุณต้องมีพื้นฐานแค่ไม่กี่อย่าง:

  • คุ้นกับ Java classes และ methods อยู่บ้าง
  • อ่าน unit tests ได้
  • ไม่จำเป็นต้องเคยเขียน Ruby มาก่อน

ถ้าคุณไม่ได้มาจาก Java ก็ไม่เป็นไร การเทียบกับ Java ยังช่วยได้อยู่ และแบบฝึกหัดก็เล็ก พอที่จะเรียนรู้จากตัวมันเองได้

สิ่งที่ผมอยากให้คุณสังเกต

คณิตศาสตร์ในหัวข้อนี้ไม่ใช่ประเด็นหลัก ผมใช้โจทย์เลขคณิตง่าย ๆ ก็เพราะไม่อยากให้ domain มาบดบังเรื่องการออกแบบ

สิ่งที่ผมอยากให้คุณเห็นจริง ๆ คือ:

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

ถ้าเวอร์ชันของ Ruby สั้นกว่าเวอร์ชันของ Java นั่นก็ดี แต่ถ้ามันทั้งสั้นและชัดขึ้นพร้อมกัน นั่นแหละคือสิ่งที่ผมอยากได้จริง

โน้ตสั้น

สิ่งแรกที่ผมอยากให้คุณจับความรู้สึกของ Ruby ให้ได้ คือ method มัก return ค่าของ expression สุดท้าย[^return] ฟังดูเหมือนกฎเล็ก ๆ แต่จริง ๆ แล้วมันเปลี่ยนบุคลิกของ โค้ดไปมาก

ใน Java แม้ method เล็ก ๆ ก็มักมีพิธีรีตองล้อมอยู่: การประกาศชนิดข้อมูล, การเขียน return แบบตรง ๆ และโครงประกอบอีกเล็กน้อยรอบไอเดียหลัก แต่ Ruby เปิด ทางให้ method เล็ก ๆ พูดไอเดียนั้นออกมาได้ตรงกว่าเดิม

def double(n)
  n * 2
end
shortnote.md
ruby

ผมไม่ได้ยก method นี้มาเพราะมันฉลาดแพรวพราว ผมยกมันมาเพราะมันตรงดี Ruby มักอ่าน ดีที่สุดตอนที่พิธีรีตองค่อย ๆ หลุดออกไป แล้วส่วนที่แบกความหมายยังมองเห็นอยู่ชัด ๆ

เพราะอย่างนี้หัวข้อแรกจึงไม่ได้เกี่ยวกับโจทย์เลขคณิตเป็นหลัก ผมใช้โจทย์เลขคณิตเพื่อสอน วินัยด้านการออกแบบในขนาดที่เล็กมาก:

  • เลือกชื่อ object ให้ชัด
  • ตั้งชื่อ method ให้บอกพฤติกรรม
  • เขียน expectation ก่อน
  • เพิ่ม validation เมื่อ domain ต้องการจริง ๆ

จุดแข็งของ Ruby ในหัวข้อแบบนี้คือ class หนึ่งสามารถสื่อไอเดียเดียวได้ด้วยโค้ดประกอบ ที่น้อยมาก แต่ข้อควรระวังก็สำคัญพอกัน: โค้ดที่สั้น ไม่ได้แปลว่าดีโดยอัตโนมัติ ถ้าชื่อที่ใช้ อ่อนเกินไป ความกระชับของ Ruby จะกลายเป็นการซ่อนความสับสนแทนที่จะช่วยลบมันออก

ระหว่างที่คุณทำแบบฝึกหัด ผมอยากให้คุณพกคำถามนี้ไว้ตลอด: อะไรในเวอร์ชันของ Ruby ทำให้ความหมายชัดขึ้นจริง และอะไรแค่ทำให้โค้ดสั้นลง

[^return]: เวลาผมพูดคำว่า return ในที่นี้ ผมไม่ได้หมายถึงแปลตรงตัวว่า "คืน" อย่าง เดียว แต่หมายถึงการส่งค่ากลับไปให้ส่วนที่เรียก method นั้น เพราะฉะนั้นคำถามสำคัญไม่ใช่ แค่ "มัน return ไหม" แต่คือ "return ค่าอะไร", "return กลับไปให้ใคร" และ "เราจำเป็นต้องเขียน return ทุกครั้งหรือไม่" ใน Ruby คำตอบข้อสุดท้ายมักเป็น "ไม่" เพราะโดยปกติ method จะ return ค่า expression สุดท้ายให้อัตโนมัติ

ตัวอย่างแบบลงมือดู

Example 1: method เล็ก ๆ ก็ยังมีจุดให้ตัดสินใจ

ผมชอบใช้การหารเป็นตัวอย่างแรก ๆ เพราะส่วนที่น่าสนใจไม่ใช่คณิตศาสตร์ ส่วนที่น่าสนใจคือ method ควรทำอย่างไรเมื่อ divisor เป็นศูนย์

class SafeDivider
  def divide(a, b)
    raise ArgumentError, "divisor cannot be zero" if b.zero?

    a.to_f / b
  end
end
worked_examples.md
ruby

ผมใช้ตัวอย่างนี้เพราะมันอ่านจากบนลงล่างได้ดี guard clause วางกรณีที่ใช้ไม่ได้ไว้ตั้งแต่ ต้น ส่วนทางเดินปกติของโค้ดก็ยังมองเห็นได้ชัด

ที่สำคัญไม่แพ้กันคือ มันแนะนำความเคยชินที่ผมอยากเก็บไว้ทั้งคอร์ส: ต่อให้ API จะเล็ก แค่ไหน มันก็ยังมีการตัดสินใจด้านการออกแบบเรื่อง failure ซ่อนอยู่เสมอ

Example 2: TDD ช่วยให้การออกแบบตรงขึ้น

ในหัวข้อแรก มันง่ายมากที่จะคิดว่าชุดทดสอบมีไว้แค่จับข้อผิดพลาดของโจทย์เลขคณิต แต่ผมอยากให้ คุณเห็นภาพที่กว้างกว่านั้น ชุดทดสอบเล็ก ๆ หนึ่งอันช่วยแยกสิ่งต่อไปนี้ออกจากกัน:

  • หน้าที่ของ object
  • พฤติกรรมที่คาดหวัง
  • edge case ที่ทำให้พฤติกรรมเปลี่ยนไป

ความเคยชินแบบนี้จะยิ่งมีค่ามากขึ้นเรื่อย ๆ เมื่อคอร์สซับซ้อนขึ้น โจทย์เลขคณิตอาจเล็ก แต่ นิสัยด้านการออกแบบไม่เล็กเลย

โพยสั้น

ผมไม่ได้คาดหวังให้คุณจำทุกอย่างในหัวข้อนี้ได้ตั้งแต่รอบแรก ผมอยากให้ cheat sheet นี้ เปิดค้างไว้ใกล้ ๆ ตอนคุณอ่านและเขียนโค้ด

รูปแบบพื้นฐาน

class Calculator
  def add(a, b)
    a + b
  end
end
cheatsheet.md
ruby

รูปแบบการทดสอบเบื้องต้น

RSpec.describe Calculator do
  it "adds two numbers" do
    expect(Calculator.new.add(2, 3)).to eq(5)
  end
end
cheatsheet.md
ruby

การคาดหวัง error

expect { divider.divide(10, 0) }.to raise_error(ArgumentError, /zero/)
cheatsheet.md
ruby

ประเด็นที่ผมอยากให้คุณจำ

  • โดยปกติ Ruby จะ return ค่า expression สุดท้าย
  • การสร้าง object ใหม่เขียนเป็น ClassName.new
  • ต่อให้ method จะเล็กแค่ไหน มันก็ควรสื่อเจตนาผ่านชื่อให้ได้

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

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

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

ผมแนะนำให้เดินแบบนี้:

  1. อ่าน overview.md เพื่อเห็นว่าหัวข้อนี้กำลังพยายามสอนอะไร
  2. อ่าน shortnote.md แล้วสังเกตการปรับวิธีคิดจาก Java ไป Ruby
  3. ใช้ worked_examples.md เพื่อดูว่าทำไม validation ถึงสำคัญ แม้ในโค้ดที่เล็กมาก
  4. เปิด cheatsheet.md ไว้ดูผ่าน ๆ ระหว่างอ่าน example.rb
  5. รันชุดทดสอบ และรักษาพฤติกรรมของตัวอย่างให้ยังเขียวอยู่
  6. ทำ basic_exercise.rb
  7. ทำ adv_exercise.rb

สิ่งที่ผมอยากให้คุณสังเกต

ระหว่างทำ ลองจับตาไอเดียเหล่านี้:

  • โดยปกติ Ruby methods ไม่ต้องเขียน return ออกมาตรง ๆ[^return_again]
  • method ที่สั้นก็ยังสื่อการตัดสินใจด้านการออกแบบที่หนักแน่นได้
  • guard clauses ช่วยไม่ให้กรณีผิดปกติมาบดบังทางเดินหลักของโค้ด

คำถามที่ผมอยากให้คุณพกไว้

เมื่อจบหัวข้อนี้ ลองกลับมาคิดกับคำถามเหล่านี้:

  • ทำไม SafeDivider ถึงเป็นตัวอย่างสอนที่ดีกว่า method คำนวณอีกอันที่ไม่มีกรณีพิเศษ
  • RSpec example สื่ออะไรได้บ้าง ที่ comment ธรรมดาทำไม่ได้
  • ตรงไหนที่ Ruby แบบกระชับ เริ่มกลายเป็น implicit มากเกินไปสำหรับผู้เริ่มต้น

[^return_again]: ถ้าคำว่า return ยังรู้สึกกว้างเกินไป ให้กลับไปดู footnote ใน shortnote.md อีกครั้ง ประเด็นสำคัญคือ method กำลังส่ง value กลับไปให้ส่วนที่เรียกมัน และใน Ruby เราไม่จำเป็นต้องเขียน return ทุกครั้งเพื่อให้สิ่งนั้นเกิดขึ้น

Source Files and Tests

โค้ด Ruby ใช้ร่วมจากโฟลเดอร์ en/ ของต้นฉบับ เพื่อให้สองภาษาผูกกับชุดทดสอบเดียวกัน

# EXAMPLE CODE
# Topic: topic_01_ruby_basics
#
# Purpose:
# - This file demonstrates reference implementation for the concept.
# - It should pass tests from the beginning.
# - Read and understand it before solving exercises.

class Calculator
  def add(a, b)
    a + b
  end
end
en/topic_01_ruby_basics/example.rb
Ruby course source
# STUDENT TASK (BASIC)
# Topic: topic_01_ruby_basics
#
# What to do:
# - Implement or improve the class/methods in this file.
# - Read tests in tests/topic_01_ruby_basics_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 Multiplier
  def multiply(a, b)
    a * b
  end
end
en/topic_01_ruby_basics/basic_exercise.rb
Ruby course source
# STUDENT TASK (ADVANCED)
# Topic: topic_01_ruby_basics
#
# Academic purpose:
# - Learn that even a tiny Ruby method still expresses a domain decision.
# - Practice guard clauses as a readable way to separate invalid input from the happy path.
#
# Real-world use case:
# - Division-like operations appear in pricing, reporting, analytics, and rate calculations.
# - In those settings, silently accepting a zero divisor usually hides a bug upstream.
# - Raising a focused exception is often better than producing misleading data.
#
# Why Ruby is beautiful here:
# - A guard clause states the invalid condition in one line.
# - The normal behavior remains visually dominant.
# - The method stays short without losing intent.
#
# What to do:
# - Complete the challenge behavior requested by the guide.
# - Explain to yourself why raising is preferable to returning a magic value here.
# - Use tests in tests/topic_01_ruby_basics_spec.rb under the "advanced exercise" examples.
#
# Expected outcome:
# - Advanced tests pass and you can justify the validation strategy.

class SafeDivider
  def divide(a, b)
    raise ArgumentError, "divisor cannot be zero" if b.zero?

    a.to_f / b
  end
end
en/topic_01_ruby_basics/adv_exercise.rb
Ruby course source
# ANSWER KEY (BASIC)
# Topic: topic_01_ruby_basics
#
# Solution idea:
# - The behavior is intentionally small.
# - The point is to see that Ruby can express a clear method without ceremony.
# - Multiplication delegates to Ruby's numeric operator directly.

class Multiplier
  def multiply(a, b)
    a * b
  end
end
en/topic_01_ruby_basics/answer_basic_exercise.rb
Ruby course source
# ANSWER KEY (ADVANCED)
# Topic: topic_01_ruby_basics
#
# Solution idea:
# - Separate invalid input from the normal calculation path.
# - A guard clause makes the failure policy explicit at the top of the method.
# - Returning a float keeps the result predictable for division.

class SafeDivider
  def divide(a, b)
    raise ArgumentError, "divisor cannot be zero" if b.zero?

    a.to_f / b
  end
end
en/topic_01_ruby_basics/answer_adv_exercise.rb
Ruby course source
# This spec is your learning companion for topic_01_ruby_basics.
#
# 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_01_ruby_basics" do
  describe "EXAMPLE purpose: understand the reference implementation" do
      it "adds numbers in Calculator" do
        expect(Calculator.new.add(2, 3)).to eq(5)
      end
  end

  describe "BASIC EXERCISE purpose: implement the comparable task" do
      it "multiplies positive/zero/negative numbers" do
        m = Multiplier.new
        expect(m.multiply(3, 4)).to eq(12)
        expect(m.multiply(7, 0)).to eq(0)
        expect(m.multiply(-2, 5)).to eq(-10)
      end
  end

  describe "ADVANCED EXERCISE purpose: solve challenge and edge cases" do
      it "divides safely and raises for zero divisor" do
        d = SafeDivider.new
        expect(d.divide(10, 2)).to eq(5.0)
        expect { d.divide(10, 0) }.to raise_error(ArgumentError, /zero/)
      end
  end
end
en/topic_01_ruby_basics/tests/topic_01_ruby_basics_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_01_ruby_basics/run_topic_tests.sh
Ruby course source

Study Prompts

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

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