Hiện tại, chúng tôi có khoảng 1,1 triệu dòng mã TypeScript đã được cam kết trong kho mã đơn (monorepo) của mình. Việc mở rộng quy mô đã mang lại nhiều thách thức như kiểm tra kiểu chậm, các import phình to và khả năng theo dõi mã ngày càng kém. Trong vài năm qua, chúng tôi đã nghiên cứu và thử nghiệm nhiều mẫu thiết kế khác nhau để cải thiện khả năng mở rộng của kho mã. Một mẫu thiết kế mà chúng tôi tình cờ phát hiện ra đã giúp giảm bớt nhiều vấn đề về tổ chức mã nguồn ngày càng phức tạp: Registries. Đây là một mẫu thiết kế đơn giản và là một trong những mẫu thiết kế có khả năng mở rộng nhất mà chúng tôi đã áp dụng.
Mã xử lý sự kiện nội bộ ban đầu của chúng tôi là một ví dụ điển hình về nơi mẫu thiết kế này đã tạo ra sự khác biệt lớn. Ban đầu nó rất đơn giản, nhưng theo thời gian đã dẫn đến các kiểu dữ liệu hợp nhất khổng lồ gây chậm quá trình kiểm tra kiểu, các tệp barrel nhập quá nhiều mã, và mã khó theo dõi do việc nối chuỗi quá mức. Cuối cùng, nó gây ra sự cản trở cho nhà phát triển trong việc duy trì và thêm các trình xử lý sự kiện mới.
Thiết kế gốc
Ở mức cao, dịch vụ sự kiện của chúng tôi được xây dựng dựa trên một hàng đợi tin nhắn (MQ). Các sự kiện được ghi lại và gửi đến MQ từ bất kỳ đâu trong hệ thống backend của chúng tôi (API pods, worker pods, v.v.) và được xử lý bởi một worker chính xác một lần.
Thiết kế này rất đơn giản; yêu cầu chính duy nhất là tất cả các sự kiện phải được truyền tải một cách tuần tự qua mạng. Hầu hết sự phức tạp và công việc bảo trì xuất phát từ tính an toàn kiểu dữ liệu và trải nghiệm của nhà phát triển.
1. Xác định một tập hợp các hàm trợ giúp cơ bản:
2. Định nghĩa các loại sự kiện trong một tệp chia sẻ.
3. Định nghĩa các trình xử lý sự kiện trong tệp riêng của chúng.
4. Định nghĩa tệp tổng hợp
5. Định nghĩa các trình xử lý sự kiện
6. Định nghĩa một hàm chung để ghi lại các sự kiện.
7. Xuất khẩu một hàm xử lý trình xử lý sự kiện cho hàng đợi trong một công nhân.
Nhìn chung, thiết kế này trông khá ổn. Nó an toàn về kiểu dữ liệu, dễ hiểu, dễ làm việc và rõ ràng về cách tạo sự kiện mới. Tuy nhiên, có một số vấn đề:
Độ phức tạp của kiểm tra kiểu: Đối với mỗi sự kiện mới, Sự kiện Ứng dụng Loại dữ liệu trở nên phức tạp hơn. Mỗi liên hợp loại dữ liệu riêng lẻ không gây vấn đề, nhưng khi việc sử dụng mẫu này tăng lên trong toàn bộ mã nguồn (nhiều liên hợp loại dữ liệu), hiệu suất kiểm tra loại dữ liệu nhanh chóng suy giảm.
Tải mô-đun nhanh chóng: Mô hình tệp rollup của chúng tôi khiến gần như mọi mô-đun đều nhập toàn bộ mã nguồn khi khởi động, khiến việc tải chậm (lazy-loading) trở nên bất khả thi. Điều này cũng ngăn cản chúng tôi chia tách máy chủ thành các gói riêng biệt cho các triển khai khác nhau. Mặc dù việc tải toàn bộ mã nguồn (eager-loading) là chấp nhận được trong môi trường sản xuất, nó đã ảnh hưởng nghiêm trọng đến quá trình phát triển và kiểm thử, mất hơn 20–30 giây chỉ để khởi động máy chủ phát triển.
Khả năng truy xuất nguồn gốc kém: Trả lời các câu hỏi đơn giản như "sự kiện này được thực thi ở đâu?" hoặc "hàm xử lý này được gọi ở đâu?" là rất khó khăn. Chúng tôi phải dựa vào việc tìm kiếm toàn văn bản của các chuỗi văn bản. Ví dụ, nếu một kỹ sư quyết định làm điều gì đó như sau:
Sẽ rất khó để xác định rằng dây đã gửi và dây_được_tạo Các sự kiện được kích hoạt từ đây. Việc tìm kiếm chuỗi "wire_sent" trong mã nguồn sẽ không hiển thị cách sử dụng này, vì tên được tạo động. Kết quả là, thông tin này trở thành kiến thức "bí truyền" chỉ được một số ít kỹ sư biết đến.
Thiếu ranh giới rõ ràng giữa các miền: Khi chúng tôi giới thiệu các giao diện đồng nhất hơn (người xử lý sự kiện, thực thể cơ sở dữ liệu, kiểm tra sức khỏe), điều này khuyến khích việc đặt các giao diện tương tự vào cùng một thư mục thay vì nhóm theo logic miền. Điều này đã làm phân mảnh logic kinh doanh của chúng tôi, khiến việc chuyển đổi ngữ cảnh theo miền trở nên thường xuyên và phức tạp hơn.
Tiếp tục hoàn thiện thiết kế này
Trong cấu hình trên, chúng tôi đặt tất cả các trình xử lý sự kiện vào ./app-event-handlers/<event_type>.ts</event_type> Tệp tin. Mặc dù việc lưu trữ tất cả tệp tin trong một thư mục giúp việc tìm kiếm trở nên dễ dàng, nhưng điều này không phản ánh cách chúng ta thực sự làm việc. Trên thực tế, việc đặt các trình xử lý sự kiện cùng với phần logic ứng dụng liên quan đã chứng minh là hữu ích hơn nhiều so với việc nhóm chúng với các trình xử lý khác.
Đó là nơi nảy sinh ý tưởng thêm các phần mở rộng con vào tệp (.event-handler.ts) đã được triển khai. Họ cho phép chúng tôi chia sẻ không gian lưu trữ theo tên miền đồng thời vẫn cho phép tìm kiếm dễ dàng bằng cách tra cứu phần mở rộng. Phần mở rộng tệp cũng cho phép chúng tôi loại bỏ các tệp tổng hợp được quản lý thủ công vì chúng tôi có thể quét tất cả các tệp khớp với phần mở rộng trong kho lưu trữ tại thời điểm chạy.
Dưới đây là phiên bản tóm tắt của mã đăng ký cơ sở và cách thức hoạt động của nó. Tải các mô-đun Sẽ quét tất cả các tệp và đăng ký tất cả các đối tượng được xuất với một $phân biệt Tài sản khớp với cùng biểu tượng được truyền vào Tạo sổ đăng ký.
Dưới đây là cách xây dựng trình xử lý sự kiện của chúng ta bằng cách sử dụng Registries:
1. Định nghĩa một sổ đăng ký trong một <name>.registry.ts</name> tệp:
2. Định nghĩa các trình xử lý sự kiện thực tế trong .app-event-handler.ts tệp tin
3. Định nghĩa một hàm chung để ghi lại các sự kiện.
4. Xuất khẩu một hàm xử lý trình xử lý sự kiện cho hàng đợi trong một công nhân.
Một số điểm khác biệt quan trọng cần lưu ý:
Khả năng truy vết mã nguồn được cải thiện đáng kể.: Bất cứ khi nào bạn ghi lại một sự kiện, bạn ghi lại nó như sau:
Điều này có nghĩa là việc theo dõi tất cả các vị trí nơi Xử lý sự kiện được tạo bởi thẻ được sử dụng bằng cách sử dụng các công cụ AST như "Tìm tất cả các tham chiếu" (trong VS Code). Ngược lại, khi bạn thấy một Ghi lại sự kiện Khi gọi, bạn có thể nhấp vào "Đi đến triển khai" để tìm định nghĩa sự kiện và trình xử lý của nó.
Không còn các liên minh ngành nghềThay vào đó, chúng tôi đang sử dụng các loại cơ sở, đây là một khái niệm mà... TypeScript Khuyến khích tránh các vấn đề về hiệu suất do việc kiểm tra kiểu gây ra trong các union lớn.
Các trình xử lý sự kiện được đặt cùng với logic cụ thể của miền.Các trình xử lý sự kiện ứng dụng không còn được lưu trữ trong một thư mục duy nhất. Thay vào đó, chúng được đặt cùng với logic kinh doanh liên quan. Ví dụ, một dịch vụ chuyên biệt cho một miền có thể trông giống như sau:
Làm việc với các cơ quan đăng ký hiện nay
Hiện nay, chúng tôi hợp tác với hàng chục hệ thống đăng ký để đảm bảo rằng tất cả mã nguồn được lưu trữ cùng với logic ứng dụng của họ. Một số hệ thống đáng chú ý bao gồm:
.db.tsĐể đăng ký các thực thể cơ sở dữ liệu.workflows.tsvà.hoạt động.tsĐể đăng ký Thời gian Quy trình làm việc.kiểm tra.tsĐể đăng ký kiểm tra sức khỏe (bài đăng trên blog).main.tsĐể đăng ký các dịch vụ nhóm lại các logic kinh doanh chuyên biệt theo lĩnh vực..quyền-vai-trò.tsvà.permission-key.tsĐể định nghĩa quyền RBAC trong sản phẩm của chúng tôi.email-box.tsĐể đăng ký các trình xử lý (handlers) phân tích email trong tài khoản Gmail..cron.tsĐể đăng ký các tác vụ cron.sổ cái - số dư.tsĐể định nghĩa nguyên thủy sổ cái tài chính nội bộ của chúng ta..metrics.tsĐể định nghĩa các chỉ số Datadog
và một số tiện ích mở rộng chuyên dụng khác.
Tại thời điểm này, chúng tôi chưa công bố mã nguồn mở cho mẫu thiết kế này, nhưng hy vọng bài viết này sẽ cung cấp một cái nhìn rõ ràng về cách nó có thể được triển khai trong các dự án khác. Nếu bạn thấy điều này hữu ích, hãy thử triển khai nó trong các dự án của riêng bạn và cho chúng tôi biết kết quả như thế nào!