แนวทางการออกแบบแบบ Layered ของ Go
(jerf.org)- ภาษา Go ห้ามการอ้างอิงแบบวนซ้ำระหว่างแพ็กเกจอย่างเคร่งครัด จึงผลักดันให้เกิด การออกแบบแบบเป็นชั้น (layered design) โดยธรรมชาติ
- บทความนี้อธิบาย โครงสร้างแบบเป็นชั้นที่โปรเจกต์ Go จำเป็นต้องมี และเสนอว่าเพียงเท่านี้ก็เพียงพอแล้วโดยไม่ต้องบังคับสถาปัตยกรรมอื่นเพิ่มเติม
- เมื่อเกิดการพึ่งพาแบบวนซ้ำ บทความนำเสนอ กลยุทธ์การรีแฟกเตอร์ที่เป็นรูปธรรมและใช้งานได้จริง แบบเป็นลำดับขั้นเพื่อแก้ปัญหา
- แต่ละแพ็กเกจถูกออกแบบให้มีหน่วยฟังก์ชันที่มีความหมายได้ด้วยตัวเอง จึงเอื้อต่อ การทดสอบ การบำรุงรักษา และการแยกเป็นไมโครเซอร์วิส
- ท้ายที่สุด วิธีนี้ช่วยป้องกันปัญหาที่มักเกิดขึ้นในการออกแบบโค้ดจริงอย่าง "อยากได้แค่กล้วย แต่กลับต้องยกมาทั้งป่า"
แนวทางการออกแบบแบบ Layered ใน Go
หลักการพื้นฐาน
- Go ห้ามการอ้างอิงแบบวนซ้ำระหว่างแพ็กเกจ
- ความสัมพันธ์
importของทุกโปรแกรม Go ต้องประกอบเป็น กราฟมีทิศทางแบบไม่มีวงจร (DAG) - โครงสร้างนี้ไม่ใช่ทางเลือก แต่เป็น กฎการออกแบบที่ถูกบังคับในระดับภาษา
การเกิดขึ้นโดยอัตโนมัติของการวางชั้นแพ็กเกจ
- แพ็กเกจภายในโปรเจกต์ที่ไม่นับรวมแพ็กเกจภายนอก สามารถถูกจัดเป็นชั้นได้โดยอัตโนมัติตาม ความลึกของการอ้างอิง
- ตามภาพด้านล่าง ชั้นล่างสุดจะเป็น แพ็กเกจยูทิลิตีหลัก เช่น metrics, logging, โครงสร้างข้อมูลใช้ร่วมกัน
- จากนั้นแพ็กเกจระดับบนจะค่อย ๆ ประกอบฟังก์ชันเพิ่มขึ้นและซ้อนขึ้นไปด้านบน
คุณลักษณะของแนวทางการออกแบบนี้
- ชั้นต่าง ๆ ไม่ได้อิงกับการทำ abstraction แบบลำดับชั้น แต่ยึดตามทิศทางของการอ้างอิง
- หนึ่งแพ็กเกจ สามารถอ้างอิงแพ็กเกจระดับล่างได้หลายตัว
- แนวทางการออกแบบเดิมอย่าง MVC, Hexagonal Architecture เป็นต้น ก็สามารถ "นำมาปรับใช้" บนโครงสร้างนี้ได้
→ แต่ต้องคำนึงถึงข้อจำกัดเชิงโครงสร้างของ Go เสมอ
กลยุทธ์การแก้ปัญหาการอ้างอิงแบบวนซ้ำ
เมื่อเกิดการอ้างอิงแบบวนซ้ำ ให้ลองรีแฟกเตอร์ตามลำดับดังนี้:
1. ย้ายฟังก์ชัน
- เป็นวิธีที่แนะนำมากที่สุด
- วิเคราะห์ฟังก์ชันที่ทำให้เกิดวงจร อย่างแม่นยำ แล้ว ย้ายไปยังตำแหน่งที่เหมาะสมในเชิงตรรกะ
- แม้จะไม่ได้ใช้บ่อย แต่ ช่วยเพิ่มความชัดเจนเชิงแนวคิดได้มากที่สุด
2. แยกฟังก์ชันที่ใช้ร่วมกันออกเป็นแพ็กเกจต่างหาก
- ย้ายชนิดข้อมูลหรือฟังก์ชันที่ทั้งสองฝั่งใช้งานร่วมกัน (
Usernameเป็นต้น) ไปไว้ใน แพ็กเกจที่สาม - ถึงแพ็กเกจจะดูเล็ก ก็แยกออกมาได้อย่างไม่ต้องลังเล
→ เพราะเมื่อเวลาผ่านไป มีโอกาสสูงที่แพ็กเกจนั้นจะเติบโตขึ้น
3. สร้างแพ็กเกจระดับบนสำหรับการประกอบ
- สร้าง แพ็กเกจที่สาม สำหรับประกอบสองแพ็กเกจที่เกิดวงจรเข้าด้วยกัน
- ตัวอย่าง: แยกการพึ่งพาสองทางของ
CategoryและBlogPostไปไว้ในแพ็กเกจระดับบน
→ ทำให้แพ็กเกจระดับล่างยังคงเป็น dumb struct ส่วนฟังก์ชันจริงอยู่ที่การประกอบในแพ็กเกจระดับบน
4. นำอินเทอร์เฟซมาใช้
- แทนที่การพึ่งพาด้วย อินเทอร์เฟซที่มีเฉพาะเมธอดที่จำเป็น สำหรับ struct หรือฟังก์ชันนั้น
- ช่วยกำจัดการพึ่งพาที่ไม่จำเป็นและเพิ่ม ความสะดวกในการทดสอบ
- อย่างไรก็ตาม หากใช้มากเกินไป กลับอาจทำให้การออกแบบซับซ้อนขึ้น
5. คัดลอก (Copy)
- หากสิ่งที่ต้องพึ่งพามีขนาดเล็กมาก ก็ คัดลอกมาใช้อย่างง่าย ๆ ได้
- แม้อาจดูเหมือนขัดกับ DRY แต่ในทางปฏิบัติ มักช่วยให้การออกแบบชัดเจนขึ้น
6. รวมเป็นแพ็กเกจเดียว
- หากทุกวิธีก่อนหน้านี้ใช้ไม่ได้ ให้ รวมสองแพ็กเกจเข้าด้วยกัน
- หากไม่ได้กลายเป็นแพ็กเกจที่ใหญ่เกินไป ก็ถือว่ายอมรับได้
→ แต่ไม่ควรรวมแบบอัตโนมัติโดยไม่คิด และควรตัดสินใจอย่างรอบคอบ
ข้อดีเชิงปฏิบัติของแนวทางการออกแบบนี้
- แต่ละแพ็กเกจมี หน่วยฟังก์ชันที่มีความหมายได้ด้วยตัวเอง และทดสอบแยกได้อย่างอิสระ
- การอ้างอิงภายในแพ็กเกจมีข้อจำกัด จึง ทำความเข้าใจแพ็กเกจแต่ละตัวได้โดยไม่ต้องเข้าใจโค้ดทั้งหมด
- ช่วยหลีกเลี่ยงการเชื่อมโยง dependency ทั้งระบบโดยไม่ตั้งใจ (= ปัญหาป่า) และผลักดันให้เขียนโค้ดที่ใช้เฉพาะสิ่งจำเป็น
- แยกออกเป็นไมโครเซอร์วิสได้ง่าย เมื่อต้องการ
→ เพราะ dependency ส่วนใหญ่ถูกนิยามไว้อย่างชัดเจน
บทสรุป
- ข้อจำกัดด้านการออกแบบแพ็กเกจของ Go ไม่ใช่ข้อจำกัดที่น่ารำคาญ แต่เป็น กลไกที่ช่วยผลักดันสู่การออกแบบที่ดี
- แม้ไม่มีสถาปัตยกรรมพิเศษ ก็ยัง สร้างการออกแบบที่แข็งแรงได้จากโครงสร้างการอ้างอิงระหว่างแพ็กเกจเพียงอย่างเดียว
- การวิเคราะห์อย่างละเอียดและกลยุทธ์การรีแฟกเตอร์ สำหรับการอ้างอิงแบบวนซ้ำนั้นใช้ได้ผลไม่เฉพาะกับ Go แต่กับภาษาอื่นด้วย
4 ความคิดเห็น
ตอนเริ่มเขียนแบบรีบ ๆ จนมันพอรันได้ก็ดูสนุกดี
แต่พอเริ่มใส่เทสต์เข้าไป
ก็จะเริ่มย้อนคิดว่าตอนนั้นฉันทำแบบนั้นไปทำไม
คำว่า “อยากได้กล้วย แต่กลับยกทั้งป่ามาให้” นี่ตลกมากเลยนะ
ตอนพัฒนา Spring หนึ่งในเรื่องที่ยากที่สุดน่าจะเป็นการวนลูปของ dependency..
ความอึดอัดของการที่มัน initializе กันไปมาไม่รู้จบจนพังเพราะ memory leak...
ความคิดเห็นบน Hacker News
การไม่อนุญาตให้มีการพึ่งพาแบบวนกลับเป็นตัวเลือกด้านการออกแบบที่ยอดเยี่ยมเมื่อสร้างโปรแกรมขนาดใหญ่
เป็นบล็อกโพสต์ที่ยอดเยี่ยม
เทคนิคเสริมที่เกี่ยวกับคำแนะนำแบบ "ย้ายไปยังแพ็กเกจที่สาม"
เหมือนกำลังอ่านหนังสือเกี่ยวกับวิธีการเชิงโครงสร้างของ Yourdon อยู่
แพ็กเกจไม่สามารถอ้างอิงกันแบบวนกลับได้
ทำให้นึกถึงแนวคิดเฉพาะของ randomizer
จุดที่น่าสนใจของ Golang คือแม้จะมีการพึ่งพาแบบวนกลับในระดับแพ็กเกจไม่ได้ แต่ใน go.mod กลับทำได้
เป็นคำอธิบายที่ยอดเยี่ยมว่า Jerf มองเรื่องแพ็กเกจและจัดการกับการพึ่งพาแบบวนกลับอย่างไร