ลาก่อน คลีนโค้ด (2020)
(overreacted.io)- แสดงให้เห็นว่าการ ยึดติดกับคลีนโค้ด อาจทำร้ายความสามารถในการแก้ไขเปลี่ยนแปลงจริงได้ ผ่านประสบการณ์ที่เขียนโค้ดปรับขนาดในกราฟิกเอดิเตอร์ใหม่โดยเน้นการกำจัดโค้ดซ้ำ แล้วสุดท้ายต้องย้อนกลับ
- เดิมที implementation มีสูตรคำนวณตาม handle ของ
Rectangle,Oval,Header,TextBlockที่ซ้ำกันอยู่ และ implementation ใหม่พยายาม กำจัดความซ้ำซ้อน ด้วยวิธีแยกทิศทางกับรูปร่างออกจากกันแล้วนำมาประกอบ - ทันทีหลังรีแฟกเตอร์ ขนาดโค้ดลดลงครึ่งหนึ่งและจุดที่ต้องแก้ไขก็ดูเหมือนจะถูกรวมไว้ที่เดียว แต่เมื่อภายหลังต้องมี พฤติกรรมพิเศษ ตามรูปร่างหรือตาม handle การทำ abstraction กลับอาจกลายเป็นตัวถ่วง
- ปัญหาที่ใหญ่กว่าการตัดสินใจทางเทคนิคคือการเช็กอินเข้า master โดยตรงโดยไม่คุยกับเพื่อนร่วมงานที่เขียนโค้ดเดิม ซึ่งอาจทำลาย การสร้างความไว้วางใจ ในทีม
- “ความสะอาด” ไม่ใช่เป้าหมาย แต่เป็นเพียงเครื่องมือสำหรับรับมือกับระบบที่ซับซ้อน สิ่งสำคัญกว่าโค้ดหน้าตาเป็นอย่างไร คือทีมจะ รับมือกับการเปลี่ยนแปลง ร่วมกันได้หรือไม่
โค้ดปรับขนาดในกราฟิกเอดิเตอร์ที่ซ้ำกัน
- บนแคนวาสของกราฟิกเอดิเตอร์มีโค้ดสำหรับปรับขนาดรูปร่างอย่างสี่เหลี่ยมและวงรีด้วย handle เล็ก ๆ ที่ขอบ
- ตัว implementation ทำงานได้ แต่มีสูตรคำนวณตำแหน่งและขนาดที่ซ้ำกันตามรูปร่างและทิศทางของ handle
Rectangleมีเมธอดอย่างresizeTopLeft,resizeTopRight,resizeBottomLeft,resizeBottomRightOvalมีเมธอดอย่างresizeLeft,resizeRight,resizeTop,resizeBottomHeaderและTextBlockก็มีเมธอดปรับขนาดของตัวเองเช่นกัน
- ถ้าผู้ใช้กด Shift ก็ต้องรองรับการ คงสัดส่วนเดิม ระหว่างปรับขนาดด้วย ทำให้มีสูตรเพิ่มขึ้นอีก
abstraction เพื่อกำจัดโค้ดซ้ำ
- ความซ้ำดูเหมือนมีอยู่ตามสองแกน
- ซ้ำกันในกลุ่ม handle ที่มีทิศทางเดียวกัน:
Oval.resizeLeft()และHeader.resizeLeft()ต่างก็เป็นการลาก handle ด้านซ้ายเหมือนกัน - ซ้ำกันในกลุ่มเมธอดของรูปร่างเดียวกัน: หลายเมธอดของ
Ovalต่างก็จัดการกับวงรีเหมือนกัน
- ซ้ำกันในกลุ่ม handle ที่มีทิศทางเดียวกัน:
- โครงสร้างใหม่พยายามแยกโค้ดเป็น
DirectionsและShapesเพื่อประกอบ พฤติกรรมตามทิศทาง กับ พฤติกรรมตามรูปร่าง เข้าด้วยกัน- ใน
Directionsมีtop,left,bottom,right - ใน
ShapesมีOval,Rectangle - ใช้
createHandle,createBoxเพื่อประกอบ handle กับรูปร่าง
- ใน
- แนวทางนี้สร้างชุด handle อย่าง
fourCorners,fourSides,twoSidesแล้วใช้สิ่งเหล่านี้สร้างRectangle,Oval,Header,TextBlock
ภาพลวงตาทันทีหลังรีแฟกเตอร์
- หลังรีแฟกเตอร์ ขนาดโค้ดโดยรวมลดลงครึ่งหนึ่ง และ ความซ้ำก็ดูเหมือนจะหายไป
- มองว่าถ้าจะเปลี่ยนพฤติกรรมของทิศทางหรือรูปร่างใด ก็แก้เพียงจุดเดียวแทนที่จะต้องแก้หลายเมธอด
- เขาเช็กอินการเปลี่ยนแปลงเข้า master ตอนดึก และคิดว่าตัวเองเพิ่งจัดระเบียบโค้ดรก ๆ ของเพื่อนร่วมงานให้เรียบร้อยสวยงาม
ปัญหาที่เผยออกมาในวันถัดมา
- วันรุ่งขึ้นหัวหน้าขอให้ย้อนการเปลี่ยนแปลงกลับ ซึ่งในตอนนั้นยอมรับได้ยาก เพราะยังเชื่อว่าโค้ดใหม่สะอาดกว่าโค้ดเดิม
- เมื่อเวลาผ่านไปก็เห็นว่าการตัดสินใจนั้นถูกต้องด้วยเหตุผลสองข้อ
- เขาเขียนโค้ดใหม่และเช็กอินเข้าไปโดยไม่ได้คุยกับคนที่เขียนโค้ดเดิมก่อน
- เขาได้การลดโค้ดซ้ำมา แต่ต้องแลกด้วย ความง่ายต่อการรองรับการเปลี่ยน requirement
- ทีมวิศวกรรมที่ดีต้องค่อย ๆ สร้างความไว้วางใจกันอย่างต่อเนื่อง และการเขียนโค้ดของเพื่อนร่วมงานใหม่ครั้งใหญ่สามารถสร้างผลกระทบหนักต่อการทำงานร่วมกันได้
ต้นทุนของ abstraction ที่ผิดพลาด
- หลังจากนั้นมีความจำเป็นต้องรองรับ กรณีพิเศษและพฤติกรรมพิเศษ จำนวนมากสำหรับหลาย handle ของหลายรูปร่าง
- ถ้าจะรองรับ requirement เหล่านี้ในโครงสร้าง abstraction ใหม่ โค้ดอาจซับซ้อนขึ้นหลายเท่า
- ตรงกันข้าม โครงสร้างแบบ “รก ๆ” เดิมกลับทำให้แต่ละเมธอดแยกขาดจากกัน จึงค่อนข้างง่ายต่อการใส่การเปลี่ยนแปลงลักษณะนี้
- การกำจัดโค้ดซ้ำไม่ใช่ของฟรี คุณอาจได้ความสามารถบางอย่างมา แต่ก็สูญเสียความสามารถอีกอย่างไป
ทบทวนความหมายของ “ความสะอาด”
- ช่วงเวลาที่หมกมุ่นกับ “คลีนโค้ด” และการกำจัดโค้ดซ้ำเป็นสิ่งที่นักพัฒนาหลายคนอาจเคยผ่าน
- เมื่อยังไม่มั่นใจในโค้ดที่ตัวเองเขียน ก็อาจยึดความภูมิใจกับสิ่งที่วัดได้ เช่นกฎ lint ที่เข้มงวด กฎการตั้งชื่อ โครงสร้างไฟล์ หรือการไม่มีโค้ดซ้ำ
- เมื่อเริ่มทำ abstraction เป็น ก็อาจเชื่อว่า abstraction คือคุณธรรม ทุกครั้งที่เห็นโค้ดซ้ำ และอยากนำมันไปใช้
- ควรคิดให้ลึกว่าความรู้สึกว่า “สะอาด” หรือ “สกปรก” นั้นเชื่อมโยงกับผลลัพธ์ทางวิศวกรรมอะไรจริง ๆ
- สิ่งสำคัญไม่ใช่แค่โค้ดดูเป็นอย่างไร แต่รวมถึงโค้ดจะ เปลี่ยนแปลงและถูกแก้ไขอย่างไร ร่วมกับคนในทีมด้วย
เมื่อถึงเวลาต้องปล่อยวางคลีนโค้ด
- คลีนโค้ดไม่ใช่เป้าหมาย แต่เป็นความพยายามในการทำความเข้าใจระบบที่ซับซ้อน
- ในเวลาที่คุณยังไม่แน่ใจว่าการเปลี่ยนแปลงใน codebase จะส่งผลอย่างไร มันอาจเป็น กลไกป้องกันตัว ที่ช่วยชี้ทาง
- ในช่วงแรก ประสบการณ์การทำให้โค้ดซับซ้อนง่ายขึ้นด้วยการแยกฟังก์ชันหรือรีแฟกเตอร์คลาสอาจเป็นเรื่องสนุก
- แต่ก็ไม่ควรหยุดอยู่ตรงนั้นจนกลายเป็น “สาวกคลีนโค้ด”
- ให้คลีนโค้ดเป็นผู้นำทางได้ แต่เมื่อถึงจุดหนึ่งก็ต้องรู้จักปล่อยวางมัน
1 ความคิดเห็น
ความเห็นจาก Hacker News
โค้ดซ้ำ บางครั้งก็ไม่เป็นไร แต่ก็ไม่ได้เป็นหลักฐานว่า Clean Code แย่
ดูเหมือนจะรีแฟกเตอร์เกินไปหน่อย และถ้าแค่แยก “คณิตศาสตร์ซ้ำ ๆ 10 บรรทัด” ออกเป็นฟังก์ชัน ก็น่าจะสะอาดกว่านี้
เพื่อนร่วมทีมที่ไม่แยก 10 บรรทัดนั้นเป็นฟังก์ชันก็ไม่ได้ทำถูกนัก ดังนั้นถ้าเป็นฉันคงปฏิเสธ PR แต่คงไม่ถึงกับเขียนใหม่เอง
การเขียนทับ PR ของคนอื่นใหม่หมดเป็นการดูหมิ่นอย่างรวดเร็วที่ปิดโอกาสการถกเถียง และบ่อยครั้งเราก็ไม่รู้บริบทที่ทำให้โค้ดถูกเขียนมาแบบนั้น
ถ้าไม่มีมาตรฐานการเขียนโค้ด ก็ควรรีวิวแล้วปฏิเสธ PR ไม่ใช่ปล่อยให้คอมมิตด้วยทำนองว่า “Clean Code ไม่สำคัญ” เพราะไม่กี่เดือนโค้ดเบสก็จะกลายเป็นสิ่งที่ไม่มีใครอยากทำงานด้วย
สิ่งที่ควรเรียนรู้จากเรื่องนี้ไม่ใช่ Clean Code แต่คือ การสื่อสารและความใส่ใจ
โอกาสที่ผู้เขียนเดิมจะได้เรียนรู้ว่าโค้ดคุณภาพต่ำทำให้เสียเวลาอย่างไรในโลกจริงก็หายไปด้วย
ต่อให้แนวทาง Clean Code จะดีกว่า ประเด็นสำคัญตรงนี้ก็ไม่ใช่ตัวโค้ด แต่เป็นการที่เขาเสียเวลาของตัวเองโดยไม่ได้ให้ฟีดแบ็กที่มีประโยชน์
ทางที่ดีที่สุดคือให้ผู้เขียนเดิมยกระดับมาตรฐานของตัวเอง ส่วนเจ้าตัวก็ไปทำงานอื่นที่ควรแก้จริง ๆ ซึ่งก็คือคำตอบที่ถูกต้องคือ code review
คนแบบนี้มักสร้างปัญหาในอนาคตจากการแก้สิ่งที่ไม่ใช่ปัญหา แทนที่จะไปแก้ปัญหาจริง
สำหรับการทำครั้งแรก มักจะถูกต้องกว่าถ้าไม่รีบทำ abstraction กับโค้ดเล็ก ๆ แค่ 10 บรรทัดแบบนั้น โค้ดที่ทำหน้าที่นั้นตรง ๆ มักแก้ทีหลังได้ง่าย และการทำ abstraction กับ 10 บรรทัดเพื่อประโยชน์ที่ยังไม่พิสูจน์ก็คุ้มค่าน้อย
Clean Code ไม่ได้สำคัญเลย และกฎหรือ “หลักการ” ที่คล้ายกันส่วนใหญ่ก็ไม่สำคัญเช่นกัน ตรงกันข้าม มันมักเป็นสิ่งที่คนใช้ยึดเพื่ออธิบายโครงสร้างแบบรังผึ้งที่กองทับกันในโค้ดเบสเมื่อเวลาผ่านไป
สุดท้ายมักได้โค้ดที่มี abstraction มากเกินไป เปลี่ยนยาก ช้า และเข้าใจยาก
ถ้าจะยึดหลักอะไร ก็ควรดูแค่ว่าโปรแกรมแสดง การแปลงข้อมูล ซึ่งเป็นสิ่งที่มันทำจริง ๆ ได้อย่างชัดเจนและแก้เปลี่ยนได้ง่ายหรือไม่
หลักการ SOLID ส่วนใหญ่ไม่ได้ช่วยให้เห็นว่าการแปลงข้อมูลคืออะไรและเกิดขึ้นอย่างไร หรือบางครั้งถึงขั้นขัดขวาง จึงมักไม่ค่อยมีประโยชน์ต่อการเข้าใจโปรแกรมจริง
ถ้าดูการรีแฟกเตอร์ที่เสนอ 10 บรรทัดนั้นจริง ๆ ไม่ได้เหมือนกัน แต่แค่คล้ายกัน
สูตรสำหรับปรับขนาดมุมซ้ายบนของสี่เหลี่ยม กับสูตรสำหรับเปลี่ยนมุมขวาล่างของวงรีนั้นต่างกัน แม้ภายนอกจะดูคล้าย แต่บางจุดเป็น
+แทน-ในกรณีนี้ ความซับซ้อนอยู่ที่ เรขาคณิตนั้นเอง และถึงจะ “ทำ abstraction” ก็แค่ย้ายความรกไปไว้อีกที่ ไม่ได้หายไปจริง
อาจมี abstraction ทางคณิตศาสตร์ที่ฉลาดกว่านั้น เช่นระบบจำนวนแบบพิเศษ แต่ถ้าไม่ได้มีในภาษาอยู่แล้ว ต้นทุนของการอิมพลีเมนต์โอเปอเรชันพิเศษบนจำนวนเต็มธรรมดา ก็มักสูงกว่าประโยชน์ที่จะได้
“Clean Code” ต้องการ การรีแบรนด์
ตลอดอาชีพกว่า 25 ปี ฉันเห็นแรงต้านต่อ design pattern, abstraction, การกำจัดความซ้ำซ้อน อยู่ไม่รู้จบ
เหตุผลที่ได้ยินตลอดคือ abstraction ทำให้โค้ดซับซ้อนขึ้น, ไม่มีเวลาเขียน Clean Code, ทุกอย่างเป็นแค่รสนิยมและความเห็น
แต่เป้าหมายของ Clean Code คือทำให้โค้ด เรียบง่ายขึ้นและดูแลรักษาง่ายขึ้น เมื่อ requirements เปลี่ยน
คุณค่าของซอฟต์แวร์อยู่ที่มันเปลี่ยนได้เมื่อเวลาผ่านไป ถ้าไม่ใช่แบบนั้นก็ใช้วงจรคงที่ซึ่งทั้งทำและดูแลรักษาง่ายกว่ามากและถูกกว่าด้วย
ถ้าการรีแฟกเตอร์ทำเป้าหมายนี้ไม่สำเร็จ และหัวหน้าสามารถอธิบายได้อย่างน่าเชื่อว่ามันทำให้รับมือกับการเปลี่ยน requirements ในอนาคตได้ยากขึ้น โค้ดนั้นก็ไม่ “สะอาด” ตั้งแต่แรก
การไม่คุยกับผู้เขียนเดิมเป็นปัญหาเรื่อง กระบวนการและมารยาท ที่อยู่นอกขอบเขตของ Clean Code การที่ใครสักคนทำตัวหยาบคายและตัดสินใจฝ่ายเดียว ไม่ได้ทำให้คุณค่าของ Clean Code หายไป
หนังสือ Clean Code ชื่อดังเสนอว่า “ไม่มีความซ้ำซ้อน” เป็นเงื่อนไขของระบบที่ออกแบบมาดี และบอกว่าความซ้ำซ้อนคือศัตรูตัวฉกาจของงานเพิ่ม ความเสี่ยงเพิ่ม และความซับซ้อนที่ไม่จำเป็น
ตามนิยามนี้ การกำจัดความซ้ำซ้อนก็กลายเป็นคำพ้องกับการทำให้เรียบง่าย ซึ่งไม่ถูกต้อง
การกำจัดความซ้ำซ้อนคือ การเพิ่ม dependency ถ้า dependency นั้นจำลองปัญหาได้ดี มันก็อาจเป็น abstraction ที่ดีและเป็นการทำให้เรียบง่ายได้ แต่ถ้าไม่ใช่ มันก็เป็นแค่การบีบอัดที่สวมหน้ากาก abstraction
อ้างอิงนี้มาจากการกล่าวถึง Simple Design ของ Kent Beck ใน Clean Code
อย่างหนึ่งคือความรู้สึกเชิงอัตวิสัยว่าโค้ดไหนดี และอีกอย่างคือการทำตามหนังสือ Clean Code และแนวทางที่เกี่ยวข้อง
คนจำนวนมากขึ้นเรื่อย ๆ รู้สึกว่าสองอย่างนี้ต่างกันมากพอสมควร บางครั้งการทำตามแนวทางในหนังสือบางข้อกลับทำให้ได้โค้ดที่ไม่ได้สะอาดเลย
ในโลกอุดมคติทุกคนคงอยากเขียนโค้ดที่สะอาด แต่คนที่รู้สึกว่า Clean Code เป็นวิธีที่ดีในการไปถึงเป้าหมายนั้นกำลังลดลง
โดยเฉพาะ abstraction ที่มากเกินไป และการกำจัดความซ้ำซ้อน กำลังถูกหลายคนมองว่าเป็นต้นตอของโค้ดที่รก
บางครั้งหนี้ทางเทคนิคถูกเข้ารหัสอยู่ใน requirements ของผลิตภัณฑ์เอง จนรีแฟกเตอร์หรือ best practice ก็แก้ไม่ได้ ทำได้แค่บรรเทาความเจ็บปวด
เป้าหมายของ Clean Code กับผลลัพธ์จากการปฏิบัติจริงต่างกันมาก เพราะคนส่วนใหญ่มักคาดเดาสภาพของโค้ดในอนาคตได้ไม่ดี แม้จะมี requirements อยู่ตรงหน้าก็ตาม
Clean Code มักชวนให้ถามคำถามชวนสยองแบบ “ถ้าเกิดว่า...?” และสิ่งนี้ก็มักกลายเป็นกล่องแพนดอราที่ทำให้การปล่อยของล่าช้าไม่รู้จบ
เกือบทุกครั้ง การเขียนตามสิ่งที่รู้อยู่แล้ว และให้คนที่มีประสบการณ์ในโดเมนเป็นคนนำสถาปัตยกรรม จะดีกว่า
abstraction ที่ดีจำนวนมากมีอยู่ในไลบรารีแล้ว แน่นอนว่าน่าเสียดายที่มันไม่ได้เป็นแบบนั้นเสมอไป
abstraction ที่แย่ จะคอยดูดเวลา และเพราะโค้ดก็เป็นเรื่องของการสื่อสารกับคนอื่นในทีมด้วย วิธีที่เพื่อนร่วมทีมรับสารจึงสำคัญกว่าวิธีคิดของฉันมากนัก
เพื่อนร่วมงานเขียนโค้ดจำนวนมากด้วยการคัดลอก-วาง และคุณก็รีแฟกเตอร์หลังจากคอมมิตแล้ว
เพื่อนร่วมงานไปบ่นกับหัวหน้า หัวหน้าก็เรียกคุณไปตำหนิ ซึ่งเท่ากับได้ข้อสรุปว่า คราวหน้าควรปล่อยความรกไว้ใน codebase
มีบทเรียนที่ควรเรียนรู้จากเรื่องนี้ก็จริง แต่ไม่ได้แปลว่าการคัดลอก-วางดีกว่าการเขียนฟังก์ชัน
การรีแฟกเตอร์ควรถูกส่งเป็นการเปลี่ยนแปลงแยกต่างหากพร้อมแท็กผู้เขียนเดิมเพื่อขอรีวิว
แต่ที่เพื่อนร่วมงานไม่คุยกันตรงๆ แล้วไปแจ้งหัวหน้า และหัวหน้าก็สั่งให้ย้อนกลับ นี่เป็นสัญญาณที่ค่อนข้างแย่มาก
คำตอบที่เป็นไปได้คือ “ไม่ครับ/ค่ะ เพราะว่า...” หรือถ้าโชคดีก็อาจเป็น “จริงด้วย พรุ่งนี้ผม/ฉันทำให้”
ทุกคนหลีกเลี่ยงปัญหาได้โดยไม่ต้องเป็นเรื่องใหญ่ และบางคนอาจได้เรียนรู้อะไรด้วย
แต่ผู้เขียนก็เพิ่มบริบทสำคัญไว้ เขาแลกการลดความซ้ำกับความเป็นไปได้ที่ requirement จะเปลี่ยน และต่อมาก็พบว่าจำเป็นต้องมีกรณีพิเศษและพฤติกรรมจำนวนมากสำหรับ handle ของแต่ละ shape
นี่แสดงให้เห็นความต่างระหว่าง DRY แบบบังเอิญ กับ DRY แบบแก่นแท้
บางครั้งไลบรารี A กับ B ดูสอดคล้องกันจนทำให้โค้ดออกมาเหมือนกัน แต่ในความจริงอาจเป็นแค่เรื่องบังเอิญ
ถึงแม้ wrapper ที่หุ้ม lib A กับ wrapper ที่หุ้ม lib B จะมีโค้ดเหมือนกัน แต่ถ้าจริงๆ แล้วมันหุ้มคนละสิ่งโดยสิ้นเชิง การทำ abstraction จะให้ผลลัพธ์ที่แย่ลง
“15 รายการ” ก็เหมือนกัน ถ้าหมายถึง “ทุกลิสต์ต้องแสดง 15 รายการเสมอ” การทำ abstraction กับเลข 15 ก็ถูกต้อง แต่ถ้าเป็น “15 เพลงอันดับต้นบนหน้าแรก” กับ “คอมเมนต์เริ่มต้น 15 รายการใต้เพลง” อย่างละกรณีกัน การจับเลข 15 สองจุดนี้มารวมกันกลับเป็นทางเลือกที่แย่กว่า
สัญญาณอันตรายอันดับสองคือแม้แต่ตัวการรีแฟกเตอร์เองก็ไม่ใช่การรีแฟกเตอร์ที่ดี มันยังขาดความเป็นโมดูลาร์ ยังผูกกันแน่น และแค่ลดความซ้ำลงได้นิดหน่อยเท่านั้น
มันอาจฟังเหมือน “โค้ดที่คุณเขียนมันไม่ค่อยดี ฉันเลยเขียนใหม่ให้ มีปัญหาอะไรไหม?”
ทางออกที่ดีที่สุดน่าจะเป็นการขอคำอธิบายในลักษณะ “รีวิวช้า” พร้อมเสนอแนวทางแก้ และหวังว่าผู้เขียนเดิมจะไม่ปล่อยผ่านไปเฉยๆ
แก่นสำคัญคือโค้ดจะเปลี่ยนในอนาคต ทั้ง requirement ก็เปลี่ยน และโค้ดที่เกี่ยวข้องก็เปลี่ยน
ถ้าคุณสร้าง abstraction ที่จำกัดความสามารถในการเปลี่ยนแปลงนั้น คุณก็จะทำลายความสามารถของตัวเองในอนาคตในการปรับ codebase ให้เข้ากับสถานการณ์
โดยเฉพาะ abstraction ที่ลงทุนลงแรงไปมาก มักทำให้เราเกิดความยึดติดทางอารมณ์ และเสียเวลาเพิ่มไปอีกกับการพยายามยัดการเปลี่ยนแปลงให้เข้ากับวิธีแก้ที่ดูสวยงามนั้น
KISS นั่นแหละถูกต้อง
กรณีนี้ดูเหมือนเป็นปัญหาแบบ “ไม่มีใครชอบคนที่ทำตัวอวดเก่ง”
โดยเฉพาะเวลานักพัฒนารุ่นน้องยังไม่ได้เรียนรู้ว่างานไม่ได้อยู่แค่ระหว่างตัวเองกับคอมพิวเตอร์ แต่ยังอยู่ระหว่างตัวเอง คอมพิวเตอร์ และคนอื่นๆ ด้วย
เวอร์ชันที่สะอาดกว่านี้มีโอกาสจะถูกยอมรับมาก ถ้าได้คุยกับผู้เขียนเดิมจริงๆ และตกลงกันได้ถึงวิธีที่ดีกว่า
ชื่อบทความดูจะเหมาะเป็น ฉันได้เรียนรู้วิธีทำงานเป็นทีม มากกว่า “ลาก่อน คลีนโค้ด”
โค้ดที่ซับซ้อนกว่าถูกมองว่าเป็นภัยคุกคาม และอาจถูกมองว่าเป็นคู่แข่งที่ต้องกดไว้ผ่านสายการบังคับบัญชา
ถ้าเป็นนักพัฒนารุ่นน้อง ก็ควรรู้ไว้ว่านี่ขึ้นอยู่กับบริบท ในที่ทำงานจำนวนมาก ความเรียบง่ายสำคัญกว่าความยืดหยุ่น มีการหลีกเลี่ยงฟีเจอร์ภาษาที่ชวนสับสน และนิยมวิธีทำงานที่นั่งอยู่หน้าแป้นพิมพ์นานๆ รวมถึงภาษาที่เอื้อแบบนั้น
ในทางกลับกัน ปรัชญาที่ให้ความสำคัญกับการเรียนรู้ตลอดชีวิต ความเป็นช่างฝีมือด้านซอฟต์แวร์ และการทบทวนกระบวนการ กลับขัดแย้งกับสภาพแวดล้อมแบบนี้โดยสิ้นเชิง
ความพยายามที่จะดีขึ้นมักไม่เป็นที่นิยมในหมู่คนที่ไม่เติบโต
ทั้งสองแบบก็เป็นทางเลือกชีวิตที่ใช้ได้ทั้งคู่ ซอฟต์แวร์อาจเป็นที่ที่ทำงาน 7 ชั่วโมงแล้วกลับบ้านอย่างสบายใจ หรืออาจเป็นโลกแห่งการสำรวจที่มีเพียงจินตนาการและสติปัญญาเป็นขีดจำกัดก็ได้
ถ้าคุณอยากค้นพบสิ่งใหม่ๆ ก็ควรระวังการใช้เวลาหลายสิบปีกับคนที่กลัวสิ่งที่ไม่รู้จัก
พูดให้สั้นกว่านั้นคือ ถ้า แรงจูงใจ ของคุณกับเพื่อนร่วมงานไม่ตรงกัน ก็ควรพิจารณาจากไป
บทความนี้มีเหตุผลที่คนรักกันมานาน และมันช่วยเปิดตาให้นักพัฒนาหลายคนที่ตกหลุมพรางแบบเดียวกัน
ต่อให้ทำงานคนเดียว คุณก็ยังอาจออกแบบ abstraction ที่ดูสะอาดมากเกินไป แล้วในอนาคตก็ถูก abstraction นั้นเล่นงานได้
ปัญหาคือการเปลี่ยนแปลงถูกคอมมิตเข้า main branch โดยตรงโดยไม่มีรีวิว
การเปลี่ยนแปลงโค้ดทุกครั้งควรมี peer review ไม่ว่าจะผ่าน pair programming หรือ PR แบบดั้งเดิม เพื่อป้องกันไม่ให้เรื่องบานปลายแบบนี้
หัวหน้าเป็นหัวหน้าที่แย่ เขาควรสังเกตว่าโปรเซสที่ดีต่างหากที่หายไป ไม่ใช่มาคอยดุคน
ถ้าเป็นทีมที่มีวัฒนธรรม code review แม้เด็กใหม่ไฟแรงจะลองทำแบบนี้ เพื่อนร่วมงานก็น่าจะพูดว่า “ขอบคุณนะ แต่แบบนี้ยังไม่ใช่” หรือ “ไอเดียดีนะ ลองทำแบบนี้ดีกว่า”
อีกเรื่องหนึ่งคือฉันคิดว่าไม่ควรรีแฟกเตอร์โค้ดโดยไม่มีเหตุผลที่ดี โค้ดที่แย่ก็อาจเป็นโค้ดที่แทบไม่ต้องแก้อีกเลยก็ได้
เวลาที่ดีที่สุดในการรีแฟกเตอร์คือเมื่อคุณตระหนักว่าจำเป็นต้องเปลี่ยนพฤติกรรม แต่โครงสร้างเดิมทำให้มันยากเกินไป
น่าเสียดายที่ในกรณีนี้ดูเหมือนจะได้บทเรียนที่ผิด
ไม่เข้าใจว่าทำไมทุกคนถึงพูดแค่ว่าเขาควรทำอะไรแทน นี่เป็นบทเรียนที่ชอบธรรมอย่างเพียงพอแล้ว
ในวงการการเงิน เรารับมือกับผลิตภัณฑ์ที่คล้ายกันในระดับหนึ่งแต่ไม่เหมือนกันเสียทีเดียวอยู่ตลอด ตัวเลือกหุ้นกับตัวเลือกอัตราแลกเปลี่ยนต่างก็เป็น option ต่างก็มีราคาใช้สิทธิ และตั้งราคาด้วย Black-Scholes บางเวอร์ชัน แต่ในความเป็นจริงแล้วต่างกันมาก
คนที่เพิ่งเข้ามาใหม่มักถูกล่อลวงอย่างมากให้อยากทำให้พฤติกรรมหลายอย่างใช้ร่วมกันได้ และเราก็มักลงเอยอยู่ในจุดยืนของหัวหน้าตามโพสต์ต้นฉบับ ที่ต้องบอกนักพัฒนารุ่นหนุ่มสาวให้ใจเย็น ๆ และแยกมันออกจากกันไว้
ไม่ได้หมายความว่าควรบอกลา Clean Code แต่หมายความว่าต้องหลีกเลี่ยง การทำ abstraction มากเกินไป จึงจะรักษาโค้ดให้สะอาดได้จริง
สิ่งที่ handle ทั่วไปทำคือ affine transform ได้แก่ การเลื่อน การหมุน การย่อ-ขยาย และการสะท้อน และถ้าเป็นการย่อ-ขยายแบบสม่ำเสมอก็เป็น similarity transform ซึ่งรับประกันได้ว่าใช้ได้กับรูปทรงใด ๆ
ปัญหาที่แท้จริงคือไม่ควรทำ abstraction กับสิ่งที่เรายังรู้ไม่มากพอ โดยเฉพาะถ้าไม่รู้ว่าความเป็นไปได้ในอนาคตจะมีอะไรบ้าง
ตัวอย่างเช่น ถ้าคิดว่าวงรีมีแค่ circle กับ ellipse ก็จะตกใจไม่น้อยเมื่อ “oval” ตัวแรกของจริงปรากฏขึ้น
ผู้เขียนไม่ได้แยกความต่างระหว่างโค้ดสกปรกกับโค้ดที่ “ไม่ clean” อย่างชัดเจน คนอื่นเลยพยายามแยกให้
DRY มีคุณค่า แต่ abstraction ก็มีต้นทุน มันเป็นเรื่องของ trade-off เสมอ
เหตุผลหนึ่งที่ชอบภาษาอย่าง Haskell คือแนวคิดสุดโต่งเรื่อง abstraction ไปอยู่ที่ ระดับภาษา ไม่ใช่ระดับการเขียนโค้ด
ทุกคนเห็นตรงกันว่า Applicative ควรทำอะไร ความท้าทายจึงเลื่อนไปทางขวาของ pipeline จากไอเดียสู่โค้ดมากขึ้น คือไปอยู่ที่การหา Applicative ที่เหมาะกับปัญหา
แพตเทิร์นการออกแบบเชิงวัตถุส่วนใหญ่มีคู่เทียบที่เป็นธรรมชาติและนิยามชัดเจนในสภาพแวดล้อมแบบนี้ และช่วยลด abstraction เฉพาะโปรเจ็กต์ได้
สำหรับมือใหม่ ต้นทุนเริ่มต้นในการเรียนภาษาสูงมาก และก็มักมีเหตุการณ์ที่เปิดโค้ด Haskell ของคนอื่นขึ้นมาแล้วเจอ compiler extension ที่ไม่เคยเห็นมาก่อนราว 12 ตัวซึ่งต้องทำความเข้าใจ
ถึงอย่างนั้น compiler extension เหล่านั้นก็เอาไปใช้ต่อได้ในทุกโปรเจ็กต์หลังจากนั้น ดังนั้นผมมองว่าเป็นกำไรสุทธิ
สำหรับผู้เชี่ยวชาญ ข้อเสียคือถ้าจะกะให้ได้ว่า cats จะกิน RAM เท่าไร อาจต้องมีปริญญาเอก
ถ้าโค้ดไหนใช้ Applicative หรือ Monad เราก็รู้พฤติกรรมของมันได้เพราะกฎเหล่านั้น
แพตเทิร์นการออกแบบเชิงวัตถุไม่มีกฎแบบนั้น มันเป็นแนวคิดหรือแพตเทิร์นที่นิยามแบบหลวม ๆ เปิดให้ตีความส่วนตัวและปรับแต่งได้ แต่ไม่มีกฎเฉพาะเจาะจงที่บอกว่าแท้จริงแล้วมันคืออะไร และควรคาดหวังพฤติกรรมหรือผลลัพธ์แบบไหน
แต่กับตัวอย่างในบล็อกนี้ ผมพูดได้แค่ว่าผมเดินตามเส้นทาง Clean Code แล้วทำร้ายตัวเองในอนาคตแบบนี้มานับครั้งไม่ถ้วน
ถ้าต้องไปศึกษาตัว abstraction เพื่อจะกลับมาเข้าใจมันใหม่ทีหลัง แปลว่า abstraction นั้นไม่ได้เป็นประโยชน์
ที่ที่ object-oriented เข้ากันอย่างเป็นธรรมชาติที่สุดคือการทำ GUI widget และ editor
กฎที่รวมกฎอื่น ๆ ของ object-oriented เข้าด้วยกันคือ อย่าถาม จงสั่ง
ในตัวอย่างนี้ handle จะถูกแชร์กันในแบบที่รู้แน่ชัด และจะมี control point เฉพาะตามรูปทรง ถ้า box handle เปลี่ยน ก็เรียก
updateControlPoints(changedHandle)ได้ และถ้า control point เปลี่ยน ก็เรียกupdateBoxHandles(changedControlPoint)ได้อาจมีวิธีแยกที่ดูสะอาดแบบอื่นได้เหมือนกัน แต่ถ้าเป็นผมจะทำแบบนั้น
แต่พอเป็นเรื่องจะใช้ไลบรารี parsing ตัวไหน จะใช้
Data.Text.StrictหรือData.Text.Lazyจะเขียนแบบ point-free มากแค่ไหน จะใช้.หรือ>>>หรือ combinator อื่น ๆ หรือจะเปิดใช้ language extension อะไร กลับไม่ได้มีฉันทามติกันง่าย ๆผมชอบ Haskell แต่ก็เจอปัญหาแบบเดียวกับภาษาอื่น ๆ คนเราไม่สามารถเห็นพ้องกันได้จริง ๆ ว่าวิธีที่ดีที่สุดในการทำงานคืออะไร
คุณจะได้ทั้ง type system ของ Haskell และประสิทธิภาพที่คาดเดาได้
แค่ย้ายส่วนคณิตศาสตร์ที่ซ้ำกันออกไปเป็นฟังก์ชันแยก แล้วเรียกจากฟังก์ชันปรับขนาดก็พอแล้ว
ที่ฟังก์ชันปรับขนาดเองดูเหมือนซ้ำกันก็เพราะมันประกอบกันเป็น interface และเพราะอย่างนั้นการรีแฟกเตอร์เพื่อลบมันออกจึงเป็นความผิดพลาด
แต่ส่วนคณิตศาสตร์ที่ฟังก์ชันเหล่านั้นทำเป็นเป้าหมายที่สมเหตุสมผลอย่างยิ่งสำหรับการจัดระเบียบโค้ด
สุดท้ายจะได้ฟังก์ชันเล็ก ๆ แบบ pure เต็มไปหมด แต่ก็ไม่เป็นปัญหาเพราะทดสอบและรีแฟกเตอร์ได้ง่าย
มีหลายวิธีและหลายแพตเทิร์นในการแก้ปัญหาเดียวกัน แต่การไปรื้อ interface เดิมที่คนอื่นใช้อยู่ไม่ใช่จุดเริ่มต้นที่ดี
ข้อดีของ abstraction คือสามารถทำให้ภายในยืดหยุ่นขึ้นได้โดยไม่กระทบส่วนที่เหลือ
ก้าวแรกที่ดีคืออย่างที่บอก คือสร้างฟังก์ชันคณิตศาสตร์ด้านล่างที่ยืดหยุ่นขึ้น แล้วปล่อยส่วนอื่นไว้ตามเดิม
จากนั้นค่อยคุยกับทีมให้ลึกขึ้นว่าควรสื่อสารอย่างไรเพื่อให้ interface ให้ façade ที่ชัดเจน ขณะที่ implementation ด้านในถูกทำให้เป็น abstraction
สิ่งที่อธิบายไว้ตรงนี้ให้ความรู้สึกเหมือนเป็น abstraction ที่แย่
ปัญหาที่แท้จริงคือคนจำนวนมากออกแบบจากการดูตัวอย่างแค่สองสามกรณี แล้วไม่ได้ตรวจสอบว่ามันทั่วไปพอที่จะรองรับการใช้งานในอนาคตหรือไม่
ควร abstract จากล่างขึ้นบน ไม่ใช่จากบนลงล่าง
ไม่จำเป็นต้องสร้างจุดเดียวที่ทุกอย่างไปรวมกันและต้องรองรับชุดผสม
2^nแบบถึงอย่างนั้นก็ยังลดการทำซ้ำได้ แก้ไขได้จากจุดเดียว และยังคงความสามารถในการปรับแต่งเมื่อจำเป็นไว้ได้
เพราะฉะนั้นการพูดแค่ว่า “หลีกเลี่ยง abstraction ที่แย่” หรือ “ทำให้รองรับการใช้งานในอนาคต” อย่างเดียวนั้นไม่พอ
โดยทั่วไปแล้วไม่มีใครทำนายอนาคตได้
ต่อให้ abstraction ดูเหมือนถูกต้อง ก็ควรใช้อย่างระมัดระวังอย่างยิ่ง เป็นพวกขี้ระแวงสุดโต่งยังดีกว่า
ถ้าแตะอย่างหนึ่งในโค้ดคณิตศาสตร์ ก็จะกระทบทุกอย่าง
พอถึงจุดหนึ่ง การปล่อยให้มีโค้ดซ้ำอยู่บ้าง พร้อมใช้โครงสร้างของโค้ดหรือคอมเมนต์เพื่อบอกความคล้ายกัน และค่อย ๆ เน้นความแตกต่างหลักด้วยคอมเมนต์เมื่อเวลาผ่านไป จะง่ายกว่า
แนวทางแบบ clean code กับโค้ดประเภทนี้รับมือกับ requirement ที่เปลี่ยนไปได้ไม่ดี
ท้ายที่สุดก็มักกลายเป็นกอง helper function ที่มีหลายเส้นทางของโค้ดตามตำแหน่งที่ถูกเรียกใช้
บางครั้งก็ abstract สิ่งนี้ได้ ซึ่งบางคนอาจสังเกตแล้วว่าประโยคก่อนหน้านี้กำลังอธิบาย object-oriented polymorphism
แต่จะไปรู้ requirement ที่แน่นอนได้ล่วงหน้าได้อย่างไร? ถ้าพยายามทำ “clean code” ตั้งแต่ระยะต้นแบบนี้ ก็มักจบลงด้วย abstraction ที่แย่หรือการออกแบบเกินจำเป็น
วิธีที่ถูกต้องคือยังไม่ต้องทำอะไร จดปัญหาที่อาจเกิดขึ้นไว้ก่อน แล้วรอจนกว่าจะรู้จัก problem domain มากกว่านี้
โค้ดที่ซ้ำกันเล็กน้อยแต่เข้าใจง่ายแบบตัวอย่างเดิมนั้นไม่น่ากังวลมาก
แต่ถ้าต้องแก้ issue เดียวกันในสามที่ หรือเพิ่มโค้ดฟีเจอร์ใหม่ในสามที่ แบบนั้นถึงจะเริ่มกังวล
ตอนนั้นจึงค่อยเริ่มคิดว่าจะผลักโค้ดที่ต้องแก้หรือเพิ่มนั้นไปไว้ในตำแหน่งร่วมกันได้อย่างไร
คนมักอ่านเรื่อง design pattern หรือ DRY แล้วระหว่างที่ยังเรียนรู้อยู่ก็มักนำไปใช้ผิด ๆ จากนั้นก็โยนความผิดให้ต้นฉบับ
Rob Pike เคยบอกว่าเหตุผลข้อหนึ่งที่ Go ออกมาเป็นแบบนั้นคือ “copy เล็ก ๆ ดีกว่า dependency เล็ก ๆ”
ยิ่งเขียนโปรแกรมมากขึ้น ก็ยิ่งเข้าใจความหมายนั้น
การทำซ้ำไม่ใช่ปัญหาในตัวมันเอง สิ่งสำคัญคือการมองให้ออกว่าตอนนี้กำลังอ่านและอาจต้องแก้อะไรเป็นหน่วย
DRY มักหมายถึงการเพิ่ม abstraction เข้าไปอีกชั้นหนึ่ง และถ้า abstraction นั้นไม่ได้ orthogonal พอ สถานการณ์ก็จะแย่ลง เพราะจากเดิมที่ต้องพิจารณาหน่วยเดียว ตอนนี้ต้องพิจารณาสองหน่วยแทน