วันนี้ เรามีโค้ด TypeScript ที่ถูกคอมมิตแล้วประมาณ 1.1 ล้านไลน์ในโมโนรีโปของเรา การขยายขนาดนี้มาพร้อมกับความท้าทายหลากหลาย เช่น การตรวจสอบประเภทข้อมูลที่ช้า การนำเข้าไฟล์ที่มากเกินไป และการติดตามโค้ดที่ยากขึ้นเรื่อย ๆ ในช่วงไม่กี่ปีที่ผ่านมา เราได้สำรวจและปรับปรุงรูปแบบการออกแบบต่างๆ เพื่อขยายขนาดโค้ดเบสของเราให้ดียิ่งขึ้น รูปแบบหนึ่งที่พวกเราได้พบโดยบังเอิญช่วยบรรเทาปัญหาการจัดระเบียบโค้ดที่เพิ่มขึ้นของเราได้มากมาย: ระบบทะเบียน (Registries) มันเรียบง่ายและเป็นหนึ่งในรูปแบบที่ขยายขนาดได้ดีที่สุดที่เราได้นำมาใช้
โค้ดการจัดการเหตุการณ์ภายในต้นฉบับของเราเป็นตัวอย่างที่ดีของที่รูปแบบนี้สร้างความแตกต่างอย่างมาก มันเริ่มต้นอย่างง่าย แต่เมื่อเวลาผ่านไปก็กลายเป็นยูเนียนประเภทขนาดใหญ่ที่ทำให้การตรวจสอบประเภทช้าลง ไฟล์ขนาดใหญ่ที่นำเข้าโค้ดมากเกินไป และโค้ดที่ยากต่อการติดตามเนื่องจากการต่อสตริงมากเกินไป สุดท้าย มันยังสร้างความขัดแย้งให้กับนักพัฒนาในการบำรุงรักษาและเพิ่มตัวจัดการเหตุการณ์ใหม่
การออกแบบดั้งเดิม
ในระดับสูง บริการอีเวนต์ของเราถูกสร้างขึ้นบนระบบคิวข้อความ (MQ) อีเวนต์จะถูกบันทึกและส่งไปยัง MQ จากทุกที่ในระบบหลังบ้านของเรา (API pods, worker pods, เป็นต้น) และจะถูกประมวลผลโดยผู้ทำงานเพียงครั้งเดียวเท่านั้น
การออกแบบเองนั้นเรียบง่ายมาก ข้อจำกัดหลักเพียงอย่างเดียวคือทุกเหตุการณ์ต้องสามารถเรียงลำดับได้อย่างสมบูรณ์ผ่านสายสัญญาณ ความซับซ้อนและการบำรุงรักษาส่วนใหญ่มาจากความปลอดภัยของประเภทข้อมูลและประสบการณ์ของนักพัฒนา
1. กำหนดชุดของตัวช่วยพื้นฐาน:
2. กำหนดประเภทของเหตุการณ์ในไฟล์ที่ใช้ร่วมกัน
3. กำหนดตัวจัดการเหตุการณ์ไว้ในไฟล์ของตัวเอง
4. กำหนดไฟล์สรุป
5. กำหนดตัวจัดการเหตุการณ์
6. กำหนดฟังก์ชันที่ใช้ร่วมกันเพื่อบันทึกเหตุการณ์
7. ส่งออกฟังก์ชันที่ประมวลผลตัวจัดการเหตุการณ์สำหรับคิวในตัวทำงาน
เมื่อมองแวบแรก การออกแบบนี้ดูโอเคดี มันปลอดภัยทางประเภท เข้าใจง่าย ใช้งานสะดวก และชัดเจนเกี่ยวกับวิธีการสร้างเหตุการณ์ใหม่ อย่างไรก็ตาม มีปัญหาหลายประการ:
ความซับซ้อนของการตรวจสอบประเภท: สำหรับแต่ละเหตุการณ์ใหม่, แอปอีเวนต์ ประเภทจะใหญ่ขึ้น เมื่อพิจารณาเป็นรายบุคคล การรวมประเภทเดียวไม่ใช่ปัญหา แต่เมื่อการใช้งานรูปแบบนี้เพิ่มขึ้นทั่วทั้งโค้ดเบส (การรวมประเภทหลายรายการ) ประสิทธิภาพของการตรวจสอบประเภทจะลดลงอย่างรวดเร็ว
การโหลดโมดูลแบบกระตือรือร้น: รูปแบบของไฟล์ rollup ของเราทำให้เกือบทุกโมดูลนำเข้าโค้ดทั้งหมดตั้งแต่เริ่มต้น ซึ่งทำให้การโหลดแบบ lazy-loading เป็นไปไม่ได้ นอกจากนี้ยังทำให้เราไม่สามารถแยกเซิร์ฟเวอร์ออกเป็นชุดย่อยสำหรับการปรับใช้ที่แตกต่างกันได้อย่างง่ายดาย แม้ว่าการโหลดโค้ดทั้งหมดแบบ eager-loading จะยอมรับได้ในสภาพแวดล้อมการผลิต แต่มันส่งผลกระทบอย่างรุนแรงต่อการพัฒนาและการทดสอบ โดยใช้เวลาเกิน 20–30 วินาทีเพียงเพื่อเริ่มเซิร์ฟเวอร์สำหรับการพัฒนา
การติดตามย้อนกลับได้ต่ำ: การตอบคำถามง่าย ๆ เช่น "การดำเนินการของกิจกรรมนี้อยู่ที่ไหน?" หรือ "ตัวจัดการนี้ถูกเรียกใช้ที่ไหน?" เป็นเรื่องยาก เราต้องพึ่งพาการค้นหาข้อความเต็มของสตริงตัวอักษร ตัวอย่างเช่น หากวิศวกรตัดสินใจทำบางสิ่งเช่นนี้:
มันจะเป็นเรื่องยากมากที่จะติดตามว่า ส่งแล้ว และ สร้างสายแล้ว เหตุการณ์จะถูกกระตุ้นจากที่นี่ การค้นหาสตริง "wire_sent" ในโค้ดเบสจะไม่พบการใช้งานนี้ เนื่องจากชื่อถูกสร้างขึ้นแบบไดนามิก ส่งผลให้ข้อมูลนี้กลายเป็นความรู้เฉพาะกลุ่มที่คลุมเครือซึ่งอยู่ในหัวของวิศวกรเพียงไม่กี่คนเท่านั้น
การขาดขอบเขตของโดเมนที่ชัดเจน: เมื่อเราแนะนำอินเทอร์เฟซที่มีความเหมือนกันมากขึ้น (เช่น ตัวจัดการเหตุการณ์, เอนทิตีฐานข้อมูล, การตรวจสอบสถานะ) มันส่งเสริมให้มีการจัดวางอินเทอร์เฟซที่คล้ายกันไว้ในโฟลเดอร์เดียวกันแทนที่จะจัดกลุ่มตามตรรกะของโดเมน สิ่งนี้ทำให้ตรรกะทางธุรกิจของเราแตกเป็นเสี่ยง ๆ ส่งผลให้การสลับบริบทเฉพาะโดเมนเกิดขึ้นบ่อยและซับซ้อนมากขึ้น
การปรับปรุงการออกแบบนี้
ในการตั้งค่าข้างต้น เราได้วางตัวจัดการเหตุการณ์ทั้งหมดไว้ใน ./app-event-handlers/<event_type>.ts</event_type> ไฟล์. แม้ว่าการเก็บไฟล์ทั้งหมดไว้ในโฟลเดอร์เดียวจะช่วยให้ค้นหาได้ง่าย แต่ก็ไม่สะท้อนถึงวิธีการทำงานของเราจริง ๆ ในทางปฏิบัติ การจัดวางตัวจัดการเหตุการณ์ไว้กับตรรกะการทำงานที่เกี่ยวข้องของแอปพลิเคชันนั้น ๆ ปรากฏว่ามีประโยชน์มากกว่าการจัดกลุ่มไว้กับตัวจัดการอื่น ๆ
นั่นคือที่มาของแนวคิดในการเพิ่มส่วนขยายย่อยให้กับไฟล์ (.event-handler.ts) เข้ามา พวกเขาอนุญาตให้เราใช้โดเมนร่วมกันได้ ในขณะที่ยังคงสามารถค้นหาได้ง่ายโดยการดูส่วนต่อท้ายของชื่อไฟล์ ส่วนต่อท้ายของไฟล์ยังช่วยให้เราสามารถลบไฟล์รวมที่จัดการด้วยตนเองออกได้ เนื่องจากเราสามารถสแกนหาไฟล์ทั้งหมดที่ตรงกับส่วนต่อท้ายของชื่อไฟล์ในคลังข้อมูลได้ในขณะทำงาน
นี่คือเวอร์ชันย่อของรหัสทะเบียนฐานและวิธีการทำงาน โหลดโมดูล จะสแกนไฟล์ทั้งหมดและลงทะเบียนวัตถุที่ส่งออกทั้งหมดกับ $discriminator ทรัพย์สินที่ตรงกับสัญลักษณ์เดียวกันที่ถูกส่งเข้ามา สร้างทะเบียน.
ต่อไปนี้คือลักษณะของตัวจัดการเหตุการณ์ที่เราสร้างขึ้นโดยใช้ Registries:
1. กำหนดทะเบียนใน <name>.registry.ts</name> ไฟล์:
2. กำหนดตัวจัดการเหตุการณ์จริงใน .app-event-handler.ts ไฟล์
3. กำหนดฟังก์ชันที่ใช้ร่วมกันเพื่อบันทึกเหตุการณ์
4. ส่งออกฟังก์ชันที่ประมวลผลตัวจัดการเหตุการณ์สำหรับคิวในเวิร์กเกอร์
ข้อแตกต่างที่สำคัญที่ควรทราบ:
การตรวจสอบย้อนกลับของโค้ดดีขึ้นมาก: ทุกครั้งที่คุณบันทึกเหตุการณ์ คุณบันทึกมันแบบนี้:
ซึ่งหมายความว่าสามารถติดตามทุกสถานที่ได้อย่างง่ายดาย เหตุการณ์การ์ดสร้างขึ้น_Handler ใช้โดยใช้เครื่องมือ AST เช่น "ค้นหาการอ้างอิงทั้งหมด" (ใน VS Code) ในทางกลับกัน เมื่อคุณเห็น บันทึกเหตุการณ์ เรียก, คุณสามารถ "ไปที่การใช้งาน" ได้ในคลิกเดียวเพื่อค้นหาการกำหนดเหตุการณ์และผู้จัดการของมัน.
ไม่ต้องใช้การรวมประเภทอีกต่อไป: แต่เราใช้ประเภทพื้นฐาน ซึ่งเป็นสิ่งที่ TypeScript สนับสนุนให้หลีกเลี่ยงปัญหาด้านประสิทธิภาพที่เกิดจากการตรวจสอบประเภทข้อมูลซึ่งการรวมข้อมูลขนาดใหญ่ก่อให้เกิด
ตัวจัดการเหตุการณ์จะอยู่ร่วมกับตรรกะเฉพาะโดเมน: ตัวจัดการเหตุการณ์ของแอปจะไม่ถูกเก็บไว้ในโฟลเดอร์เดียวอีกต่อไป แต่จะถูกจัดเก็บไว้เคียงข้างกับตรรกะทางธุรกิจที่เกี่ยวข้องแทน ตัวอย่างเช่น บริการเฉพาะโดเมนอาจมีลักษณะดังนี้:
การทำงานกับทะเบียนในวันนี้
วันนี้ เราทำงานร่วมกับทะเบียนหลายสิบแห่งเพื่อให้แน่ใจว่าโค้ดทั้งหมดถูกจัดเก็บไว้ร่วมกับตรรกะของแอปพลิเคชันของพวกเขา บางแห่งที่น่าสนใจได้แก่:
.db.tsสำหรับการลงทะเบียนเอนทิตีฐานข้อมูล.workflows.tsและกิจกรรม.tsสำหรับการลงทะเบียน ชั่วคราว เวิร์กโฟลว์.ตรวจสอบ.tsสำหรับการลงทะเบียนการตรวจสุขภาพ (โพสต์บล็อก).main.tsสำหรับการลงทะเบียนบริการที่รวบรวมตรรกะทางธุรกิจเฉพาะโดเมนเข้าด้วยกัน.permission-role.tsและ.permission-key.tsสำหรับการกำหนดสิทธิ์ RBAC ในผลิตภัณฑ์ของเรา.กล่องจดหมาย.tsสำหรับการลงทะเบียนตัวจัดการที่แยกวิเคราะห์อีเมลในบัญชี Gmail.cron.tsสำหรับการลงทะเบียนงาน cron.ledger-balance.tsสำหรับการกำหนด "บัญชีแยกประเภท" ภายในทางการเงินของเรา.เมตริกส์.tsสำหรับการกำหนดเมตริกของ Datadog
และส่วนขยายเฉพาะโดเมนอื่น ๆ อีกหลายรายการ
ณ จุดนี้ เรายังไม่ได้เปิดเผยรูปแบบนี้แบบโอเพนซอร์ส แต่หวังว่าโพสต์นี้จะให้แนวคิดที่ชัดเจนเกี่ยวกับวิธีการนำไปใช้ในโค้ดเบสอื่นๆ หากคุณพบว่าข้อมูลนี้มีประโยชน์ ลองนำไปใช้ในโปรเจกต์ของคุณเองและแจ้งให้เราทราบผลลัพธ์ด้วย!