นักพัฒนาทุกคนต้องเคยมีโฟลเดอร์แบบนั้น โฟลเดอร์ที่ชื่อ utils หรือ helpers ที่คุณคอยก๊อปปี้จาก repo หนึ่งไปอีก repo หนึ่ง คุณวางมันลงไป ใช้เวลาสักยี่สิบนาทีลบการอ้างอิงถึง database schemas เก่าๆ, ดึงเอา auth checks ที่ไม่เกี่ยวข้องออก และเปลี่ยนชื่อตัวแปรเพื่อให้ linter ตัวใหม่เลิกส่งเสียงเตือน ผมเคยทำแบบนี้กับระบบ theming ที่ผมสร้างขึ้นมาเอง นั่นคือ Dynamic Theme Kit มันเริ่มจากการเป็นฟีเจอร์หนึ่งในแอปพลิเคชัน และเป็นเวลาหลายเดือนที่ผมปฏิบัติกับมันเหมือนเป็นเครื่องมือที่พกพาไปไหนมาไหนได้ แต่ผมคิดผิด การก๊อปปี้โค้ดไม่ใช่การนำกลับมาใช้ใหม่ (reuse) แต่มันคือการทำซ้ำ (duplication) ที่มีขั้นตอนยุ่งยากเพิ่มขึ้นมาต่างหาก
กับดักของแนวคิดแบบโปรเจกต์เดียว (Single-Project Mindset)
เมื่อคุณสร้างฟีเจอร์ขึ้นมาภายในโปรเจกต์หนึ่ง คุณจะสร้างสมมติฐานที่มองไม่เห็นนับร้อยอย่างขึ้นมาโดยไม่รู้ตัว ชุดสี (color palette) อาจจะตั้งสมมติฐานถึงการตั้งค่า CSS-in-JS แบบใดแบบหนึ่ง, สเกลระยะห่าง (spacing scale) อาจจะอ้างอิงถึง design token จากคู่มือแบรนด์ของบริษัท, หรือปุ่มสลับโหมดสว่าง/มืด (light/dark mode) อาจจะเรียกใช้ user preference endpoint ที่มีเฉพาะใน backend ของแอปนั้นๆ ความเกี่ยวพัน (dependencies) เหล่านี้ดูเหมือนไม่มีพิษมีภัย เพราะเมื่ออยู่ในโปรเจกต์ มันก็ไม่มีปัญหาอะไร เพราะมันควรจะอยู่ตรงนั้นอยู่แล้ว
ปัญหามันเริ่มขึ้นเมื่อคุณพยายามจะดึงโค้ดนั้นออกมา คุณจะพบว่าคอมโพเนนต์ที่ว่า "นำกลับมาใช้ใหม่ได้" แท้จริงแล้วคือเครือข่ายของสายใยที่ซ่อนอยู่ซึ่งเชื่อมโยงมันเข้ากับ codebase เดิมนั้น ผมเรียนรู้เรื่องนี้ผ่าน DTK มันสร้าง theme variables ได้จริง แต่ในขณะเดียวกันมันก็คาดหวังโครงสร้างโฟลเดอร์ที่เฉพาะเจาะจง มันมีการ import type definition จากที่ไหนสักแห่งที่ลึกเข้าไปในไดเรกทอรี types ของแอปต้นฉบับ มันตั้งสมมติฐานว่าต้องมี global config object ที่มีอยู่แค่ใน repository นั้นเท่านั้น ผมไม่เคยสังเกตเห็นเลย เพราะภายในโปรเจกต์นั้น ทุกอย่างมันมีพร้อมอยู่แล้วเสมอ
การเปลี่ยน DTK ให้กลายเป็น standalone package หมายถึงการผ่าตัด ไม่ใช่การขยายขอบเขต ผมไม่ได้ต้องการฟีเจอร์เพิ่ม แต่ผมต้องการการเชื่อมต่อที่น้อยลง
การสกัด Dynamic Theme Kit ออกมา
งานที่ยากที่สุดคือการนั่งลงหน้า codebase แล้วตั้งคำถามกับทุกฟังก์ชันและทุกการ export ว่า: สิ่งนี้ทำหน้าที่เพื่อ logic ของการทำ theming หรือทำหน้าที่เพื่อโปรเจกต์กันแน่? ผมถอดพวก styling presets ออกไป, ผมลบสมมติฐานที่ว่าผู้ใช้งานจะต้องเป็น React application ออก, ผมลบชุดสีเริ่มต้นออกทั้งหมด โปรเจกต์เดิมมีสไตล์แบบองค์กรโทนสีน้ำเงินเข้มและสีเทา (navy-and-slate) ฝังอยู่ในค่าเริ่มต้น ซึ่งสิ่งนั้นต้องถูกกำจัดออกไป เพราะ package ไม่ควรจะพ่วงเอาสีประจำแบรนด์ของคุณติดไปด้วย
kit ตัวใหม่จะทำหน้าที่เพียงอย่างเดียวเท่านั้น คือรับ configuration object—ซึ่งประกอบด้วยค่าสี, ตัวเลขระยะห่าง, และสเกล typography—แล้วสร้าง CSS custom properties ออกมา แค่นั้นเลย มันไม่ได้เป็นคนนำไปใช้ (apply) มันไม่ได้ตัดสินใจว่าค่าเหล่านั้นจะไปอยู่ที่ไหนใน DOM ของคุณ และมันไม่สนใจว่าคุณจะใช้ Tailwind, Styled Components หรือ HTML ธรรมดา มันแค่ส่งตัวแปรให้แอปพลิเคชันของคุณ แล้วโปรเจกต์ของคุณจะเป็นคนเลือกว่าจะใช้งานพวกมันอย่างไร
ข้อจำกัดนั้นทำให้รู้สึกเหมือนถูกตีกรอบในตอนแรก แต่สุดท้ายมันกลับกลายเป็นการปลดปล่อย
สิ่งที่พังเมื่อคุณพยายามจะนำมันกลับมาใช้ใหม่จริงๆ
ก่อนที่ผมจะเผยแพร่อะไรออกไป ผมต้องการหลักฐานว่า abstraction นี้ใช้งานได้จริง ผมจึงดึงโปรเจกต์ส่วนตัวเล็กๆ สามโปรเจกต์ออกมาจากคลังเก็บของ: เครื่องมือพรีวิว markdown, แอปติดตามนิสัย (habit tracker), และหน้า landing page สำหรับงานอีเวนต์หนึ่ง ซึ่งไม่มีโปรเจกต์ไหนเลยที่ใช้ framework หรือโครงสร้างโฟลเดอร์เหมือนกัน ผมติดตั้ง DTK แบบ local ในแต่ละโปรเจกต์และลองทำ theming ดู
ความพยายามครั้งแรกล้มเหลวทันที ชื่อตัวแปรที่ DTK สร้างขึ้นนั้นเฉพาะเจาะจงเกินไป มันพ่น token อย่าง --primary-action และ --background-overlay ออกมา ซึ่งสื่อถึงเลย์เอาต์ UI แบบใดแบบหนึ่ง ในเครื่องมือพรีวิว markdown ชื่อเหล่านั้นมันไม่มีความหมายเลย เพราะมันไม่มีปุ่ม action และไม่มี overlay ผมจึงเปลี่ยนชื่อ logic การสร้างใหม่ให้เป็นชื่อเชิงโครงสร้างที่เป็นกลาง (neutral) ซึ่งอธิบายถึง "ค่า" ของมัน แทนที่จะอธิบายถึง "วิดเจ็ต"
ผมยังพบอีกว่าค่าเริ่มต้น (default values) ของผมนั้นรุกรานเกินไป เมื่อผู้ใช้ส่ง config ที่ไม่สมบูรณ์มา DTK จะเติมช่องว่างด้วยค่าที่ดูดีใน dashboard ที่มีข้อมูลหนาแน่น แต่กลับทำให้หน้า landing page ที่มีข้อมูลน้อยๆ พังลง ผมจึงเปลี่ยนไปใช้ค่าเริ่มต้นแบบโปร่งใส (transparent defaults) โดยที่ถ้า token ไหนหายไป มันก็จะไม่แสดงผลออกมาเลย เพื่อปล่อยให้โปรเจกต์ที่นำไปใช้เป็นคนกำหนดค่า fallback ของตัวเอง
ต่อมาคือเรื่องเอกสาร (documentation) สิ่งที่ดูเหมือนจะชัดเจนสำหรับผมอย่าง "แค่ส่ง config object เข้าไป" กลับดูคลุมเครือสำหรับคนที่กำลังอ่าน README ตอนเที่ยงคืน ผมจึงเขียนมันใหม่โดยใช้ object จริง, พาธไฟล์จริง และคำอธิบายที่ชัดเจนว่าเกิดอะไรขึ้นเมื่อคุณเรียกใช้ฟังก์ชัน เทียบกับสิ่งที่แอปพลิเคชันของคุณต้องทำหลังจากนั้น
โปรเจกต์ส่วนตัวเล็กๆ เหล่านี้ทำหน้าที่เป็นสนามทดสอบ แม้ความเสี่ยงจะต่ำ แต่มันก็เผยให้เห็นข้อบกพร่องที่แท้จริง ซึ่งผมคงไม่มีทางตรวจเจอหากเอาแต่จ้องมองโค้ดต้นฉบับเพียงลำพัง
บททดสอบที่แท้จริง: การใช้งานจริงที่ Web Weavers World
โปรเจกต์ส่วนตัวเปรียบเสมือนสนามทดลอง (sandboxes) พวกมันไม่มีกำหนดส่ง ไม่มีผู้มีส่วนได้ส่วนเสีย หรือ CSS รุ่นเก่าที่เขียนไว้ก่อนที่คุณจะมีแพ็กเกจของคุณเอง บททดสอบที่แท้จริงเกิดขึ้นเมื่อผมนำ DTK ไปรวมเข้ากับ Web Weavers World ซึ่งเป็นเว็บไซต์ธุรกิจของผม นี่คือเว็บไซต์ที่ใช้งานจริงซึ่งมีสไตล์เดิมอยู่แล้ว มีความคาดหวังของลูกค้า และมีข้อมูลวิเคราะห์ (analytics) ที่ต้องคำนึงถึง หากแพ็กเกจทำให้บางอย่างพัง ผมไม่สามารถแค่ลบ repo ทิ้งแล้วเริ่มใหม่ได้
ผมเพิ่ม DTK เข้าไปใน build pipeline กำหนดค่าสีใหม่ และปล่อยให้มันสร้างชุด CSS variables ชุดใหม่ขึ้นมา การรวมระบบใช้เวลาเพียงช่วงบ่าย ไม่ใช่เป็นสัปดาห์ นั่นคือสัญญาณที่ชัดเจน เมื่อก่อน การเพิ่มธีมใหม่หมายถึงการต้องเขียน CSS ใหม่ ไล่หาค่า hex ที่เขียนตายตัว (hardcoded) ในไฟล์กว่ายี่สิบไฟล์ และภาวนาว่าผมจะไม่พลาดกรณีพิเศษ (edge case) ใดๆ ไป ตอนนี้ผมแค่เพิ่มพาเลทสี (palette) ลงในไฟล์การตั้งค่า DTK จะสร้างตัวแปรขึ้นมา และส่วนที่เหลือของเว็บไซต์ก็จะนำตัวแปรเหล่านั้นไปใช้งาน ตรรกะของธีมเปลี่ยนจากกระบวนการทำด้วยมือที่เปราะบาง กลายเป็นสิ่งที่ผมไว้วางใจมากพอที่จะส่งต่อให้ผู้ร่วมงานได้
3 คำถามที่เปลี่ยนวิธีการสร้างของผม
การผ่านกระบวนการนี้ทำให้ผมต้องสร้างรายการตรวจสอบในใจ (mental checklist) ที่ผมใช้ก่อนจะทำ abstraction กับอะไรก็ตาม:
- ตัวแปรนี้เป็นแบบทั่วไป (generic) จริงๆ หรือไม่? หากชื่อหรือตรรกะมีการอ้างอิงถึงแนวคิดเฉพาะทาง (domain concept) จากโปรเจกต์เดิม ให้เก็บสิ่งนั้นไว้ที่เดิม
- สิ่งนี้ควรอยู่ในแพ็กเกจหรือในแอปพลิเคชัน? กฎทางธุรกิจ, อัตลักษณ์ของแบรนด์ และข้อกำหนดด้านเลย์เอาต์ควรอยู่ในแอป ส่วนระบบพื้นฐาน (plumbing) ที่สร้างผลลัพธ์ที่เป็นมาตรฐานควรอยู่ในแพ็กเกจ
- ผมกำลังแก้ปัญหาที่นำกลับมาใช้ใหม่ได้ หรือปัญหาเฉพาะโปรเจกต์? นี่คือคำถามที่ตอบอย่างซื่อสัตย์ได้ยากที่สุด เรามักชอบคิดว่าโซลูชันของเราเป็นสากล แต่โดยปกติแล้วมันมักจะเป็นแค่เรื่องเฉพาะที่
การตอบคำถามเหล่านี้บังคับให้ผมต้องทำให้การออกแบบเรียบง่ายขึ้น ซึ่งบ่อยครั้งคือการลบโค้ดออกแทนที่จะเพิ่มเข้าไป DTK สอนผมว่าการนำกลับมาใช้ใหม่ไม่ใช่ของขวัญที่คุณมอบให้ตัวเอง แต่มันคือวินัยที่คุณฝึกฝนด้วยการปฏิเสธความสะดวกสบาย
วิธีคิดเรื่องการ Refactoring ที่แตกต่างออกไป
เมื่อก่อนผมวัดผลการ refactor จากความสั้นของโค้ด จำนวนบรรทัดที่น้อยลงทำให้รู้สึกเหมือนมีความคืบหน้า แต่ตอนนี้ผมวัดจากจำนวน "ประตู" ที่มันเปิดออก Dynamic Theme Kit ไม่ได้ดูสง่างามเพราะความกระชับ แต่มันมีประโยชน์เพราะมันผ่านโปรเจกต์ส่วนตัวที่ไม่เกี่ยวข้องกันถึงสามโปรเจกต์ และเว็บไซต์ธุรกิจที่ใช้งานจริงมาได้ โดยไม่ต้องเปลี่ยนโครงสร้างภายในเลย
นั่นคือตัวชี้วัดที่สำคัญ โค้ดที่ทำงานได้เพียงครั้งเดียวคือค่าใช้จ่าย โค้ดที่ทำงานได้ซ้ำๆ คือสินทรัพย์ ก่อนจะเริ่มทำฟีเจอร์ใดๆ ในตอนนี้ ผมจะหยุด และถามตัวเองว่าผมกำลังสร้างสิ่งที่ต้องใช้ซ้ำอีกหรือไม่ ถ้าคำตอบคือใช่ ผมจะสร้างมันให้ต่างออกไปตั้งแต่บรรทัดแรก ผมแยกส่วน input ออกมา ผมกำหนด output ให้ชัดเจน และผมตัดข้อสันนิษฐานต่างๆ ออกไป
การ refactor ที่ดีที่สุดไม่ได้ทำให้โค้ดของคุณสั้นลง แต่มันทำให้โค้ดของคุณทำงานได้ในที่ที่คุณยังจินตนาการไปไม่ถึง
