4 คะแนน โดย GN⁺ 2023-12-09 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • แสดงให้เห็นว่าการ ยึดติดกับคลีนโค้ด อาจทำร้ายความสามารถในการแก้ไขเปลี่ยนแปลงจริงได้ ผ่านประสบการณ์ที่เขียนโค้ดปรับขนาดในกราฟิกเอดิเตอร์ใหม่โดยเน้นการกำจัดโค้ดซ้ำ แล้วสุดท้ายต้องย้อนกลับ
  • เดิมที implementation มีสูตรคำนวณตาม handle ของ Rectangle, Oval, Header, TextBlock ที่ซ้ำกันอยู่ และ implementation ใหม่พยายาม กำจัดความซ้ำซ้อน ด้วยวิธีแยกทิศทางกับรูปร่างออกจากกันแล้วนำมาประกอบ
  • ทันทีหลังรีแฟกเตอร์ ขนาดโค้ดลดลงครึ่งหนึ่งและจุดที่ต้องแก้ไขก็ดูเหมือนจะถูกรวมไว้ที่เดียว แต่เมื่อภายหลังต้องมี พฤติกรรมพิเศษ ตามรูปร่างหรือตาม handle การทำ abstraction กลับอาจกลายเป็นตัวถ่วง
  • ปัญหาที่ใหญ่กว่าการตัดสินใจทางเทคนิคคือการเช็กอินเข้า master โดยตรงโดยไม่คุยกับเพื่อนร่วมงานที่เขียนโค้ดเดิม ซึ่งอาจทำลาย การสร้างความไว้วางใจ ในทีม
  • “ความสะอาด” ไม่ใช่เป้าหมาย แต่เป็นเพียงเครื่องมือสำหรับรับมือกับระบบที่ซับซ้อน สิ่งสำคัญกว่าโค้ดหน้าตาเป็นอย่างไร คือทีมจะ รับมือกับการเปลี่ยนแปลง ร่วมกันได้หรือไม่

โค้ดปรับขนาดในกราฟิกเอดิเตอร์ที่ซ้ำกัน

  • บนแคนวาสของกราฟิกเอดิเตอร์มีโค้ดสำหรับปรับขนาดรูปร่างอย่างสี่เหลี่ยมและวงรีด้วย handle เล็ก ๆ ที่ขอบ
  • ตัว implementation ทำงานได้ แต่มีสูตรคำนวณตำแหน่งและขนาดที่ซ้ำกันตามรูปร่างและทิศทางของ handle
    • Rectangle มีเมธอดอย่าง resizeTopLeft, resizeTopRight, resizeBottomLeft, resizeBottomRight
    • Oval มีเมธอดอย่าง resizeLeft, resizeRight, resizeTop, resizeBottom
    • Header และ TextBlock ก็มีเมธอดปรับขนาดของตัวเองเช่นกัน
  • ถ้าผู้ใช้กด Shift ก็ต้องรองรับการ คงสัดส่วนเดิม ระหว่างปรับขนาดด้วย ทำให้มีสูตรเพิ่มขึ้นอีก

abstraction เพื่อกำจัดโค้ดซ้ำ

  • ความซ้ำดูเหมือนมีอยู่ตามสองแกน
    • ซ้ำกันในกลุ่ม handle ที่มีทิศทางเดียวกัน: Oval.resizeLeft() และ Header.resizeLeft() ต่างก็เป็นการลาก handle ด้านซ้ายเหมือนกัน
    • ซ้ำกันในกลุ่มเมธอดของรูปร่างเดียวกัน: หลายเมธอดของ Oval ต่างก็จัดการกับวงรีเหมือนกัน
  • โครงสร้างใหม่พยายามแยกโค้ดเป็น 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 ความคิดเห็น

 
GN⁺ 2023-12-09
ความเห็นจาก Hacker News
  • โค้ดซ้ำ บางครั้งก็ไม่เป็นไร แต่ก็ไม่ได้เป็นหลักฐานว่า Clean Code แย่
    ดูเหมือนจะรีแฟกเตอร์เกินไปหน่อย และถ้าแค่แยก “คณิตศาสตร์ซ้ำ ๆ 10 บรรทัด” ออกเป็นฟังก์ชัน ก็น่าจะสะอาดกว่านี้
    เพื่อนร่วมทีมที่ไม่แยก 10 บรรทัดนั้นเป็นฟังก์ชันก็ไม่ได้ทำถูกนัก ดังนั้นถ้าเป็นฉันคงปฏิเสธ PR แต่คงไม่ถึงกับเขียนใหม่เอง
    การเขียนทับ PR ของคนอื่นใหม่หมดเป็นการดูหมิ่นอย่างรวดเร็วที่ปิดโอกาสการถกเถียง และบ่อยครั้งเราก็ไม่รู้บริบทที่ทำให้โค้ดถูกเขียนมาแบบนั้น
    ถ้าไม่มีมาตรฐานการเขียนโค้ด ก็ควรรีวิวแล้วปฏิเสธ PR ไม่ใช่ปล่อยให้คอมมิตด้วยทำนองว่า “Clean Code ไม่สำคัญ” เพราะไม่กี่เดือนโค้ดเบสก็จะกลายเป็นสิ่งที่ไม่มีใครอยากทำงานด้วย
    สิ่งที่ควรเรียนรู้จากเรื่องนี้ไม่ใช่ Clean Code แต่คือ การสื่อสารและความใส่ใจ

    • การลงมือเขียนทับโค้ดที่ใช้งานได้ของคนอื่นเอง ไม่ใช่แค่เป็นการดูหมิ่นผู้เขียน แต่ยังเป็น พฤติกรรมที่ไม่เป็นมืออาชีพ เพราะเสียเวลาไปกับการแก้โค้ดที่มีคนดูแลอยู่แล้ว
      โอกาสที่ผู้เขียนเดิมจะได้เรียนรู้ว่าโค้ดคุณภาพต่ำทำให้เสียเวลาอย่างไรในโลกจริงก็หายไปด้วย
      ต่อให้แนวทาง Clean Code จะดีกว่า ประเด็นสำคัญตรงนี้ก็ไม่ใช่ตัวโค้ด แต่เป็นการที่เขาเสียเวลาของตัวเองโดยไม่ได้ให้ฟีดแบ็กที่มีประโยชน์
      ทางที่ดีที่สุดคือให้ผู้เขียนเดิมยกระดับมาตรฐานของตัวเอง ส่วนเจ้าตัวก็ไปทำงานอื่นที่ควรแก้จริง ๆ ซึ่งก็คือคำตอบที่ถูกต้องคือ code review
    • บทเรียนที่ใกล้เคียงกว่าคือ “อย่าใช้กฎอย่างเกินเลยโดยไม่คำนึงถึงการประนีประนอมตามบริบท” และนี่เป็นบทเรียนที่ต้องใช้เวลากว่าจะเรียนรู้
    • จากที่เห็นมานานกว่า 20 ปี ความคิดแบบ “ไม่ได้แยก 10 บรรทัดที่ซ้ำกันออกเป็นฟังก์ชัน แสดงว่าแย่มาก” มักมาจากคนที่ไปโฟกัสผิดจุดในโค้ดเบส
      คนแบบนี้มักสร้างปัญหาในอนาคตจากการแก้สิ่งที่ไม่ใช่ปัญหา แทนที่จะไปแก้ปัญหาจริง
      สำหรับการทำครั้งแรก มักจะถูกต้องกว่าถ้าไม่รีบทำ 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 และแนวทางที่เกี่ยวข้อง
      คนจำนวนมากขึ้นเรื่อย ๆ รู้สึกว่าสองอย่างนี้ต่างกันมากพอสมควร บางครั้งการทำตามแนวทางในหนังสือบางข้อกลับทำให้ได้โค้ดที่ไม่ได้สะอาดเลย
      ในโลกอุดมคติทุกคนคงอยากเขียนโค้ดที่สะอาด แต่คนที่รู้สึกว่า Clean Code เป็นวิธีที่ดีในการไปถึงเป้าหมายนั้นกำลังลดลง
      โดยเฉพาะ abstraction ที่มากเกินไป และการกำจัดความซ้ำซ้อน กำลังถูกหลายคนมองว่าเป็นต้นตอของโค้ดที่รก
    • สิ่งที่เรียนรู้จากการทำงานในอุตสาหกรรมคือ ต่อให้เขียนโค้ดได้สะอาดแค่ไหน พอมันเจอกับ requirements ของผลิตภัณฑ์ ก็มักอยู่ได้ไม่นาน
      บางครั้งหนี้ทางเทคนิคถูกเข้ารหัสอยู่ใน requirements ของผลิตภัณฑ์เอง จนรีแฟกเตอร์หรือ best practice ก็แก้ไม่ได้ ทำได้แค่บรรเทาความเจ็บปวด
    • มันฟังดูคล้ายตรรกะแบบ “No true Scotsman”
      เป้าหมายของ Clean Code กับผลลัพธ์จากการปฏิบัติจริงต่างกันมาก เพราะคนส่วนใหญ่มักคาดเดาสภาพของโค้ดในอนาคตได้ไม่ดี แม้จะมี requirements อยู่ตรงหน้าก็ตาม
      Clean Code มักชวนให้ถามคำถามชวนสยองแบบ “ถ้าเกิดว่า...?” และสิ่งนี้ก็มักกลายเป็นกล่องแพนดอราที่ทำให้การปล่อยของล่าช้าไม่รู้จบ
      เกือบทุกครั้ง การเขียนตามสิ่งที่รู้อยู่แล้ว และให้คนที่มีประสบการณ์ในโดเมนเป็นคนนำสถาปัตยกรรม จะดีกว่า
    • ถ้าจะเสนอรายการทางเลือก ก็คือ abstraction ส่วนใหญ่ที่เราสร้างนั้นแย่ และแม้แต่สิ่งที่เราคิดว่าดีก็มักเป็นแบบนั้น
      abstraction ที่ดีจำนวนมากมีอยู่ในไลบรารีแล้ว แน่นอนว่าน่าเสียดายที่มันไม่ได้เป็นแบบนั้นเสมอไป
      abstraction ที่แย่ จะคอยดูดเวลา และเพราะโค้ดก็เป็นเรื่องของการสื่อสารกับคนอื่นในทีมด้วย วิธีที่เพื่อนร่วมทีมรับสารจึงสำคัญกว่าวิธีคิดของฉันมากนัก
  • เพื่อนร่วมงานเขียนโค้ดจำนวนมากด้วยการคัดลอก-วาง และคุณก็รีแฟกเตอร์หลังจากคอมมิตแล้ว
    เพื่อนร่วมงานไปบ่นกับหัวหน้า หัวหน้าก็เรียกคุณไปตำหนิ ซึ่งเท่ากับได้ข้อสรุปว่า คราวหน้าควรปล่อยความรกไว้ใน codebase
    มีบทเรียนที่ควรเรียนรู้จากเรื่องนี้ก็จริง แต่ไม่ได้แปลว่าการคัดลอก-วางดีกว่าการเขียนฟังก์ชัน
    การรีแฟกเตอร์ควรถูกส่งเป็นการเปลี่ยนแปลงแยกต่างหากพร้อมแท็กผู้เขียนเดิมเพื่อขอรีวิว
    แต่ที่เพื่อนร่วมงานไม่คุยกันตรงๆ แล้วไปแจ้งหัวหน้า และหัวหน้าก็สั่งให้ย้อนกลับ นี่เป็นสัญญาณที่ค่อนข้างแย่มาก

    • ถ้าเป็นฉัน ฉันคงส่งข้อความส่วนตัวใน Slack ไปหาเพื่อนร่วมงานว่า “ตรงนี้มีโค้ดคณิตศาสตร์ 10 บรรทัดซ้ำอยู่ในแต่ละฟังก์ชัน เราแยกออกเป็นฟังก์ชันต่างหากจะดีกว่าไหม?”
      คำตอบที่เป็นไปได้คือ “ไม่ครับ/ค่ะ เพราะว่า...” หรือถ้าโชคดีก็อาจเป็น “จริงด้วย พรุ่งนี้ผม/ฉันทำให้”
      ทุกคนหลีกเลี่ยงปัญหาได้โดยไม่ต้องเป็นเรื่องใหญ่ และบางคนอาจได้เรียนรู้อะไรด้วย
    • ถ้าเป็นการคัดลอก-วางแบบขี้เกียจ การใช้ฟังก์ชันกับสิ่งที่ซ้ำกัน หรือก็คือการทำ abstraction ย่อมดีกว่าในมุมมองของ DRY
      แต่ผู้เขียนก็เพิ่มบริบทสำคัญไว้ เขาแลกการลดความซ้ำกับความเป็นไปได้ที่ requirement จะเปลี่ยน และต่อมาก็พบว่าจำเป็นต้องมีกรณีพิเศษและพฤติกรรมจำนวนมากสำหรับ handle ของแต่ละ shape
      นี่แสดงให้เห็นความต่างระหว่าง DRY แบบบังเอิญ กับ DRY แบบแก่นแท้
      บางครั้งไลบรารี A กับ B ดูสอดคล้องกันจนทำให้โค้ดออกมาเหมือนกัน แต่ในความจริงอาจเป็นแค่เรื่องบังเอิญ
      ถึงแม้ wrapper ที่หุ้ม lib A กับ wrapper ที่หุ้ม lib B จะมีโค้ดเหมือนกัน แต่ถ้าจริงๆ แล้วมันหุ้มคนละสิ่งโดยสิ้นเชิง การทำ abstraction จะให้ผลลัพธ์ที่แย่ลง
      “15 รายการ” ก็เหมือนกัน ถ้าหมายถึง “ทุกลิสต์ต้องแสดง 15 รายการเสมอ” การทำ abstraction กับเลข 15 ก็ถูกต้อง แต่ถ้าเป็น “15 เพลงอันดับต้นบนหน้าแรก” กับ “คอมเมนต์เริ่มต้น 15 รายการใต้เพลง” อย่างละกรณีกัน การจับเลข 15 สองจุดนี้มารวมกันกลับเป็นทางเลือกที่แย่กว่า
    • สัญญาณอันตรายที่ใหญ่ที่สุดคือการแก้งานที่เพื่อนร่วมงานส่งมาโดยไม่สื่อสารอะไรเลย
      สัญญาณอันตรายอันดับสองคือแม้แต่ตัวการรีแฟกเตอร์เองก็ไม่ใช่การรีแฟกเตอร์ที่ดี มันยังขาดความเป็นโมดูลาร์ ยังผูกกันแน่น และแค่ลดความซ้ำลงได้นิดหน่อยเท่านั้น
    • การแท็กผู้เขียนเดิมก็อาจก่อปัญหาได้เหมือนกัน
      มันอาจฟังเหมือน “โค้ดที่คุณเขียนมันไม่ค่อยดี ฉันเลยเขียนใหม่ให้ มีปัญหาอะไรไหม?”
      ทางออกที่ดีที่สุดน่าจะเป็นการขอคำอธิบายในลักษณะ “รีวิวช้า” พร้อมเสนอแนวทางแก้ และหวังว่าผู้เขียนเดิมจะไม่ปล่อยผ่านไปเฉยๆ
    • ตอนท้ายบทความมีประเด็นสำคัญว่า ในการเปลี่ยนแปลง codebase ภายหลัง จำเป็นต้องจัดการ handle ของแต่ละ shape แบบเฉพาะกรณี ซึ่งทำได้ยากในโครงสร้างที่รีแฟกเตอร์แล้ว แต่ทำได้ง่ายในโค้ดที่ซ้ำกัน
      แก่นสำคัญคือโค้ดจะเปลี่ยนในอนาคต ทั้ง requirement ก็เปลี่ยน และโค้ดที่เกี่ยวข้องก็เปลี่ยน
      ถ้าคุณสร้าง abstraction ที่จำกัดความสามารถในการเปลี่ยนแปลงนั้น คุณก็จะทำลายความสามารถของตัวเองในอนาคตในการปรับ codebase ให้เข้ากับสถานการณ์
      โดยเฉพาะ abstraction ที่ลงทุนลงแรงไปมาก มักทำให้เราเกิดความยึดติดทางอารมณ์ และเสียเวลาเพิ่มไปอีกกับการพยายามยัดการเปลี่ยนแปลงให้เข้ากับวิธีแก้ที่ดูสวยงามนั้น
      KISS นั่นแหละถูกต้อง
  • กรณีนี้ดูเหมือนเป็นปัญหาแบบ “ไม่มีใครชอบคนที่ทำตัวอวดเก่ง”
    โดยเฉพาะเวลานักพัฒนารุ่นน้องยังไม่ได้เรียนรู้ว่างานไม่ได้อยู่แค่ระหว่างตัวเองกับคอมพิวเตอร์ แต่ยังอยู่ระหว่างตัวเอง คอมพิวเตอร์ และคนอื่นๆ ด้วย
    เวอร์ชันที่สะอาดกว่านี้มีโอกาสจะถูกยอมรับมาก ถ้าได้คุยกับผู้เขียนเดิมจริงๆ และตกลงกันได้ถึงวิธีที่ดีกว่า
    ชื่อบทความดูจะเหมาะเป็น ฉันได้เรียนรู้วิธีทำงานเป็นทีม มากกว่า “ลาก่อน คลีนโค้ด”

    • วลี “ไม่มีใครชอบคนที่ทำตัวอวดเก่ง” จับบรรยากาศในสภาพแวดล้อมนักพัฒนาได้ดี
      โค้ดที่ซับซ้อนกว่าถูกมองว่าเป็นภัยคุกคาม และอาจถูกมองว่าเป็นคู่แข่งที่ต้องกดไว้ผ่านสายการบังคับบัญชา
      ถ้าเป็นนักพัฒนารุ่นน้อง ก็ควรรู้ไว้ว่านี่ขึ้นอยู่กับบริบท ในที่ทำงานจำนวนมาก ความเรียบง่ายสำคัญกว่าความยืดหยุ่น มีการหลีกเลี่ยงฟีเจอร์ภาษาที่ชวนสับสน และนิยมวิธีทำงานที่นั่งอยู่หน้าแป้นพิมพ์นานๆ รวมถึงภาษาที่เอื้อแบบนั้น
      ในทางกลับกัน ปรัชญาที่ให้ความสำคัญกับการเรียนรู้ตลอดชีวิต ความเป็นช่างฝีมือด้านซอฟต์แวร์ และการทบทวนกระบวนการ กลับขัดแย้งกับสภาพแวดล้อมแบบนี้โดยสิ้นเชิง
      ความพยายามที่จะดีขึ้นมักไม่เป็นที่นิยมในหมู่คนที่ไม่เติบโต
      ทั้งสองแบบก็เป็นทางเลือกชีวิตที่ใช้ได้ทั้งคู่ ซอฟต์แวร์อาจเป็นที่ที่ทำงาน 7 ชั่วโมงแล้วกลับบ้านอย่างสบายใจ หรืออาจเป็นโลกแห่งการสำรวจที่มีเพียงจินตนาการและสติปัญญาเป็นขีดจำกัดก็ได้
      ถ้าคุณอยากค้นพบสิ่งใหม่ๆ ก็ควรระวังการใช้เวลาหลายสิบปีกับคนที่กลัวสิ่งที่ไม่รู้จัก
      พูดให้สั้นกว่านั้นคือ ถ้า แรงจูงใจ ของคุณกับเพื่อนร่วมงานไม่ตรงกัน ก็ควรพิจารณาจากไป
    • ฉันว่าชื่อเรื่องก็โอเคแล้ว
      บทความนี้มีเหตุผลที่คนรักกันมานาน และมันช่วยเปิดตาให้นักพัฒนาหลายคนที่ตกหลุมพรางแบบเดียวกัน
      ต่อให้ทำงานคนเดียว คุณก็ยังอาจออกแบบ abstraction ที่ดูสะอาดมากเกินไป แล้วในอนาคตก็ถูก abstraction นั้นเล่นงานได้
    • ปัญหาแบบนี้ส่วนใหญ่มักเกิดจาก ซีเนียร์ ที่หลงอำนาจ มากกว่าจะเกิดจากจูเนียร์
  • ปัญหาคือการเปลี่ยนแปลงถูกคอมมิตเข้า main branch โดยตรงโดยไม่มีรีวิว
    การเปลี่ยนแปลงโค้ดทุกครั้งควรมี peer review ไม่ว่าจะผ่าน pair programming หรือ PR แบบดั้งเดิม เพื่อป้องกันไม่ให้เรื่องบานปลายแบบนี้
    หัวหน้าเป็นหัวหน้าที่แย่ เขาควรสังเกตว่าโปรเซสที่ดีต่างหากที่หายไป ไม่ใช่มาคอยดุคน
    ถ้าเป็นทีมที่มีวัฒนธรรม code review แม้เด็กใหม่ไฟแรงจะลองทำแบบนี้ เพื่อนร่วมงานก็น่าจะพูดว่า “ขอบคุณนะ แต่แบบนี้ยังไม่ใช่” หรือ “ไอเดียดีนะ ลองทำแบบนี้ดีกว่า”
    อีกเรื่องหนึ่งคือฉันคิดว่าไม่ควรรีแฟกเตอร์โค้ดโดยไม่มีเหตุผลที่ดี โค้ดที่แย่ก็อาจเป็นโค้ดที่แทบไม่ต้องแก้อีกเลยก็ได้
    เวลาที่ดีที่สุดในการรีแฟกเตอร์คือเมื่อคุณตระหนักว่าจำเป็นต้องเปลี่ยนพฤติกรรม แต่โครงสร้างเดิมทำให้มันยากเกินไป
    น่าเสียดายที่ในกรณีนี้ดูเหมือนจะได้บทเรียนที่ผิด

    • การรีวิวโค้ดหลัง merge ก็โอเคเต็มที่ และในบางแง่ก็ดีกว่าด้วย
  • ไม่เข้าใจว่าทำไมทุกคนถึงพูดแค่ว่าเขาควรทำอะไรแทน นี่เป็นบทเรียนที่ชอบธรรมอย่างเพียงพอแล้ว
    ในวงการการเงิน เรารับมือกับผลิตภัณฑ์ที่คล้ายกันในระดับหนึ่งแต่ไม่เหมือนกันเสียทีเดียวอยู่ตลอด ตัวเลือกหุ้นกับตัวเลือกอัตราแลกเปลี่ยนต่างก็เป็น option ต่างก็มีราคาใช้สิทธิ และตั้งราคาด้วย Black-Scholes บางเวอร์ชัน แต่ในความเป็นจริงแล้วต่างกันมาก
    คนที่เพิ่งเข้ามาใหม่มักถูกล่อลวงอย่างมากให้อยากทำให้พฤติกรรมหลายอย่างใช้ร่วมกันได้ และเราก็มักลงเอยอยู่ในจุดยืนของหัวหน้าตามโพสต์ต้นฉบับ ที่ต้องบอกนักพัฒนารุ่นหนุ่มสาวให้ใจเย็น ๆ และแยกมันออกจากกันไว้
    ไม่ได้หมายความว่าควรบอกลา Clean Code แต่หมายความว่าต้องหลีกเลี่ยง การทำ abstraction มากเกินไป จึงจะรักษาโค้ดให้สะอาดได้จริง

    • ตัวอย่างด้านการเงินก็ถูกต้อง แต่ที่นี่เรากำลังจัดการกับ เรขาคณิต
      สิ่งที่ handle ทั่วไปทำคือ affine transform ได้แก่ การเลื่อน การหมุน การย่อ-ขยาย และการสะท้อน และถ้าเป็นการย่อ-ขยายแบบสม่ำเสมอก็เป็น similarity transform ซึ่งรับประกันได้ว่าใช้ได้กับรูปทรงใด ๆ
      ปัญหาที่แท้จริงคือไม่ควรทำ abstraction กับสิ่งที่เรายังรู้ไม่มากพอ โดยเฉพาะถ้าไม่รู้ว่าความเป็นไปได้ในอนาคตจะมีอะไรบ้าง
      ตัวอย่างเช่น ถ้าคิดว่าวงรีมีแค่ circle กับ ellipse ก็จะตกใจไม่น้อยเมื่อ “oval” ตัวแรกของจริงปรากฏขึ้น
    • ผมเข้าใจประเด็นของบทความ แต่ชื่อเรื่องสามารถถูกอ่านได้ง่ายมากเหมือนเป็นข้ออ้างเพื่อทำให้โค้ดสกปรกดูชอบธรรม
      ผู้เขียนไม่ได้แยกความต่างระหว่างโค้ดสกปรกกับโค้ดที่ “ไม่ clean” อย่างชัดเจน คนอื่นเลยพยายามแยกให้
    • ค่อนข้างชัดว่าทุกคนที่นี่รู้สึกว่าตัวเองรู้ดีกว่าผู้เขียนและสมาชิกทีมในตอนนั้น ทั้งที่มีบริบทเพียงแค่ pseudocode ไม่กี่บรรทัด
      DRY มีคุณค่า แต่ abstraction ก็มีต้นทุน มันเป็นเรื่องของ trade-off เสมอ
  • เหตุผลหนึ่งที่ชอบภาษาอย่าง Haskell คือแนวคิดสุดโต่งเรื่อง abstraction ไปอยู่ที่ ระดับภาษา ไม่ใช่ระดับการเขียนโค้ด
    ทุกคนเห็นตรงกันว่า Applicative ควรทำอะไร ความท้าทายจึงเลื่อนไปทางขวาของ pipeline จากไอเดียสู่โค้ดมากขึ้น คือไปอยู่ที่การหา Applicative ที่เหมาะกับปัญหา
    แพตเทิร์นการออกแบบเชิงวัตถุส่วนใหญ่มีคู่เทียบที่เป็นธรรมชาติและนิยามชัดเจนในสภาพแวดล้อมแบบนี้ และช่วยลด abstraction เฉพาะโปรเจ็กต์ได้
    สำหรับมือใหม่ ต้นทุนเริ่มต้นในการเรียนภาษาสูงมาก และก็มักมีเหตุการณ์ที่เปิดโค้ด Haskell ของคนอื่นขึ้นมาแล้วเจอ compiler extension ที่ไม่เคยเห็นมาก่อนราว 12 ตัวซึ่งต้องทำความเข้าใจ
    ถึงอย่างนั้น compiler extension เหล่านั้นก็เอาไปใช้ต่อได้ในทุกโปรเจ็กต์หลังจากนั้น ดังนั้นผมมองว่าเป็นกำไรสุทธิ
    สำหรับผู้เชี่ยวชาญ ข้อเสียคือถ้าจะกะให้ได้ว่า cats จะกิน RAM เท่าไร อาจต้องมีปริญญาเอก

    • ที่เรารู้ว่า Applicative กับ Monad ทำอะไร ก็เพราะมี กฎทางคณิตศาสตร์ อยู่
      ถ้าโค้ดไหนใช้ Applicative หรือ Monad เราก็รู้พฤติกรรมของมันได้เพราะกฎเหล่านั้น
      แพตเทิร์นการออกแบบเชิงวัตถุไม่มีกฎแบบนั้น มันเป็นแนวคิดหรือแพตเทิร์นที่นิยามแบบหลวม ๆ เปิดให้ตีความส่วนตัวและปรับแต่งได้ แต่ไม่มีกฎเฉพาะเจาะจงที่บอกว่าแท้จริงแล้วมันคืออะไร และควรคาดหวังพฤติกรรมหรือผลลัพธ์แบบไหน
    • ในฐานะผู้ใช้ Haskell ผมเห็นด้วยว่าความสามารถด้าน abstraction ของภาษานั้นยอดเยี่ยมมาก
      แต่กับตัวอย่างในบล็อกนี้ ผมพูดได้แค่ว่าผมเดินตามเส้นทาง Clean Code แล้วทำร้ายตัวเองในอนาคตแบบนี้มานับครั้งไม่ถ้วน
      ถ้าต้องไปศึกษาตัว abstraction เพื่อจะกลับมาเข้าใจมันใหม่ทีหลัง แปลว่า abstraction นั้นไม่ได้เป็นประโยชน์
    • โพสต์ต้นฉบับน่าจะดีกว่าถ้าทีมใช้หลักการเชิงวัตถุมากกว่านี้ เว้นแต่ว่าทั้งทีมจะต่อต้านมันโดยรวม ซึ่งถ้าเป็นทีมแบบนั้นผมคงไปทำงานที่อื่น
      ที่ที่ object-oriented เข้ากันอย่างเป็นธรรมชาติที่สุดคือการทำ GUI widget และ editor
      กฎที่รวมกฎอื่น ๆ ของ object-oriented เข้าด้วยกันคือ อย่าถาม จงสั่ง
      ในตัวอย่างนี้ handle จะถูกแชร์กันในแบบที่รู้แน่ชัด และจะมี control point เฉพาะตามรูปทรง ถ้า box handle เปลี่ยน ก็เรียก updateControlPoints(changedHandle) ได้ และถ้า control point เปลี่ยน ก็เรียก updateBoxHandles(changedControlPoint) ได้
      อาจมีวิธีแยกที่ดูสะอาดแบบอื่นได้เหมือนกัน แต่ถ้าเป็นผมจะทำแบบนั้น
    • อาจจะเห็นด้วยก็ได้ว่า Applicative ควรทำอะไร
      แต่พอเป็นเรื่องจะใช้ไลบรารี parsing ตัวไหน จะใช้ Data.Text.Strict หรือ Data.Text.Lazy จะเขียนแบบ point-free มากแค่ไหน จะใช้ . หรือ >>> หรือ combinator อื่น ๆ หรือจะเปิดใช้ language extension อะไร กลับไม่ได้มีฉันทามติกันง่าย ๆ
      ผมชอบ Haskell แต่ก็เจอปัญหาแบบเดียวกับภาษาอื่น ๆ คนเราไม่สามารถเห็นพ้องกันได้จริง ๆ ว่าวิธีที่ดีที่สุดในการทำงานคืออะไร
    • จะก้าวไปอีกขั้นก็ใช้ Rust ที่มี affine type ไปเลย
      คุณจะได้ทั้ง type system ของ Haskell และประสิทธิภาพที่คาดเดาได้
  • แค่ย้ายส่วนคณิตศาสตร์ที่ซ้ำกันออกไปเป็นฟังก์ชันแยก แล้วเรียกจากฟังก์ชันปรับขนาดก็พอแล้ว
    ที่ฟังก์ชันปรับขนาดเองดูเหมือนซ้ำกันก็เพราะมันประกอบกันเป็น interface และเพราะอย่างนั้นการรีแฟกเตอร์เพื่อลบมันออกจึงเป็นความผิดพลาด
    แต่ส่วนคณิตศาสตร์ที่ฟังก์ชันเหล่านั้นทำเป็นเป้าหมายที่สมเหตุสมผลอย่างยิ่งสำหรับการจัดระเบียบโค้ด

    • ใช่ ดูเป็นทิศทางที่ชัดเจน และปกติผมก็ทำแบบนั้น
      สุดท้ายจะได้ฟังก์ชันเล็ก ๆ แบบ pure เต็มไปหมด แต่ก็ไม่เป็นปัญหาเพราะทดสอบและรีแฟกเตอร์ได้ง่าย
    • โดยเฉพาะถ้าเป็นการรีแฟกเตอร์ เวลาทำงานกับทีมปกติก็ควรทำแบบ ค่อยเป็นค่อยไป
      มีหลายวิธีและหลายแพตเทิร์นในการแก้ปัญหาเดียวกัน แต่การไปรื้อ interface เดิมที่คนอื่นใช้อยู่ไม่ใช่จุดเริ่มต้นที่ดี
      ข้อดีของ abstraction คือสามารถทำให้ภายในยืดหยุ่นขึ้นได้โดยไม่กระทบส่วนที่เหลือ
      ก้าวแรกที่ดีคืออย่างที่บอก คือสร้างฟังก์ชันคณิตศาสตร์ด้านล่างที่ยืดหยุ่นขึ้น แล้วปล่อยส่วนอื่นไว้ตามเดิม
      จากนั้นค่อยคุยกับทีมให้ลึกขึ้นว่าควรสื่อสารอย่างไรเพื่อให้ interface ให้ façade ที่ชัดเจน ขณะที่ implementation ด้านในถูกทำให้เป็น abstraction
  • สิ่งที่อธิบายไว้ตรงนี้ให้ความรู้สึกเหมือนเป็น abstraction ที่แย่
    ปัญหาที่แท้จริงคือคนจำนวนมากออกแบบจากการดูตัวอย่างแค่สองสามกรณี แล้วไม่ได้ตรวจสอบว่ามันทั่วไปพอที่จะรองรับการใช้งานในอนาคตหรือไม่

    • โดยเฉพาะแนวทางที่ดีคือ abstract เฉพาะส่วนที่เหมือนกันด้วยฟังก์ชันที่เรียบง่ายมาก ๆ เท่านั้น
      ควร abstract จากล่างขึ้นบน ไม่ใช่จากบนลงล่าง
      ไม่จำเป็นต้องสร้างจุดเดียวที่ทุกอย่างไปรวมกันและต้องรองรับชุดผสม 2^n แบบ
      ถึงอย่างนั้นก็ยังลดการทำซ้ำได้ แก้ไขได้จากจุดเดียว และยังคงความสามารถในการปรับแต่งเมื่อจำเป็นไว้ได้
    • ปัญหาในโลกความจริงคือไม่มีใครรู้ตอนเขียนหรอกว่าตัวเองกำลังใช้ abstraction ที่แย่ อยู่ ถ้ารู้ก็คงไม่เขียน
      เพราะฉะนั้นการพูดแค่ว่า “หลีกเลี่ยง abstraction ที่แย่” หรือ “ทำให้รองรับการใช้งานในอนาคต” อย่างเดียวนั้นไม่พอ
      โดยทั่วไปแล้วไม่มีใครทำนายอนาคตได้
      ต่อให้ abstraction ดูเหมือนถูกต้อง ก็ควรใช้อย่างระมัดระวังอย่างยิ่ง เป็นพวกขี้ระแวงสุดโต่งยังดีกว่า
    • ปัญหาของเวอร์ชัน “clean” คือมันผูกกันแน่นไปหมด
      ถ้าแตะอย่างหนึ่งในโค้ดคณิตศาสตร์ ก็จะกระทบทุกอย่าง
      พอถึงจุดหนึ่ง การปล่อยให้มีโค้ดซ้ำอยู่บ้าง พร้อมใช้โครงสร้างของโค้ดหรือคอมเมนต์เพื่อบอกความคล้ายกัน และค่อย ๆ เน้นความแตกต่างหลักด้วยคอมเมนต์เมื่อเวลาผ่านไป จะง่ายกว่า
      แนวทางแบบ 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 พอ สถานการณ์ก็จะแย่ลง เพราะจากเดิมที่ต้องพิจารณาหน่วยเดียว ตอนนี้ต้องพิจารณาสองหน่วยแทน