TDD

Ruby for Java Developers: คู่มือ TDD สำหรับ repo นี้

คู่มือ Ruby TDD

th/ruby_tdd_guide_for_java_developers.md

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

repo ฉบับเต็มตอนนี้มี 15 หัวข้อ 2 ภาคผนวก ชุดทดสอบรายหัวข้อที่รันได้จริง และไฟล์ คำตอบที่แยกออกจากลำดับปกติของแบบฝึกหัด นั่นทำให้ผมสอน Ruby ที่นี่เป็นเส้นทางการ เรียนเต็ม ๆ ได้ แทนที่จะเป็น workshop สั้น ๆ

ฉบับภาษาไทยใน /th ตอนนี้ครอบคลุมเอกสาร Markdown ทั้งคอร์สแล้ว ส่วนไฟล์ Ruby และชุดทดสอบที่รันได้จริงยังอยู่ฝั่ง en/ เหมือนเดิม เพื่อให้การเรียนกับการรันโค้ดใช้ ฐานเดียวกันทั้ง repo

ทำไมผมจึงเขียนคู่มือนี้แบบนี้

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

ถ้าคุณอยากได้ภาพรวมสั้นที่สุด ให้เริ่มที่ README.md ถ้าคุณอยากเห็นแผนที่ ของคอร์ส ให้ไปที่ course_overview.md ผมเขียนคู่มือนี้ไว้ตรงกลาง ระหว่างสองไฟล์นั้น เพื่ออธิบายว่าผมอยากให้คุณเดินผ่าน repo นี้อย่างไร และการปรับวิธีคิด ช่วงไหนสำคัญที่สุดระหว่างทาง

ตอนผมเขียนคู่มือนี้ ผมตั้งสมมติฐานว่า

  • คุณอ่าน Java ได้พอสมควร
  • คุณเข้าใจ object-oriented programming ระดับพื้นฐาน
  • คุณเคยเขียนหรือเคยอ่าน unit tests มาก่อน

คู่มือนี้ไม่ได้สมมติว่าคุณเคยเขียน Ruby มาก่อน

ถ้าคุณเคยใช้ภาษาแบบ dynamic อื่นมาก่อน ไวยากรณ์บางส่วนอาจคุ้นตา แต่คำถามด้านการ ออกแบบก็ยังสำคัญอยู่ดี

ใน repo นี้มีอะไรอยู่จริงบ้าง

ไฟล์ Ruby และชุดทดสอบที่รันได้จริงของคอร์สอยู่ใต้ en/ และมี 15 หัวข้อ ส่วนเอกสาร Markdown ภาษาไทยของทั้งคอร์สอยู่ใต้ th/:

  1. topic_01_ruby_basics
  2. topic_02_methods_keywords_blocks
  3. topic_03_collections_enumerable
  4. topic_04_strings_symbols_ranges_regex
  5. topic_05_control_nil_exceptions
  6. topic_06_classes_modules_composition
  7. topic_07_duck_typing_polymorphism
  8. topic_08_ruby_specific_features
  9. topic_09_mini_capstone
  10. topic_10_rock_paper_scissors
  11. topic_11_csv_files_queries
  12. topic_12_sqlite_queries
  13. topic_13_recursion_closures
  14. topic_14_functional_style_lambdas
  15. topic_15_reduce_accumulators

นอกจากนี้ยังมี 2 ภาคผนวก:

  • appendix_a_project_lifecycle
  • appendix_b_vscode_ruby_workflow

แต่ละหัวข้อใช้โครงสร้างหลักชุดเดียวกัน:

  • overview.md
  • shortnote.md
  • cheatsheet.md
  • worked_examples.md
  • guide.md
  • example.rb
  • basic_exercise.rb
  • adv_exercise.rb
  • answer_basic_exercise.rb
  • answer_adv_exercise.rb
  • tests/
  • run_topic_tests.sh

ความสม่ำเสมอนี้สำคัญกับผมมาก ผมไม่อยากให้คุณเสียแรงไปกับการเดาโครงสร้างใหม่ทุก ครั้งที่เปลี่ยนหัวข้อ ผมอยากให้คุณเอาแรงนั้นไปใช้กับโค้ดแทน

วิธีรันคอร์ส

จาก en/ ให้รันคอร์สเต็มแบบนี้:

./run_tests.sh
ruby_tdd_guide_for_java_developers.md
bash

รันหัวข้อเดียวจากรากคอร์ส:

./run_tests.sh topic_01_ruby_basics
ruby_tdd_guide_for_java_developers.md
bash

หรือจะรันผ่านสคริปต์ของหัวข้อนั้นเอง:

./topic_01_ruby_basics/run_topic_tests.sh
ruby_tdd_guide_for_java_developers.md
bash

สคริปต์รันหลักจะตรวจ gems ที่คอร์สต้องใช้และติดตั้งให้ถ้ายังไม่มี ตอนนี้รวมถึง erb, csv, sqlite3 และ rspec

พอคุณไปถึง CSV กับ SQLite คุณจะไม่ได้กำลังเรียนแค่ฟีเจอร์ของภาษาแล้ว แต่กำลังเห็น โครงของ Ruby project ขนาดเล็กด้วย ผมจึงอยากให้เครื่องมือรอบ ๆ ง่ายพอที่คำถามด้าน การออกแบบจะยังเด่นอยู่

วิธีที่ผมแนะนำให้คุณเรียนหนึ่งหัวข้อ

ลำดับที่ผมแนะนำคือ:

  1. อ่าน overview.md
  2. อ่าน shortnote.md
  3. อ่าน worked_examples.md
  4. เปิด cheatsheet.md ไว้ข้าง ๆ ตอนอ่านไฟล์ Ruby
  5. เปิดดู example.rb
  6. รันชุดทดสอบของหัวข้อนั้น
  7. ทำ basic_exercise.rb
  8. ทำ adv_exercise.rb
  9. เปิดไฟล์คำตอบหลังจากพยายามเองจริงแล้ว

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

จังหวะ TDD ที่ผมอยากให้คุณรักษาไว้

ทั้ง repo นี้สร้างอยู่บนวงจรสั้น ๆ ที่ทำซ้ำได้:

  1. เขียนหรือเปิดดู expectation ที่กำลัง fail
  2. เขียนโค้ดให้น้อยที่สุดเท่าที่ทำให้มันผ่านได้
  3. refactor เพื่อให้มันชัดขึ้น

คนที่มาจาก Java มักเข้าใจวงจรนี้ในทางทฤษฎี แต่ยังเผลอเชื่อ implementation มากกว่า test อยู่บ่อย ๆ Ruby มักให้รางวัลกับนิสัยตรงข้าม คือปล่อยให้ test ช่วยคุมขนาดของการ ออกแบบไว้

ถ้า spec เขียนยาก ผมอยากให้คุณมองว่านั่นคือข้อมูล ไม่ใช่แค่ความน่ารำคาญ object อาจ กำลังทำมากเกินไป ชื่ออาจยังขุ่น หรือเส้นแบ่งระหว่าง object ที่ทำงานร่วมกันอาจวางผิดที่

15 หัวข้อนี้ค่อย ๆ ต่อกันอย่างไร

ตอนนี้คอร์สใหญ่พอที่ผมมองมันเป็นช่วง มากกว่าจะเป็นบทเรียนที่แยกขาดจากกัน

ช่วงที่ 1: ปรับวิธีคิดให้เป็น Ruby

หัวข้อ 1 ถึง 5 สอนการขยับวิธีคิดชุดแรก:

  • expressions แทนพิธีรีตองรอบนอก
  • methods และ keyword arguments
  • Enumerable และการแปลงข้อมูลใน collections
  • strings, symbols, ranges และ regex
  • guard clauses, nil safety และ exceptions

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

ช่วงที่ 2: การออกแบบ object แบบ Ruby

หัวข้อ 6 ถึง 9 พาไปสู่เรื่องขอบเขตของ object และการร่วมมือกันระหว่าง object:

  • classes, modules และ composition
  • duck typing และ polymorphism
  • dynamic Ruby features แบบใช้ด้วยความยับยั้งชั่งใจ
  • capstone ขนาดเล็กที่ดึงของเดิมมารวมกัน

ตรงนี้ผมเริ่มถามคำถามที่น่าสนใจกว่าเดิม ไม่ใช่แค่ว่า Ruby ทำอะไรได้ แต่ถามว่าพฤติกรรม ควรอยู่ตรงไหน

ช่วงที่ 3: Ruby ในงานที่มีบริบทจริง

หัวข้อ 10 ถึง 12 เอา Ruby ไปเจอกับปัญหาที่ใช้งานได้จริง:

  • การออกแบบเกมแบบค่อย ๆ ขยาย
  • การทำงานกับ CSV และ service สำหรับค้นข้อมูลขนาดเล็ก
  • การใช้งาน SQLite, การค้นข้อมูล และ prepared statements

หัวข้อชุดนี้สำคัญ เพราะมันบังคับให้ภาษาไปเจอกับ state, files, รูปแบบข้อมูลนำเข้า และ persistence ผู้เรียนที่สบายกับช่วงแรกแล้ว ต้องรักษาวินัยเดิมไว้ในปัญหาที่มีชิ้นส่วน มากขึ้น

ช่วงที่ 4: วิธีคิดที่สูงขึ้นอีกระดับ

หัวข้อ 13 ถึง 15 พาไปสู่ปัญหาที่นามธรรมขึ้น:

  • recursion และ closures
  • การแปลงข้อมูลทีละขั้นด้วย map, zip และ lambdas
  • การออกแบบแบบใช้ accumulator ด้วย reduce และ each_with_object

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

การปรับวิธีคิดหลักจาก Java ไป Ruby

ส่วนที่ยากที่สุดของ Ruby สำหรับคนที่มาจาก Java มักไม่ใช่ไวยากรณ์ แต่คือรสนิยมในการ ตัดสินใจ

การขยับที่เกิดขึ้นซ้ำบ่อยมักมีประมาณนี้:

  • ให้ความสำคัญกับชื่อที่บอกพฤติกรรม มากกว่าชื่อที่บอกชนิดข้อมูล
  • ให้ความสำคัญกับ expressions มากกว่าพิธีรีตอง
  • ใช้ความสามารถของ collections ที่มีอยู่ก่อนจะลงมือเขียน loops เอง
  • ใช้ composition ก่อน inheritance เว้นแต่ inheritance จะมีเหตุผลจริง
  • มอง object ที่ทำงานร่วมกันจากสิ่งที่มันทำได้ ไม่ใช่จากลำดับชั้นทางเครือญาติของชนิดข้อมูล
  • ยอมให้โค้ดกระชับได้ ก็ต่อเมื่อชื่อยังแบกความหมายอยู่

ตารางเทียบแบบเร็วก็ยังช่วยได้:

Java Ruby
Static typing (String name) Dynamic typing (name = "A")
public class User {} class User ... end
getter/setter ที่ต้องเขียนซ้ำ attr_reader, attr_accessor
List<String> Array
Map<K,V> Hash
stream().map() map
interfaces duck typing และ modules
checked exceptions ไม่มี checked exceptions
null nil

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

นิสัยที่ผมอยากให้คุณคอยจับตา

นิสัยเหล่านี้มักทำร้ายคนที่มาจาก Java ในคอร์สนี้:

  1. เปลี่ยนทุก attribute ให้กลายเป็น getter/setter
  2. สร้าง inheritance tree ทั้งที่ object ร่วมงานง่าย ๆ ก็พอแล้ว
  3. เขียน loops เอง แทนที่จะเรียน Enumerable
  4. ทำทุก utility ให้เป็น class method
  5. mock ชุดทดสอบมากเกินไป แทนที่จะทำให้การออกแบบชัดขึ้น
  6. สับสนระหว่าง "สั้นลง" กับ "ดีขึ้น"

จริง ๆ แล้วทั้งคอร์สนี้ก็คือบทสนทนายาว ๆ กับนิสัยพวกนี้นั่นเอง

วิธีที่ผมอยากให้คุณใช้ไฟล์คำตอบ

ไฟล์คำตอบถูกแยกออกจากเส้นทางปกติของการรันโดยตั้งใจ ชุดทดสอบจะใช้:

  • example.rb
  • basic_exercise.rb
  • adv_exercise.rb

และจะไม่ใช้:

  • answer_basic_exercise.rb
  • answer_adv_exercise.rb

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

เวลาอ่านไฟล์คำตอบ ลองถามตัวเองว่า:

  • ชื่อที่ใช้ชัดกว่าของผมหรือไม่
  • ทางเดินของโค้ดเรียบกว่าหรือไม่
  • คำตอบนี้ทำให้เส้นแบ่งของ object ชัดขึ้นหรือไม่
  • มันงามขึ้นจริง หรือแค่สั้นลง

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

ทำไมผมจึงใส่ภาคผนวกไว้ด้วย

ภาคผนวก A ว่าด้วยด้านปฏิบัติการของงาน Ruby: เครื่องมือ, วงจรชีวิตของโปรเจกต์ และนิสัย ของการอยู่ใน Ruby environment จริง

ภาคผนวก B ว่าด้วยวิธีทำงานใน VS Code ที่สงบและมองเห็นภาพมากขึ้น สำหรับทั้งคนเรียน และคนสอน

ผมไม่มองภาคผนวกเป็นของแถม เพราะการเรียน Ruby ใน repo แบบนี้ ไม่ได้มีแค่ expressions กับ objects แต่มีเรื่องของการทำงานกับ Ruby project จริงอยู่ด้วย

วิธีเดินคอร์สที่ผมแนะนำ

ถ้าคุณอยากได้เส้นทางที่สมเหตุสมผลผ่าน repo นี้ ผมแนะนำแบบนี้:

  1. ทำหัวข้อ 1 ถึง 5 ตามลำดับโดยไม่ข้าม
  2. ชะลอความเร็วลงที่หัวข้อ 6 ถึง 9 เพราะคำถามด้านการออกแบบจะเข้มขึ้น
  3. มองหัวข้อ 10 ถึง 12 เป็นงานเขียนโปรแกรมประยุกต์ ไม่ใช่แค่การเรียนฟีเจอร์ภาษา
  4. ค่อย ๆ ทำหัวข้อ 13 ถึง 15 ช้าพอที่จะอ่าน transformations ได้อย่างเข้าใจจริง

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

คำแนะนำสุดท้าย

มอง Ruby เป็นอีกภาษาหนึ่ง ไม่ใช่ Java ที่ถอด semicolons ออก

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