0

Triển khai ứng dụng IOS theo kiến trúc Monolith Modular

Mở đầu

Phần lớn các ứng dụng iOS ban đầu đều được xây dựng với toàn bộ source code nằm trong một Xcode project duy nhất. Cách làm này khá đơn giản, dễ bắt đầu và phù hợp khi ứng dụng còn nhỏ, team chỉ có một vài người. Mọi thứ nằm chung một chỗ, build nhanh, debug cũng không quá phức tạp.

Tuy nhiên, khi ứng dụng lớn dần lên, thêm nhiều feature mới, nhiều màn hình hơn, và đặc biệt là nhiều developer cùng tham gia phát triển, mô hình monolith truyền thống bắt đầu bộc lộ không ít vấn đề. Code ngày càng khó kiểm soát, các module phụ thuộc chằng chịt vào nhau, chỉ cần chỉnh sửa một feature nhỏ cũng có thể ảnh hưởng đến những phần tưởng chừng không liên quan. Việc merge code trở nên căng thẳng hơn, build time tăng lên, và đôi khi developer phải mất khá nhiều thời gian chỉ để hiểu xem đoạn code mình đang sửa nằm ở đâu trong tổng thể ứng dụng.

Chính trong bối cảnh đó, nhu cầu tách nhỏ ứng dụng theo feature, giảm coupling và tăng khả năng mở rộng trở nên rõ ràng hơn bao giờ hết và đây cũng là lúc các mô hình kiến trúc như Modular Monolith bắt đầu phát huy tác dụng.

Monolith Modular là gì

Có thể nhiều anh em đã tiếp xúc với cách triển khai này với cái tên Micro Frontend lấy cảm hứng từ mô hình Micro Frontend của anh em Web, tuy nhiên theo quan điểm của mình thì cách triển khai này giống với khái niệm Monolith Modular hơn là Micro Frontend. Vậy Monolith modular là gì. Để giải thích 1 cách đơn giản nhất thì Modular Monolith vẫn là một ứng dụng duy nhất:

  • Một app
  • Một lifecycle
  • Một IPA duy nhất khi build và release

Điểm khác biệt nằm ở chỗ bên trong ứng dụng được chia nhỏ thành nhiều module rõ ràng thay vì gom toàn bộ code vào 1 khối lớn như monolith truyền thống. Để dễ hình dung hơn, mình tóm tắt sự khác nhau giữa monolith truyền thống, Modular Monolith trên iOS và Micro Frontend trên web trong bảng dưới đây:

Tiêu chí Monolith truyền thống Modular Monolith (iOS) Micro Frontend (Web)
Số lượng ứng dụng 1 ứng dụng 1 ứng dụng Nhiều frontend độc lập
Cách build Build toàn bộ app Build toàn bộ app Build từng frontend riêng
Cách deploy Deploy toàn bộ app Deploy toàn bộ app Deploy độc lập từng frontend
Binary / Bundle 1 binary 1 binary Nhiều bundle / artifact
Chia theo feature Không rõ ràng Rõ ràng theo feature Rõ ràng theo feature
Feature ownership Mờ Rõ ràng theo team Rõ ràng, mỗi team sở hữu
Giao tiếp giữa feature Gọi trực tiếp code Protocol / interface / App layer Event, API, shared contract
Coupling Cao Thấp Rất thấp
Boundary Mờ Rõ ràng Rất rõ
Khả năng release độc lập
Phù hợp multi-team
Độ phức tạp triển khai Thấp Trung bình Cao
Mục tiêu chính Làm nhanh, đơn giản Scale app & team Scale độc lập theo team

Có thể thấy, Modular Monolith là điểm cân bằng hợp lý cho iOS: giữ được sự đơn giản trong build và release, nhưng vẫn áp dụng được tư duy chia tách theo feature và ownership giống Micro Frontend. Trong khi đó, Micro Frontend “thuần” chỉ thực sự phát huy hiệu quả trên web, nơi nền tảng cho phép deploy và load từng frontend một cách độc lập.

Triển khai trong thực tế

Trong thực tế, mỗi team có thể sẽ triển khai kiến trúc này theo những concept khác nhau, trong nội dung bài viết này mình sẽ trình bày cách mà team mình đã sử dụng.
Toàn bộ dự án sẽ được chia thành nhiều feature, mỗi feature sẽ là một module độc lập, được kết nối với nhau thông qua 1 trung tâm mình tạm gọi là AppCoordinator. Thay vì để các feature gọi trực tiếp lẫn nhau như trong monolith truyền thống, mọi luồng khởi tạo, điều hướng và dependency injection đều được gom về AppCoordinator. Điều này giúp giữ cho các feature “biết ít nhất có thể” về phần còn lại của hệ thống. Ngoài các module feature chính thì sẽ có thêm các module ở các layer khác đảm nhận các nhiệm vụ khác như networking, handle business logic, ... Mọi người có thể xem sơ đồ dưới đây để có thể hình dung rõ hơn về cách các module phụ thuộc lẫn nhau trong kiến trúc này. Sau đó mình sẽ giải thích cụ thể về nhiệm vụ của từng module.

1. App Coordinator

Đóng vai trò là Composition Root của app, đây là nơi đảm nhiệm các vai trò:

  • Khởi tạo vòng đời ứng dụng (AppDelegate / SceneDelegate)
  • Thiết lập Dependency Injection
  • Điều phối navigation giữa các feature
  • ...

2. Core / Shared

Module Core giữ vai trò giống như tên gọi của nó, đây là module chứa các thành phần core của app, các base class như BaseViewController, BaseViewModel, BaseRouter, Interface của các feature, ... sẽ được define trong module này. Có thể hiểu đây là xương sống của kiến trúc mà app định triển khai.
Đối với Shared thì đây cũng là một module chứa các thành phần dùng chung trong nhiều module khác tuy nhiên những thứ đặt ở đây sẽ mang tính helper nhiều hơn ví dụ như các extension cơ bản như format datetime hay các biến constant global sẽ được lưu trữ ở đây. Để quyết định 1 class hay đoạn code nên đặt ở module nào, mình thường follow 1 checklist câu hỏi dưới đây
Có logic kiến trúc / flow app? → Core
Có protocol giao tiếp giữa module? → Core
Chỉ là helper / extension? → Shared
Chỉ liên quan UI / layout? → Shared
Ảnh hưởng toàn app? → Core

3. Domain

Domain sẽ là module chứa các khái niệm cốt lõi và các business rule của hệ thống, thứ mà nhiều module khác sẽ dùng chung. Domain module không chứa UI, không xử lý navigation, và cũng không phụ thuộc vào bất cứ third party library hay module nào khác trong dự án. Thứ tồn tại trong domain sẽ là:

  • Entity / Model mang ý nghĩa nghiệp vụ (User, Account, Transaction, …)
  • Use case ở mức abstraction cao
  • Business rule thuần Swift, có thể test độc lập

4. Data

Data đóng vai trò là tầng triển khai cụ thể cho các abstraction được định nghĩa ở Domain. Nếu Domain trả lời câu hỏi “hệ thống cần làm gì” thì Data trả lời câu hỏi “làm bằng cách nào và lấy dữ liệu từ đâu”. Data module chịu trách nhiệm làm việc với tất cả các nguồn dữ liệu của ứng dụng, bao gồm:

  • Remote API (REST, GraphQL, gRPC, …)
  • Local database
  • Cache (memory / disk)
  • Secure storage (Keychain, encrypted storage)

Data module sẽ không để lộ chi tiết triển khai với các module khác. Các module FeatureDomain sẽ không được depend trực tiếp vào Data, các implementation trong Data sẽ được inject vào trong feature thông qua App Coordinator

5. Feature

Mỗi feature (Login, Dashboard, Payment, ...) sẽ là một module riêng lẻ chịu trách nhiệm độc lập cho một nhóm tính năng trong app. Đây sẽ là nơi các màn hình của app được implement và xử lý tương tác của người dùng. Bên trong mỗi feature, team hoàn toàn có thể áp dụng bất kỳ architectural pattern nào phù hợp như MVC, MVP, MVVM, VIPER…; việc lựa chọn Modular Monolith chỉ ảnh hưởng đến cách tổ chức và kết nối các module, hoàn toàn không xung đột với kiến trúc nội bộ của từng feature.

Có một nguyên tắc là feature không nên biết lẫn nhau, một feature sẽ không biết và không thể import các feature còn lại. Feature sẽ chỉ có thể import Domain, Core, Shared và các third party trong Shared. Nếu cần chuyển flow giữa các feature, feature hiện tại sẽ callback lại cho app coordinator, việc điều hướng sẽ do app coordinator thực hiện. Cách làm này giúp feature giữ được boundary rõ ràng, dễ test, dễ refactor và ít rủi ro khi thay đổi.

Ưu nhược điểm

Bên trên mình đã giải thích sơ bộ về cách một ứng dụng vận hành khi triển khai theo hướng Monolith Modular và thực tế là dự án mình đang thực hiện cũng đã triển khai theo hướng nay trong hơn 2 năm nay. Tất nhiên mô hình nào cũng tồn tại tại ưu và nhược điểm của nó. Sau một thời gian làm việc với mô hình này, mình có thể rút ra được một số ưu nhược điểm như sau.

Ưu điểm

  1. Boundary rõ ràng, code dễ kiểm soát hơn Việc chia app thành các module theo feature giúp ranh giới trách nhiệm trở nên rõ ràng. Mỗi feature biết mình làm gì và không can thiệp vào phần việc của feature khác, từ đó giảm đáng kể tình trạng coupling ngầm giữa các feature.
  2. Dễ scale team và phát triển song song. Đối với mình thì đây chính là điểm ăn tiền nhất của kiến trúc này. Mỗi feature có thể được một team hoặc một nhóm nhỏ ownership. Các team có thể làm việc song song trên cùng một codebase mà ít va chạm, giảm conflict khi merge và tăng tốc độ phát triển. Bản thân mình khi làm việc với kiến trúc này thì rất hiếm khi mình gặp các conflict lớn trong file .pbxproj - điều mà rất hay gặp trong kiến trúc Monolith thuần khi có nhiều team thêm nhiều file cùng lúc để tạo feature mới.
  3. Dễ kiểm soát phạm vi ảnh hưởng khi thay đổi. Khi một feature được đóng gói tốt, việc thay đổi hoặc refactor chỉ ảnh hưởng trong phạm vi module đó. Điều này giúp giảm rủi ro khi sửa code, đặc biệt quan trọng với các feature nhạy cảm.

Nhược điểm

  1. Effort ban đầu khi khởi tạo project sẽ cao hơn, đòi hỏi team phải đầu tư thời gian vào việc thiết kế module, interface và dependency ngay từ đầu. Nếu không làm tốt có thể sẽ gặp 1 số vấn đề không thể giải quyết khi app scale.
  2. Không phù hợp với app nhỏ, cần ưu tiên tốc độ khi phát triển.
  3. Việc debug giữa nhiều module có thể sẽ gặp khó khăn
  4. Git flow phức tạp. Bình thường mỗi feature chỉ cần tạo 1 pull request tuy nhiên với kiến trúc này thì mỗi feature hoàn thiện sẽ cần tạo nhiều pull request do cần update nhiều module khác nhau, mình đã từng phải tạo tới 9 pull request khác nhau cho 1 feature T_T. Do đó nếu không quản lý tốt thì việc merge thiếu code lên môi trường live hoàn toàn có thể xảy ra.

Kết

Monolith Modular không làm ứng dụng của bạn trở nên “xịn” hơn ngay lập tức, nhưng nó tạo ra một nền tảng đủ vững để app có thể phát triển lâu dài, đặc biệt trong bối cảnh nhiều team, nhiều feature và yêu cầu ổn định cao. Với team mình, đây là một lựa chọn đủ thực tế, đủ an toàn và đủ linh hoạt cho một ứng dụng iOS quy mô lớn. Nếu các bạn có hứng thú với mô hình này, mình sẽ viết một bài khác hướng dẫn chi tiết cách triển khai trong các bài viết tiếp theo.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí