[หนังสือแปล] Refactoring JavaScript: บทที่ 1

Tanakorn Numrubporn
p4ftech
Published in
7 min readJul 16, 2017
Refactoring คืออะไร?

หนังสือเล่มได้รับอนุญาติให้แปลได้เป็นที่เรียบร้อยแล้วจาก Evan Burchard หากใครอยากอ่านต้นฉบับสามารถไปอุดหนุนผู้เขียนได้ที่ Amazon

Refactoring คืออะไร?

Refactoring ไม่ใช่การแก้ไข Code นะจ๊ะ

อาจจะมีคนเถียงว่า “ก็เวลา Refactoring กรูก็ต้องแก้ Code อยู่ดี หรือใครจะไฝว้” ซึ่งผมก็ไม่กล้าเถียงหรอกครับพี่ เพียงแต่ความหมายของมันกินความมากกว่านั้น

การแก้ไข Code ถือเป็นกิจกรรมหนึ่งในการ Refactoring แต่เราไม่ควรบอกแบบเหมารวมว่า การ Refactoring คือการ “แก้ไข Code” เพราะนิยามของการ Refactoringคือ “การแก้ไข Code โดยที่ไม่กระทบต่อ พฤติกรรม ของมัน” การนิยามแบบนี้จะนำมาซึ่งคำถามสองข้อในทันทีว่า:

  • แล้วกระผมจะเอาอะไรไปรับประกันได้ว่า พฤติกรรมของ Code จะไม่เปลี่ยน?
  • แล้วจะให้กระผมแก้ Code ไปทำซากอ้อยอะไร หากแก้แล้วพฤติกรรม Code ยังเหมือนเดิม?

เนื้อหาภายในบทนี้ จะมุ่งไปที่การตอบคำถามทั้งสองข้อข้างต้นเป็นหลัก อ้อ! อย่าหวังว่าจะได้อ่านเนื้อหาประวัติศาสตร์ของ JavaScript นะครับ เพราะในอินเตอร์เน็ตมีข้อมูลเกี่ยวกับเรื่องนี้เต็มไปหมดอยู่แล้ว

แล้วกระผมจะเอาอะไรไปรับประกันได้ว่า พฤติกรรมของ Code จะไม่เปลี่ยน?

ให้ตอบตรงๆ มั๊ยครับ บอกเลยว่า การแก้ Code โดยคงพฤติกรรมให้เหมือนเดิมนั้น เป็นเรื่องที่โคตรยาก เดชะบุญที่มีพฤติกรรมของ Code บางกลุ่มที่เราไม่ต้องไปสนใจในจังหวะ Refactoring (ซึ่งเราจะพูดถึงในภายหลัง) ได้แก่:

  • รายละเอียดของการ Implementation
  • พฤติกรรมที่ยังไม่ได้รับการ Test และไม่ได้ระบุใน Spec.
  • Performance

ตอบแบบกว้างๆ ก็คือ การคงพฤติกรรมของ Code จะสัมฤทธิ์ผลได้ด้วยการใช้ Test กับ Version Control

William Opdyke ได้นำเสนอวิธีการ Refactoring อีกแบบหนึ่งผ่านวิทยานิพนธ์ของเขา (สามารถไปหาอ่านวิทยานิพนธ์ของเขาได้ที่ http://www.ai.univ-paris8.fr/~lysop/opdyke-thesis.pdf) วิธีการก็คือ การใช้พวก Automated Tools ทั้งหลายเพื่อทำการแก้ Code แบบรับประกันความปลอดภัย Coder ระดับมืออาชีพอาจจะมองว่า วิธีการนี้เป็นการกำจัดมนุษย์ออกจากวงจร อีกทั้งยังไปจำกัดรูปแบบการแก้ Code ให้แคบลงอีกด้วย เพราะ Tool จะแก้ code ให้ก็ต่อเมื่อมัน “ปลอดภัย” ที่จะแก้ ซึ่งผู้ใช้งาน Tool จะต้องเข้าไป Config ไว้แต่แรก

ส่วน Martin Fowler ได้นำเสนออีกวิธีหนึ่ง เป็นแนวทางที่คล้ายๆ กับแคตตาล็อก โดยเอารูปแบบของการ Refactoring ทุกแบบยัดเข้าไปในเอกสารชุดเดียว แล้วให้ Coder ไปเปิดดูเอาเอง วิธีการนี้ถูกนำเสนอผ่านหนังสือของเขาที่ชื่อว่า Refactoring: Improving the Design of Existing Code (Addison-Wesley) ซึ่งได้รับการพิสูจน์มาแล้วว่ายากสุดขั้ว แล้วยิ่งมาอยู่ในโลกของ JavaScript โลกที่เต็มไปด้วยความเปลี่ยนแปลง อุดมไปด้วยแนวทางที่ต่างคนต่างทำ ไม่มีมาตรฐานกลาง ใครใคร่เสนออะไร ก็เสนอไป (เดี๋ยวผมจะพูดเรื่องนี้โดยละเอียดในบทที่ 2) แนวทางแบบเปิดแคตตาล็อกก็ดูจะยิ่งไม่เข้าพวกกันไปใหญ่ แถมจะดูไม่ทันการเปลี่ยนแปลงที่รวดเร็วแบบในโลกของ JS เสียด้วยสิ

ปู่มาร์ตินเลือกใช้วิธีทิ้ง Automated Tools ไป แล้วหันมาใช้มนุษย์ในการทำงานแทน โดยยังคงแก่นเดิมของการ Refactoring ไว้ซึ่งก็คือ การปรับแก้ Code โดยสร้างผลกระทบให้เกิดน้อยที่สุด

หากเลือกใช้วิธีการแบบ “Opdykian” ที่เน้น Automated Tools เป็นหลัก Tool เหล่านั้นจะกลายมาเป็นตัวดึงหลังคุณไว้ในอนาคต แต่ถึงกระนั้น หนังสือเล่มนี้ก็ไม่เลือกใช้วิธีการแบบปู่มาร์ตินเช่นกัน (ที่เน้นแนวเขียนแบบ step-by-step) แต่เราจะใช้วิธีการ Refactoring โดยใช้ Test เป็นกองหลังสนับสนุน แล้วใช้ Version Control เป็นหน่วยเก็บกวาดยามเกิดความผิดพลาด

เหตุผลก็เพราะ ในจังหวะที่เรากำลังเพิ่มความเชื่อมั่นใน Code ผ่านการ Refactoring หากมี Test คอยคุมอยู่เบื้องหลัง เราจะสามารถชี้วัดความสำเร็จของการ Refactoring ได้ง่าย และหากมีอะไรผิดพลาด เรายังสามารถใช้ Version Control (เราเลือกใช้ Git) ในการ “Rollback” ไปหา Code เวอร์ชั่นก่อนหน้าได้อย่างง่ายดาย

คำเตือน! จงใช้ Version Control!

หากไม่สามารถย้อน version กลับไปหา version ที่ปลอดภัยได้ การ “แก้ Code” จะกลายเป็นกิจกรรมที่เพิ่มความเสี่ยงเข้าไปใน Codebase ของคุณในทันที หาก Codebase ที่คุณกำลังใช้ในงานจริงไม่มี Version Control แล้วล่ะก็ ปิดหนังสือเลยครับ แล้วไปติดตั้งให้เรียบร้อยก่อน

หากคุณยังไม่เคยใช้ Version Control อยากให้คุณลองพิจารณา Git (http://git-scm.com/ ) พร้อมๆ ไปกับ GitHub (http://github.com ) เครื่องมือสำหรับการ Backup Codebase ของคุณ

ขอยอมรับแบบลูกผู้ชายเลยว่า หากเปรียบเทียบกับวิธีทั้งสองแบบที่กล่าวมาข้างต้น แนวทางในหนังสือเล่มนี้อาจจะดูเป็นแนวตามใจผู้เขียน และบางครั้งดูไม่มีมาตรฐานทางวิชาการมารองรับซักเท่าไหร่ วิธีที่เราเลือกใช้จะนำเอา TDD มาเป็นฐาน โดยใช้วงจรของ “Red” (จุด Fail ของการ Test) “Green” (จุด Pass ของการ Test) และ “Refactor” แล้วเมื่อมีสิ่งใดผิดพลาด แค่ Rollback ก็จบแล้ว

ไม่แน่นะครับ หากในอนาคต มีคนสร้าง Automated Refactoring Tool โดยใช้ข้อมูลจากแคตตาล็อกของปู่มาร์ติน เราก็อาจจะเอามารวมกับแนวทางของหนังสือเล่มนี้แล้วออกวิธีการที่สมบูรณ์พร้อมต่อไป (ตื่น ครับตื่น!) แต่เอาจริงๆ ผมคิดว่า ฝันหวานแบบนี้คงจะไม่เกิดขึ้นในอนาคตอันใกล้เป็นแน่แท้

เป้าหมายของพวกเราก็คือ ช่วยดึง JavaScript Developer ขึ้นมาจากหลุมทรายดูดของ Bad Code ให้ได้ โดยไม่สนใจว่าจะใช้วิธีไหน ไม่ว่าจะเป็น Automate (ของ Opdyke) หรือแคตตาล็อก (ของปู่มาร์ติน) ก็ได้ทั้งนั้น เพราะที่สุดแล้ว บรรดาเหล่าองค์เทพ และพรหมทุกองค์ที่เขียนหนังสืออกมา ต่างก็ช่วยให้เราได้รับ mindset ใหม่ๆ ของการสร้าง Better Code อย่างปลอดภัยไปแล้ว และนั่นถือเป็นคุณูปการอันใหญ่หลวง

ทำไมเราถึงไม่สนใจผลกระทบต่อพฤติกรรมของ Code ในระดับ Implementation

สมมติว่าเรามีฟังก์ชั่นสำหรับเพิ่มค่า input เป็นสองเท่า ดังนี้:

function byTwo(number){  return number * 2;}

เราสามารถเขียน Code ใหม่ขึ้นมาโดยยังคงรักษาเป้าหมายเดิมไว้ ด้วยการปรับเปลี่ยนแนวทางไปนิดหน่อย:

function byTwo(number){  return number << 1;}

ทั้งสอง function สามารถนำไปใช้ได้โดยไม่มีผลเสียหายอะไร ส่วนการ Test ก็แค่ทำการจับคู่ระหว่างเลขสองตัว นั่นคือ input กับ output แล้วทดสอบว่า output มีค่าเป็นสองเท่าของ input หรือเปล่าเท่านี้ก็จบ แต่โดยส่วนมากแล้ว เนื้อหาภายในหนังสือเล่มนี้ จะมุ่งความสนใจไปที่ Output มากกว่ารายละเอียดของฟังก์ชั่นว่าเลือกใช้ * หรือ << เป็น operator ในการคำนวณ ซึ่งตรงนี้แหละที่เรียกว่ารายละเอียดของ Implementation ถึงแม้ว่าบางคนอาจจะบอกว่า Implementation ก็คือพฤติกรรมของ Code ในรูปแบบนึง แต่มันเป็นพฤติกรรมที่ไม่ได้มีความสำคัญกับบริบทของ Refactoring ซึ่งโฟกัสไปที่เรื่องของ Input และ Output เสียมากกว่า

หากเราเลือกใช้ฟังก์ชั่น byTwo แบบที่สอง เราอาจจะพบว่าฟังก์ชั่นจะพังเมื่อ input มันใหญ่เกินไป (ลองใช้ ร้อยล้าน: 1000000000000 << 1 ดูดิ) มีข้อยืนยันแบบนี้ ผมในฐานะผู้เขียนหนังสือยังจะยืนยันว่ารายละเอียด Implementation ไม่สำคัญอีกเหรอ

คำตอบก็คือ ไม่สนใจ เหมือนเดิมครับ เพราะเราสนใจแค่ว่า output เราพังเท่านั้น ซึ่งแปลว่า ใน Test Suite ของเรา ยังเขียนไว้ไม่ครอบคลุมพอก็แค่นั้น (จบปะ) และหากว่าการกลับไปใช้ * operator จะช่วยให้ Test ตัวนี้ผ่าน เราก็โอเคครับ ส่วนคุณในฐานะ Coder จะเขียนว่า return number * 2 หรือ return number + numberมันก็แล้วแต่คุณ (ชัดมะ)

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

การเขียน Test แบบเฉพาะเจาะจงเกินไปในบางครั้ง นอกจากจะเป็นสิ่งเกินความจำเป็นในบางครั้งแล้ว มันยังส่งผลให้ Codebase มีข้อจำกัดและทำให้การ Refactoring ทำได้ไม่สะดวก

Test ภาษา JavaScript

หากคุณต้องการจะ Test เพื่อเจาะลึกในระดับ Implementation บางทีคุณอาจจะแค่ทำการทดสอบ environment ของภาษา แต่ไม่ได้ Test ตัวโปรแกรมก็เป็นได้ ซึ่งเนื้อหาในเรื่องการ Test นั้น เราจะมาพูดถึงกันในภายหลัง แต่ตอนนี้ เราจะลองรัน test ใน node console โดยคุณผู้อ่านอาจจะเขียน test ลงในไฟล์ แล้วรันด้วยคำสั่ง node file_name.js หรือเขียนสดๆ ลงใน console ก็ไม่ผิดกติกา

ก่อนจะไปต่อ เราอยากให้คุณตรวจเช็คดูก่อนว่าเครื่องของคุณได้ติดตั้ง nodejs ไว้หรือยัง หากยังไม่ติดตั้ง ให้ไปดาวน์โหลดได้ที่ https://nodejs.org/en/

เมื่อติดตั้งเสร็จแล้วให้เปิด terminal/command line tool แล้วพิมพ์คำสั่งว่า

`$node

สมมติว่าคุณกำลัง Test Code ดังต่อไปนี้:

> assert = require(‘assert’);
> assert(2 + 2 === 4)

output ของ Code บรรทัดแรกดูค่อนข้าง น่ากลัว แต่อย่าไปหงอ มันก็แค่แสดงให้เราเห็นว่ากำลังโหลดอะไรอยู่ จริงๆ แล้วตัว node มี assert ติดมาให้อยู่แล้ว ดังนั้น บรรทัดแรกไม่ต้องพิมพ์ก็ได้ ยกเว้นว่าคุณจะต้องรันไฟล์ .js เท่านั้นถึงต้องใช้ เมื่อพิมพ์บรรทัดที่สองไปแล้ว มันจะแสดงผล undefined ออกมา ดูแปลกๆ ดีนะครับ

คราวนี้ลองพิมพ์ assert(3 === 2) ดูสิครับ จะเห็น error ทันที

หากคุณทำ assertion (การตรวจสอบภายใน Test) แบบนี้ เท่ากับว่าคุณกำลัง Test ตัว JavaScript อยู่ ซึ่งเป็นการ Test ระบบ Number และ Operator สามตัว อันได้แก่ + และ ===

และก็เช่นกัน บางครั้งคุณก็อาจจะเผลอไปเขียน Test ที่ดูแล้วจะเอียงไปในการ Test ตัว Library เสียมากกว่า อย่างเช่น (code ต่อไปนี้ไม่ต้อง run เพราะใน node ไม่มี library ดังกล่าว เป็นการแสดงเพื่อโชว์ไอเดียเท่านั้น)

_ = require(‘underscore’);
assert(_.first([3, 2]) === 3);

มันเป็นการ Test เพื่อดูว่า library ชื่อ underscore ทำงานถูกต้องตามที่คิดไว้หรือไม่ (จำเป็นหรือไม่ ถามใจเธอดู) การ Test แบบเจาะลึกลงระดับ Implementation แบบที่ยกตัวอย่างมานี้ จะมีประโยชน์ในจังหวะที่เราต้องการศึกษา Library ใหม่ๆ ที่เราไม่คุ้นเคย หรือทดสอบฟังก์ชั่นบางตัวในภาษา JavaScript เท่านั้น แต่ทางที่ดี เราควรจะไว้ใจ Test ของ package เหล่านี้ไปเลยจะดีกว่า เพราะ Library ชั้นดีทั้งหลายต่างก็มี Test เป็นของตัวเองทั้งนั้น

ถึงตรงนี้ มีสิ่งที่ผมอยากจะเน้นอยู่สองข้อ;

ข้อแรก “Sanity Tests” หรือการเขียน Test แบบที่เราเพิ่งเขียนมา มีความจำเป็นสำหรับการทดสอบฟังก์ชั่น หรือ Library บางตัวว่ามันทำงานร่วมกับ environment ของคุณได้หรือไม่ ซึ่งสุดท้ายแล้วเมื่อ environment ของคุณเริ่มเสถียร​และอยู่ตัวแล้ว ก็อาจจะลบ Test เหล่านี้ทิ้งไปได้ทั้งหมด

ข้อสอง หากคุณกำลังเขียน Test เพื่อจุดประสงค์ในการเพิ่มความเชื่อมั่นใน Code (เดี๋ยวจะลงรายละเอียดในเนื้อหาถัดๆ ไป) เมื่อนั้น การ Test พฤติกรรมของ Library จะถูกใช้เพื่อ Demo การทำงานใน Code ของเรา

ทำไมผมถึงไม่สนใจพฤติกรรม Code ที่ยังไม่ได้รับการ Test และไม่ได้ระบุใน Spec.

การเขียน Spec. (ไม่ว่าในรูปแบบไหนก็ตาม) และการ Test สำหรับ Code ของเรา เป็นเสมือนปรอทวัดระดับความใส่ใจที่เรามีต่อพฤติกรรมของ Code และ Code ที่ไร้ซึ่ง Test (อย่างน้อยเป็น excel ที่ระบุขั้นตอนการ Test ก็ยังดี) และคำบรรยายรายละเอียดการทำงานของ Code นั้นๆ ถือเป็นการบอกเป็นนัยๆ ว่า Code ชุดนั้นยังไม่ได้รับการตรวจสอบเลย

สมมติให้ Code ดังต่อไปนี้ยังไม่มี Test ไม่มีเอกสารกำกับ หรือยังไม่ได้รับการระบุว่าจะนำไปใช้กับกระบวนการตรงจุดไหนในธุรกิจ:

function doesThings(args, callback){  doesOtherThings(args);  doesOtherOtherThings(args, callback);  return 5;}

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

แต่สำหรับบริบทของการ Refactoring นั้น จะ (ยัง) ไม่สนใจพฤติกรรมส่วนนี้ เพราะจะไม่มีการ Refactoring พวกมัน

เมื่อใดที่ Code ขาด Test (หรืออย่างน้อยก็มีเอกสารระบุวิธีในการ Test ด้วยมือ) เราจะไม่แก้ Code นั้นๆ ซึ่งแปลว่า เราไม่สามารถ Refactor มันได้ เพราะยังไม่มีอะไรมาเป็นตัวคอยตรวจสอบพฤติกรรมของพวกมันเลย แล้วจะเอาอะไรมารับประกันว่าเมื่อ Refactor แล้วพฤติกรรมจะไม่เปลี่ยน เนื้อหาในส่วนถัดๆ ไปของหนังสือ จะพูดถึงการทำ “Characterization Test” เพื่อใช้จัดการกับ Code ที่ไม่มี Test คุม

สิ่งนี้ (ไม่มี Test กับเอกสาร) ไม่ได้เกิดขึ้นกับเฉพาะกับ Legacy Code เท่านั้น แต่พวก Code ที่เพิ่งเขียนไป แต่ไม่ได้เขียน Test กำกับ เราก็ไม่สามารถ Refactoring ได้ ไม่ว่าจะด้วยวิธีแบบ Automated หรือ Manual ก็ตาม

บทสนทนาเมื่อเราจำต้องปฏิเสธการ Refactoring จนกว่าจะมีการเขียน Test

“ผมขอ Refactor ส่วน login เพื่อให้มันรับ email และ username ได้”

“ทำไม่ได้”

“ผมกำลังจะ Refactor Code ชุดนี้ให้มัน…”

“ไม่ได้”

“ขอ Refactor ก่อนได้มั๊ย แล้วค่อยไปเขียน Test ตามหลัง”

“ไม่”

“Refactor ตัว…”

“ไม่”

“Refa…”

“ไม่”

ทำไมเราจึงไม่แคร์พฤติกรรม Code ในส่วน Performance?

ในช่วง เริ่มต้น ของการ Refactoring เราไม่ต้องไปใส่ใจเรื่องของ Performance ซึ่งก็เหมือนกับตัวอย่างฟังก์ชั่นคูณสองข้างต้น ที่เราสนใจเพียงแค่ว่า input จะต้องสร้าง output ที่เราต้องการได้อย่างถูกต้อง ระหว่างการทำ Refactoring ให้เราวางใจ Tools ต่างๆ ที่เราเลือกใช้ว่า มันทำงานได้ ดีพอ จะได้ไม่ต้องไปกังวลกับเรื่องนี้ให้เสียเวลา ท่องไว้เลยนะครับว่า “เขียนให้มนุษย์ใช้งานก่อน”

คำว่า “ดีพอ” สำหรับการเขียน Code ในช่วงแรกหมายถึง เราสามารถที่จะวัด Output ที่คาดหวังจาก Input ที่ใส่ลงไป (โดยใช้เวลาไม่นานเกินไป) และหาก Code ของเราใช้เวลานานเกินไป เราถึงค่อยเข้าไปปรับในส่วนนั้น นอกเหนือจากกรณีนี้ ให้เน้นไปที่การตรวจสอบ Input และ Output เท่านั้น จนเมื่อ Code ของเรามี Test กำกับแล้ว เมื่อนั้นเราถึงจะมีความมั่นใจเพียงพอที่จะ Refactor Code จนไปถึงขั้นเปลี่ยนรายละเอียด Implementation บางส่วนที่เห็นว่าจำเป็น แต่หากเราดื้อดึงที่จะแก้ Code ทั้งที่ยังไม่มี Test คุมพฤติกรรม (Output สอดคล้องกับ Input) แปลว่า เรากำลังเอา Code ของเราไปขึ้นเขียง

และถึงแม้ว่า การ Test นั้นๆ จะเกี่ยวข้องกับ Requirement ในส่วนของ Non-functional ก็ตาม (ซึ่งมักจะไม่ใช่ส่วนที่จะต้องทำ Refactoring) เราก็สามารถเพิ่มความสำคัญให้กับ Performance (และ “Non-functional” ตัวอื่นๆ เช่น Usability เป็นต้น) โดยทำให้มันกลายเป็นสิ่งที่ชี้วัดได้ว่า “สอบผ่าน” หรือ “สอบตก” แบบเดียวกับที่เราใช้ในการตรวจสอบ “ความถูกต้อง” ของโปรแกรม พูดสั้นก็คือ เราต้องทำให้ Performance เป็นสิ่งที่ Test ได้เสียก่อน

Performance ถือเป็น Non-functional ตัวหนึ่งที่มักจะได้รับสิทธิ์พิเศษมากกว่า Non-functional ตัวอื่นๆ ตรงที่มันเป็นองค์ประกอบที่ง่ายต่อการตรวจสอบถึง “ความถูกต้อง” ผ่านการ Benchmark โดยเราจะเขียน Test ที่ Fail เมื่อ Performance ของฟังก์ชั่นที่กำลังทดสอบทำงานช้าเกินไป และ Pass เมื่อทำงานด้วยความเร็วที่รับได้ วิธีการก็คือ นำ Function เข้าไปเป็น input ของ Test แล้วให้เวลาที่ใช้ในการทำงานเป็น Output

แต่หากเราไม่ได้ทำงานในส่วนที่จำเป็นต้องตรวจสอบ Performance ก็ไม่จำเป็นต้องนำ “พฤติกรรม” ด้าน Performance มาใส่ใจ แล้วหากเมื่อใดที่เรามี Test ครอบคลุมอย่างเพียงพอแล้ว เราจะไปปรับระบบยังไงก็ได้ จนกว่ามันจะวิ่งด้วย Performance ที่ตรงกับมาตรฐานที่เรากำหนดไว้ เพราะเมื่อถึงเวลานั้น จะแก้ยังไง หรือปรับตรงไหน ก็ไม่สามารถรอดพ้นเงื้อมมือของ Test ไปได้

สรุปก็คือ Performance (และ Non-functional ตัวอื่นๆ) ถือเป็นสิ่งที่เรายังไม่ต้องไปสนใจในตอนนี้ จนกว่าเราจะสร้าง Test ให้ครอบคลุม และกำหนดมาตรฐานการชี้วัดพวกมันให้ได้เสียก่อน แล้วค่อยมาว่ากันในภายหลัง

JavaScript กับความยุ่งยากของ Performance

สำหรับ JavaScript บางชนิด (โปรดติดตามเนื้อใน บทที่ 2) การแก้ Code (ไม่ปลอดภัย) หรือการ Refactoring (ปลอดภัย) จะต้องกระทบกับเรื่องของ Performance อย่างหลีกเลี่ยงไม่ได้ การเพิ่ม Frontend Code ไปแค่ไม่กี่บรรทัด หากไม่ได้รับการ Minify จะไปเพิ่มเวลาในการ download และการประมวลผลหน้างานอย่างแน่นอน ซึ่งจริงๆ แล้ว โลกของ JavaScript ก็อุดมไปด้วยพวก Build Tools, Compilers ต่างๆ ที่จะช่วยอำนวยความสะดวกให้กับคุณในเรื่องนี้ได้

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

จะ Refactoring ไปทำไม หากพฤติกรรมของ Code ไม่เปลี่ยน?

จุดสำคัญคือการ ปรับปรุง คุณภาพโดย คงสภาพ พฤติกรรมไว้ ตรงนี้ไม่ได้หมายความว่า การแก้ Bug และการเขียนฟีเจอร์ใหม่ (เขียน Code เพิ่ม) จะไม่ใช่สิ่งสำคัญ เพราะทั้งสองงานต่างก็เป็นสิ่งที่ผูกโยงอยู่กับเป้าหมายทางธุรกิจอย่างใกล้ชิด แถมยังได้รับความสนใจโดยตรงจาก Project/Product Manager มากกว่าเรื่องของคุณภาพของ Code เสียอีก แต่อย่างไรก็ตาม งานทั้งสอง (แก้ bug และเพิ่มฟีเจอร์) ต่างก็เป็นงานเพื่อเปลี่ยนพฤติกรรมของ Code ดังนั้น จึงเป็นกิจกรรมที่ค่อนข้างห่างไกลจากการ Refactoring

ตอนนี้เรามีคำถามสองข้อที่จำเป็นต้องตอบ คำถามแรก ระหว่างการ “ทำให้เสร็จ” กับการเพิ่มคุณภาพ อันไหนสำคัญกว่ากัน? และคำถามที่สอง คุณภาพคืออะไร แล้วการ Refactoring จะช่วยเสริมคุณภาพได้อย่างไร?

หาจุดสมดุลระหว่างคุณภาพ และการทำให้เสร็จ?

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

มีศัพท์อยู่คำหนึ่งที่ในช่วง 20 ปีที่ผ่านมาได้รับการกล่าวขวัญถึงอย่างมาก นั่นคือคำว่า Technical Debt ดูเผินๆ คำนี้คือการพยายามนำศัพท์เทคนิคจากทั้งสองโลกมาฟีเจอร์ริ่งกัน (คำว่า เทคนิค กับคำว่า หนี้) แต่ใช้สื่อกับคนนอกฟิลด์ IT ได้ยาก และยังถูกใช้สำหรับการประเมินว่างานต่างๆ ควรจะเสร็จเร็วแค่ไหน (ตราบเท่าที่ยังไม่ทำให้หนี้เพิ่มขึ้น)

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

คุณภาพคืออะไร แล้ว Refactoring เข้ามาช่วยตรงจุดไหน?

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

  • SOLID: Single responsibility, Open/closed, Liskov substitution, Interface segregation และ Dependency injection
  • KISS: Keep it Simple, Stupid
  • GRASP: General Responsibility Assignment Software Patterns
  • YAGNI: You Ain’t Gonna Need It

นอกจากนี้ยังมีตัวชี้วัดต่างๆ ไม่ว่าจะเป็น Code/Test Coverage, Complexity, Numbers of Argument และขนาดของไฟล์ มี Tool ที่ใช้ในการตรวจสอบทั้ง Syntax Error และ Style Guide มีบางภาษาไปไกลถึงขนาดบีบ Coder จนไม่สามารถเขียน Code บางสไตล์ด้วยซ้ำ

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

  • Extract functions and modules to simplify interfaces
  • Verify code behavior through tests
  • Avoid impure functions when possible
  • Name variables and function well

เราอยากให้คุณกล้าที่จะออกมานิยาม “แนวทางเพื่อสร้างซอฟต์แวร์คุณภาพ” ในแบบของคุณเองขึ้นมาให้ได้

วัดคุณภาพโดยใช้ HUMAN READABILITY

“Human Readability” มักจะถูกหยิบยกขึ้นมาเป็น “กฎแม่” เหนือกฎทั้งมวลในการวัดคุณภาพ ปัญหาคือ มันเป็นตัวชี้วัดที่ใช้งานค่อนข้างยาก มนุษย์มีความหลากหลายทั้งในแง่ประสบการณ์ และความเข้าใจในตัว Concept ต่างๆ Coder มือใหม่ หรือแม้แต่มือเก๋าที่อาจยังใหม่กับชุดความคิดที่เพิ่งได้รับมา ก็มักจะติดปัญหากับการ Abstraction ที่เลือกใช้กันใน Codebase ไม่ว่า Code ชุดนั้นจะถูกเขียนไว้อย่างถูกต้องหรือไม่ก็ตาม

หากผมจะขอบังอาจสรุป ก็คงจะเป็นว่า มีเพียงการ Abstraction ที่เรียบง่ายที่สุดเท่านั้น ถึงจะคู่ควรต่อการเป็นส่วนหนึ่งของ Codebase คุณภาพสูงได้

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

สำหรับบริบทของการ Refactoring นั้น คุณภาพคือเป้าหมาย

ตัวคุณในฐานะ Coder มักจะต้องพบกับปัญหาอันหลากหลายในโลกของซอฟต์แวร์ ซึ่งถึงแม้คุณจะมี Tools อยู่มากมายรอให้คุณนำไปใช้ แต่ไม่มีทางที่ Code ชุดแรกจะเป็น Code ชุดที่ดีที่สุด ดังนั้น การบีบคั้นตัวเองให้เป็นคนที่สามารถสร้าง Solution ที่ดีที่สุดได้ตั้งแต่วันแรก (จนไม่ต้องกลับไปดูมันอีกในภายหลัง) ถือเป็นแนวคิดที่ไม่มีทางเป็นไปได้จริง

การ Refactoring จะช่วยให้คุณกล้าเขียน Code ที่คุณคิดว่าดีที่สุดลงไป จากนั้นก็ Test ซะ (แต่บทที่ 4 จะมาสลับลำดับให้เขียน Test ก่อนแล้วค่อย Code เพราะเราจะมาว่ากันด้วยเรื่อง TDD ในบทนั้น) Test ตัวนั้น จะถูกใช้เพื่อรับประกันว่า เมื่อคุณเข้าไปแก้ไขรายละเอียดของ Code พฤติกรรมโดยรวมของ Code ไม่เปลี่ยนแปลง ซึ่งสิ่งนี้จะมอบอิสระให้กับคุณ จนสามารถแก้ไข Code ให้เป็นไปตาม “คุณภาพ” ตามนิยามของคุณได้ (ซึ่งอาจจะรวมไปถึง Performance และ Non-functional ตัวอื่นๆ) รวมไปถึงการทำ Abstraction ในรูปแบบต่างๆ ที่คุณเห็นว่าเหมาะสม นอกเหนือไปกว่านั้น การ Refactoring จะช่วยลดโอกาสในการทำผิดตั้งแต่ครั้งแรกได้ โดยไม่จำเป็นต้อง Upfront Design ให้มากเกินไป แต่จะเน้นให้ค่อยๆ ปรับจาก Bad Code ไปเป็น Good Code แทน

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

บทที่ 2 จะเน้นไปที่ตัว Background ของภาษา JavaScript ส่วน บทที่ 3 และ 4 จะว่าด้วยภาคทฤษฎี Test ตามด้วย Test ภาคปฏิบัติ โดยทุกอย่างจะเป็นไปเพื่อทำให้คุณสามารถเขียน Code ได้อย่างมั่นใจ และกลับมาแก้ไขได้อย่างรวดเร็ว โดยไม่ได้หลับตาเดินตาม “ลัทธิคลั่ง Test” ที่เอะอะอะไรก็ให้เขียน Test แบบหลับตาเชื่อๆ กันไป บทที่ 5 จะเป็นการสำรวจคำว่า คุณภาพ แบบเจาะลึก โดยใช้แผนภาพที่เรียกว่า Trellus Diagrams ซึ่งคุณสามารถเข้าไปเรียนรู้เพิ่มเติมได้จาก trell.us

เนื้อหาใน บทที่ 6 และ 7 จะเริ่มเข้าสู่เทคนิคในการ Refactoring ตามมาด้วย บทที่ 8 กับเนื้อหาที่ว่าด้วยเทคนิคการ Refactoring สำหรับ Object-Oriented Code กับ Hierarchies รวมไปถึง บทที่ 9 กับเรื่องของ Pattern ต่างๆ จากนั้นเราจะใช้บทที่ 10 เพื่อเป็นกุญแจไขเข้าไปสู่โลกของ Asynchronous Refactoring รวมไปถึงบทที่ 11 ที่ว่าด้วยเรื่องการ Refactoring ผ่านแนวคิด Functional Programming

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

Refactoring ในฐานะของเครื่องมือสำรวจ

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

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

ผมเคยชมการแสดงเดี่ยวเปียโนของ Ben Folds ครั้งหนึ่ง และ Ben ได้ทุ่มเก้าอี้อัดใส่เปียโนในตอนจบการแสดง อยากถามว่า จะมีซักกี่คนที่กล้าทำลายเปียโนศักดิ์สิทธ์ (และแพงระยับ) ได้แบบนี้อีก? คนเหล่านั้นต้องไม่ยอมให้สิ่งใดมาควบคุม คนเหล่านั้นต้องรู้ตัวว่าตัวเองมีความสำคัญกว่าเครื่องมือ

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

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

แล้วอะไรล่ะที่ไม่ใช่การ Refactoring?

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

  • เพิ่มฟังก์ชั่น Square Root เข้าไปในแอพเครื่องคิดเลข
  • สร้าง App/Program ตั้งแต่แรก
  • นำ App/Program ไปเขียนใหม่ใน Framework อื่น
  • เพิ่ม Package ใหม่ๆ เข้าไปใน App/Program
  • ระบุตัว User โดยใช้ทั้ง ชื่อ และนามสกุล แทนที่จะเป็นชื่อเพียงอย่างเดียว
  • Localizing
  • การปรับ Performance
  • แปลง Code ไปเป็นรูปแบบอื่น เช่น แปลงจาก Synchronous ไปเป็น Asynchronous/Callback/Promise
  • ฯลฯ

สำหรับ Code เก่า การเปลี่ยนแปลงใดๆ ที่กระทำกับพฤติกรรม Code จะต้องทำให้ Test พัง หากไม่พัง แปลว่า Test ที่มีไม่ครอบคลุมเพียงพอ แต่การเปลี่ยนแปลง Code ภายในโดยคงพฤติกรรมไว้ ไม่ควรทำให้ Test พัง

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

สรุปส่งท้าย

หวังเป็นอย่างยิ่งว่า เนื้อหาในบทนี้จะช่วยให้คุณเห็นความจริงว่า Refactoring คืออะไร หรืออย่างน้อยก็ทำให้รู้ว่าอะไรที่ไม่ใช่ Refactoring ก็ยังดี

หากแค่การชำเลืองมองไปยัง Source Code ของเพื่อนบ้านข้างเคียงอย่าง Java แล้วบรรลุถึง Code คุณภาพใน JavaScript ได้ เราก็คงไม่ต้องมานั่งทนเจ็บปวดใจกับ Codebase ที่มีแต่ “เวอร์ชั่นใหม่ที่ทำของเก่าพัง” เต็มไปหมดแบบในทุกวันนี้หรอก แนวทางที่คนสายนี้มักจะเลือกใช้ก็คือ การเปลี่ยนจาก Tool A ไปใช้ Framework B เพราะคิดเอาว่ามันจะทำให้ทีมงานสามารถ maintain ได้ดีขึ้น แต่บอกไว้ตรงนี้เลยว่า นี่คือวิธีการที่แพงมาก และโคตรเสี่ยง

Framework ต่างๆ ไม่สามารถช่วยให้เรารอดจากหายนะด้านคุณภาพได้ JQuery ก็ช่วยเราไม่ได้ จะเอา ESNext, Ramda, Sanctuary, Immutable.js, React, Elm หรือจะ Framework ห่าเหวอะไรก็ช่วยไม่ได้ทั้งนั้น ของพวกนี้มันช่วยลดจำนวน Code บางส่วนลง และจัดระเบียบ Code ซึ่งผมไม่ปฏิเสธว่ามันมีประโยชน์โดยตัวของมันเอง เพียงแต่เนื้อหาในหนังสือเล่มนี้เขียนขึ้นเพื่อยกระดับตัวคุณให้หลุดพ้นออกจากวงจรอุบาทว์ของ Code คุณภาพต่ำ ที่ตามมาด้วยการพยายามทุ่มเทเวลาจำนวนมหาศาลไปกับการเขียนขึ้นใหม่โดยใช้ “Framework of the Month” แล้วมาค้นพบในภายหลังว่าตนกำลังเจ็บหนักกว่าเดิม จึงตัดสินใจพุ่งไปหา Framework ใหม่ และใหม่กว่า เวียนไปเรื่อยๆ ไม่รู้จบ

กลับไปที่ สารบัญ

--

--