พอดีวันก่อนได้ไปพาทีมที่มาเข้า workshop ทำ coding dojo เกี่ยวกับการเขียน Unit Testing ด้วยวิธี Test Driven Development (TDD) เลยขอจดบันทึกมุมมองเรื่องนี้ไว้สักหน่อย

ก่อนอื่นมาทำความรุ้จัก TDD กันก่อน

ทำความรู้จักกับ TDD
TDD ย่อมาจาก Test Driven Development หมายถึง กระบวนการพัฒนาซอฟต์แวร์ที่ขับเคลื่อนด้วยการทดสอบ เน้นการเขียน Test ก่อนที่จะเขียน Code โดยทำตาม 3 ขั้นตอนหลัก ดังนี้ 1. เขียน Test: เขียนกรณีทดสอบ (Test Case) เพื่อระบุพฤติกรรมที่คาดหวังของซอฟต์แวร์ 2. รั

ทั้งหมดนี้เป็น Step ในการเขียน code โดยนำเอาแนวคิดของ TDD เข้ามาใช้งานร่วมกัน ต่อไปเราจะมาเข้าเรื่องหลักของบทความนี้กัน


ทำไม TDD ถึงต้องเริ่มเขียนจากโค๊ดที่ง่ายก่อน

เป็นเรื่องปกติที่เมื่อเราได้รับโจทย์ในการเขียน code มาแล้ว เราจะคิดออกได้ในทันทีเลยว่า เราจะต้องเขียนอะไร

สมมติว่า เราได้โจทย์มาว่า ให้นับผลรวมของข้อมูลแต่ละชุด ในเกมส์ Yahtzee ตัวเลขที่เหมือนกัน จากข้อมูลที่ส่งมาให้ทั้งหมด 5 ตัว (ลองไปอ่านเงื่อนไข และวิธีเล่นกันดู)

เช่น

  • [1,1,3,4,5] = 2
  • [2,4,5,5,5] = 15
  • [3,3,1,1,2] = 6 | 2

ถ้าเห็นโจทย์และตัวอย่างแล้ว เราสามารถบอกได้ทันทีว่า มันต้องใช้ loop และก็ if เช็คค่า แต่หลังจากนั้นเราก็จะเริ่มคิดไม่บอกแล้วว่า ต้องทำยังไงต่อ

เวลาผมทำทีมทำ coding dojo ด้วยการใช้ TDD นั้น ผมมักพบอาการนี้เสมอ ทุกคนรู้ว่า มันต้องใช้ loop และ if แต่มันต้องทำยังไงต่อ

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

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

ซึ่งมันแทบจะเป็นไปไม่ได้เลย ที่เราจะเขียนมันขึ้นมาได้ หากเราไม่เข้าใจมันทั้งหมดก่อน

ดังนั้น แทนที่เราจะเขียน function หนึ่งฟังก์ชั่นที่สามารถแก้ไขทุกปัญหา ให้เราเริ่มจากการเขียน code แก้โจทย์ง่ายๆ ไม่ซับซ้อน ด้วยการแก้มันไปทีละปัญหาให้ได้ก่อน

เช่น

ส่งค่า [1,1,3,4,5] เพื่อคำนวนหาผลรวมของตัวเลข 1 ที่เหมือนกัน

เราก็จะเขียนได้ว่า

function sumScoreOfOnece(disc1, disc2, disc3, disc4, disc5) {
  let score = 0;
  if (disc1 === 1) score += 1;
  if (disc2 === 1) score += 1;
  if (disc3 === 1) score += 1;
  if (disc4 === 1) score += 1;
  if (disc5 === 1) score += 1;

  return score;
}

Solve One Problem

เมื่อแก้ปัญหาหนึ่งปัญหาและทดสอบเสร็จแล้ว ก็ค่อยมาดูว่าเราจะ refactor มันมั้ย ซึ่งเราสามารถ refactor บางอย่างได้ก่อน หรืออาจจะข้ามการ refactor ไปก่อน แล้วลองแก้ปัญหาเพิ่มอีกสัก 2-3 ปัญหา หรือจนกว่าจะแก้ปัญหาทั้งหมดได้ เพื่อให้เห็น Pattern การทำงานของมัน

function sumScoreOfOnece(disc1, disc2, disc3, disc4, disc5) {
  let score = 0;
  if (disc1 === 1) score += 1;
  if (disc2 === 1) score += 1;
  if (disc3 === 1) score += 1;
  if (disc4 === 1) score += 1;
  if (disc5 === 1) score += 1;

  return score;
}

function sumScoreOfTwos(disc1, disc2, disc3, disc4, disc5) {
  let score = 0;
  if (disc1 === 2) score += 2;
  if (disc2 === 2) score += 2;
  if (disc3 === 2) score += 2;
  if (disc4 === 2) score += 2;
  if (disc5 === 2) score += 2;

  return score;
}

function sumScoreOfthree(disc1, disc2, disc3, disc4, disc5) {
  let score = 0;
  if (disc1 === 3) score += 3;
  if (disc2 === 3) score += 3;
  if (disc3 === 3) score += 3;
  if (disc4 === 3) score += 3;
  if (disc5 === 3) score += 3;

  return score;
}

Solve 2-3 problems

พอเรามองเห็น pattern การทำงานของมันแล้ว ก็ค่อย refactor ใหม่มันทำงานได้ดีขึ้น หรือถูกต้องขึ้น

function sumScoreByTarget(disc, target) {
  let score = 0;
  for (let i = 0; i < disc.length; i++) {
  	if (disc[i] === target) {
      score += target
    }
  }

  return score;
}

code after refactor

เท่านี้เราก็สามารถแก้ปัญหาได้แล้ว

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

การทำแบบนี้ ดูเหมือนจะช้า แต่บอกเลยว่า มันเร็วกว่าการที่ต้องมานั่งคิดวิธีการแก้ปัญหาใหญ่ๆ ให้ได้ทั้งหมดตั้งแต่ต้น ซึ่งถ้าอ่านมาจนถึงนี้แล้ว รู้สึกคุ้นๆ กับวิธีการ ก็ไม่ต้องแปลกใจ เพราะมัน คือ การคิดแบบ Critical Thinking นั้นเอง

อีกหนึ่งวิธีคิดที่ผมใช้บ่อยมากๆ คือ

"Make it Work, Make it Right, Make it Fast" – Kent Beck

Make it Work: ทำยังไงก็ได้ให้ code ที่เราเขียนสามารถทำงาน แก้ปัญหาได้
Make it Right: Refactor หรือปรับปรุง code ของเราให้มันดีขึ้น หรือ ถูกต้องตามที่มันควรจะเป็น
Make it Fast: ปรับปรุง code ของเราให้สามารถทำงานได้เร็วขึ้น

สรุป

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

การเริ่มต้นจากอะไรง่ายๆ ตรงไปตรงมา มันช่วยเราในการเรียงลำดับความคิด จากเล็กๆ ง่ายๆ ไปจนถึงระดับที่ใหญ่และซับซ้อนได้