modular_architecture_on_ios

Không có gì ngạc nhiên khi giới thiệu một framework dịch vụ mới (ví dụ), cách làm tốt nhất là bắt đầu đơn giản. Ban đầu, chỉ cần một framework chính bao gồm cả các protocol, kiểu dữ liệu và phần hiện thực được trộn chung. Khi dần xuất hiện các trường hợp sử dụng lại một số phần public của service trong một framework khác cùng tầng, framework chính có thể được tách ra và các phần cốt lõi sẽ được chuyển vào một framework “core” riêng.

Framework core nên chỉ chứa các protocol thuần túy, định nghĩa kiểu và các đối tượng dữ liệu cơ bản nhằm thể hiện đúng phần “cốt lõi”. Lý tưởng nhất, framework core sẽ không chứa bất kỳ phần hiện thực cụ thể nào. Tuy nhiên, trong thực tế, đôi khi việc thêm một class trợ giúp hoặc một class nhỏ là điều không thể tránh khỏi.

Liên kết Core Framework và những lợi ích

Điểm tuyệt vời nhất của Core framework không chỉ nằm ở khả năng tái sử dụng trên cùng một tầng mà vẫn đảm bảo không gặp vấn đề khi biên dịch chéo, mà còn ở sự trừu tượng hóa trong liên kết (linking). Ngay khi Core framework tồn tại, không còn cần phải liên kết đến framework chính – vốn chứa nhiều phần hiện thực nặng nề – trong các tầng dịch vụ hay domain nữa. Thay vào đó, Core được sử dụng như một dependency cho các tầng cao hơn. Framework nào liên kết với Core sẽ chỉ phụ thuộc vào phần trừu tượng nhẹ của nó. Điều này mang lại thêm một vài lợi ích quan trọng.

Giảm thời gian biên dịch

Thời gian biên dịch có thể giảm đáng kể. Hãy cùng xem lý do tại sao. Kể từ khi các tầng rộng hơn như service hay domain chỉ còn phụ thuộc vào Core framework – vốn chỉ chứa các protocol và kiểu dữ liệu cơ bản – hệ thống build sẽ không cần phải biên dịch lại hoặc kiểm tra lại toàn bộ để đảm bảo tính ổn định của các framework phụ thuộc. Nếu có sự thay đổi trong phần hiện thực bên trong framework chính, thay đổi đó chỉ ảnh hưởng đến framework chính – vốn lý tưởng là chỉ được liên kết với ứng dụng chính để khởi tạo các đối tượng. Do đó, mọi thành phần khác trong Application Framework mà phụ thuộc vào Core framework sẽ không bị ảnh hưởng.

Hệ thống build, thay vì phải build lại toàn bộ cây phụ thuộc và kiểm tra đệ quy tất cả các dependency, thì chỉ cần biên dịch lại phần hiện thực. Điều này đã là một lợi ích lớn. Tuy nhiên, cần lưu ý rằng điều này chỉ đúng khi không có thay đổi nào trong phần core. Ví dụ, nếu một protocol được cập nhật trong module core, thì sẽ không tiết kiệm được thời gian biên dịch.

Đây cũng là lý do nữa để cần tập trung và thiết kế protocol một cách cẩn trọng – điều này càng trở nên quan trọng hơn ở các giai đoạn phát triển sau.

Giảm số lượng test cần chạy

Tương tự như thời gian biên dịch, số lượng bài test cần chạy để đảm bảo tính ổn định và “sức khỏe” của Application Framework cũng sẽ giảm. Trong ví dụ của chúng ta, nơi CosmonautService sử dụng SpacesuitServiceCore, CosmonautService không còn phụ thuộc vào các phần hiện thực nữa. Do đó, trong các bài test, thay vì làm việc với các class cụ thể, các stub và mock được tạo ra dựa trên protocol — và tại thời điểm này, chúng ta chắc chắn rằng các protocol đó không thay đổi.

Kết quả là, khi có thay đổi trong framework chính SpacesuitService, các bài test của CosmonautServiceTests sẽ không cần phải chạy lại, vì không có dependency trực tiếp giữa hai framework này. Các test chỉ cần chạy lại nếu SpacesuitServiceCore thay đổi.

Việc giảm liên kết đến các framework chính trong Application Framework sẽ giúp tăng tốc độ kiểm thử. Đặc biệt trong các dự án lớn, điều này mang lại rất nhiều lợi ích. Hãy tưởng tượng một dự án với hàng trăm framework, mỗi framework có hàng trăm hoặc thậm chí hàng ngàn bài test, tất cả đều cần chạy trước khi merge. Tối ưu hóa số lượng framework cần test trong mỗi pull request ở giai đoạn này là điều cực kỳ quan trọng.

Kiểm soát và đóng gói trong Framework

Một lợi ích tuy nhỏ hơn nhưng vẫn đáng chú ý là: các module sử dụng framework được liên kết sẽ không còn khả năng tự khởi tạo đối tượng. Vì trong Core framework không chứa bất kỳ phần hiện thực cụ thể nào, nên để khởi tạo đối tượng, client bắt buộc phải liên kết với framework chính.

Trường hợp này có thể cải thiện chất lượng mã nguồn rất nhiều. Vì mỗi client chỉ phụ thuộc vào các lớp trừu tượng (protocol), và ví dụ, chỉ có ứng dụng chính mới khởi tạo các đối tượng cụ thể, sau đó truyền xuống các tầng bên dưới hoặc đăng ký vào một hệ thống dependency injection — tất cả đều thông qua các protocol trừu tượng.

Điều này còn đảm bảo rằng các instance (thể hiện) của class được dùng một cách nhất quán xuyên suốt vòng đời của ứng dụng, thay vì để các module tạo mới tùy ý và dẫn đến việc có nhiều instance không cần thiết.

Nhược điểm của Core Framework

Cũng như mọi thứ trong ngành của chúng ta, Core framework cũng có những mặt hạn chế. Trước hết, đó là một framework bổ sung nữa cần được liên kết đúng cách trong Application Framework và cần được quản lý cẩn thận. Trong ví dụ dự án nhỏ thì điều này có thể khá đơn giản, nhưng đối với các dự án lớn, Application Framework có thể bao gồm hàng trăm framework và các phần core có thể làm tăng gấp đôi số lượng đó ở một số thời điểm.

Một điểm đáng lưu ý khác là thời gian khởi động ứng dụng. Không có gì ngạc nhiên khi việc sử dụng Core framework dẫn đến việc giới thiệu thêm các dynamic framework mới cần được liên kết và sao chép vào ứng dụng chính. Điều này khiến thời gian khởi động “lạnh” (cold start) trở nên chậm hơn, vì mỗi dynamic framework đều phải được load và mở khi ứng dụng khởi chạy — một quá trình tốn thời gian, đặc biệt trên các thiết bị đời cũ.

Mergeable Libraries – Giải pháp cứu cánh

Mergeable libraries đã được Apple giới thiệu tại WWDC2023 như một tính năng mới của trình biên dịch, cho phép gộp nhiều dynamic framework vào một framework chia sẻ duy nhất. Điều này giúp cải thiện đáng kể thời gian khởi động ứng dụng, đồng thời vẫn giữ được sự linh hoạt của cơ chế liên kết động (dynamic linking) cho các lập trình viên.

Tuy nhiên, tính đến hiện tại – gần một năm sau khi tính năng này được công bố – mergeable libraries vẫn còn gặp nhiều vấn đề. Chắc chắn rằng Apple sẽ dần khắc phục các lỗi này trong các bản cập nhật Xcode sắp tới, và khi đó, nhược điểm về thời gian khởi động chậm sẽ được cải thiện đáng kể.

Quy tắc cho Core Framework

Thông thường, một core framework không nên có quá nhiều dependencies. Tuy vậy, vẫn có thể liên kết đến các framework tầng thấp hơn — lý tưởng là dưới dạng đã được trừu tượng hóa bởi core framework, nhưng trong trường hợp pattern trừu tượng hóa chưa được áp dụng ở tầng thấp, thì việc liên kết trực tiếp đến phần hiện thực cụ thể vẫn có thể chấp nhận được.

Ngoài ra, một core framework cũng có thể liên kết đến một core framework khác cùng tầng. Trong ví dụ với CosmonautServiceCoreSpacesuitServiceCore, cả hai có thể liên kết lẫn nhau. Vì không có phần hiện thực cụ thể nào trong các core framework này, nên sẽ không phát sinh vấn đề. Tuy nhiên, trường hợp như vậy cần được đánh giá cẩn thận để tránh rối rắm trong kiến trúc và nguy cơ lỗi biên dịch khi xảy ra cross-linking.

Tuyệt đối không được phép liên kết framework ngược xuống dưới. Trong ví dụ, CosmonautServiceCore không bao giờ được phép liên kết đến NetworkService hoặc bất kỳ thành phần nào của nó, vì tầng thấp phải hoàn toàn không phụ thuộc (agnostic) vào tầng cao hơn.

Code Toàn Bug

Code nhiều bug nhưng biết cách giấu!

Leave a Reply

Your email address will not be published. Required fields are marked *