SoC (Separation of Concerns) là chữ viết tắt của nguyên tắc mà theo đó hệ thống phần mềm được chia thành các lĩnh vực trách nhiệm độc lập. Theo Martin Fowler, phân chia mối quan tâm là yếu tố then chốt của mã nguồn dễ bảo trì. Nguyên tắc SoC cho phép nhà phát triển thay đổi một tầng của ứng dụng mà không ảnh hưởng đến các tầng khác, điều đặc biệt quan trọng trong phát triển di động nhóm.
Những điểm chính
SoC là viết tắt của Separation of Concerns — «phân chia trách nhiệm» hoặc «phân chia lĩnh vực quan tâm». Trong bối cảnh phát triển, thuật ngữ concern chỉ bất kỳ chức năng nào có thể tách rời: hiển thị giao diện người dùng, xử lý nhấp chuột, xác thực dữ liệu, giao tiếp mạng hoặc làm việc với cơ sở dữ liệu. Nguyên tắc SoC yêu cầu nhóm mã nguồn xung quanh các lĩnh vực này để thay đổi trong một lĩnh vực không ảnh hưởng đến các lĩnh vực khác.
Chữ viết tắt SoC được sử dụng rộng rãi trong tài liệu kỹ thuật, thảo luận kiến trúc và tài liệu framework. Ví dụ, trong tài liệu Android Architecture Components, SoC được nhắc đến nhiều lần như động lực để tách ViewModel và View. Trong cộng đồng iOS, thuật ngữ này được sử dụng khi thảo luận về vấn đề Massive View Controller — hậu quả trực tiếp của việc thiếu SoC.
Điều quan trọng là hiểu rằng SoC không phải là hành động một lần mà là một quá trình liên tục. Khi ứng dụng phát triển, các lĩnh vực trách nhiệm mới xuất hiện và kiến trúc phải được xem xét lại. Một cơ sở mã tốt trải qua nhiều lần lặp phân chia trước khi đạt đến trạng thái ổn định nơi mỗi concern được cô lập và quản lý được.
Separation of Concerns và chữ viết tắt SoC của nó chỉ cùng một nguyên tắc. Sự khác biệt chỉ nằm ở ngữ cảnh sử dụng: tên đầy đủ được dùng trong tài liệu chính thức, tài liệu giáo dục và khi giải thích khái niệm lần đầu cho nhà phát triển mới. SoC thuận tiện trong thảo luận kỹ thuật, đánh giá mã và tài liệu nơi sự ngắn gọn quan trọng.
Trong môi trường chuyên nghiệp, cả hai thuật ngữ có thể thay thế cho nhau. Một nhà phát triển có thể nói «ở đây SoC bị vi phạm» hoặc «điều này vi phạm Separation of Concerns» — ý nghĩa không thay đổi. Tuy nhiên, trong tin tuyển dụng và yêu cầu kiến trúc, tên đầy đủ thường được sử dụng hơn, trong khi trong chat và đánh giá mã, chữ viết tắt được dùng. Biết cả hai biến thể là cần thiết để gia nhập ngành một cách thoải mái.
Có sự nhầm lẫn thuật ngữ: chữ viết tắt SoC cũng được sử dụng trong ngữ cảnh phần cứng cho System-on-a-Chip (hệ thống trên chip). Trong phát triển di động, ngữ cảnh luôn rõ ràng từ môi trường — nếu thảo luận về kiến trúc mã nguồn, nó đề cập đến Separation of Concerns. Trong bài viết này, SoC luôn đề cập đến nguyên tắc phân chia trách nhiệm.
Kiến trúc ba tầng là cách phổ biến nhất để triển khai SoC trong ứng dụng di động. Nó chia mã nguồn thành Presentation (UI), Domain (logic nghiệp vụ) và Data (nguồn dữ liệu). Mỗi tầng chứa các loại lớp được định nghĩa chặt chẽ và được cô lập khỏi các tầng lân cận thông qua interface. Cách tiếp cận này hiệu quả như nhau cho các dự án iOS, Android và Flutter.
View và ViewModel tạo thành tầng trình bày. View chịu trách nhiệm hiển thị giao diện và chuyển tiếp sự kiện người dùng. ViewModel lưu trữ trạng thái màn hình và chuyển đổi dữ liệu từ tầng Domain thành định dạng sẵn sàng hiển thị. ViewModel không có tham chiếu đến Activity, Fragment hay UIViewController — điều này đảm bảo SoC giữa UI và logic.
Ví dụ, trong Android Jetpack, ViewModel tồn tại qua vòng xoay màn hình trong khi UI được tạo lại. Nếu không có SoC, phải lưu trạng thái trong Activity, trộn lẫn quản lý vòng đời với dữ liệu. ViewModel giải quyết vấn đề này một cách cô lập, thể hiện một triển khai sạch sẽ của nguyên tắc phân chia trách nhiệm.
Use Cases chứa các quy tắc nghiệp vụ độc lập với nền tảng. Tầng này không import Android SDK, iOS UIKit hay Flutter framework. Use Case nhận dữ liệu từ Repository, áp dụng logic nghiệp vụ và trả về kết quả. Nhờ SoC, một Use Case có thể được tái sử dụng trên nhiều màn hình và nền tảng khác nhau.
Một ví dụ kinh điển là ValidateAndSaveUseCase cho biểu mẫu đăng ký. Nó xác thực email và mật khẩu, gọi UserRepository để lưu và trả về ValidationResult. Cả UI và cơ sở dữ liệu đều không biết các quy tắc xác thực — chúng tập trung tại một nơi, giúp dễ dàng thay đổi.
Repository trừu tượng hóa các nguồn dữ liệu khỏi phần còn lại của ứng dụng. ViewModel không biết dữ liệu đến từ đâu — REST API, GraphQL, cơ sở dữ liệu cục bộ hay bộ nhớ đệm. Repository quyết định sử dụng nguồn nào và ẩn logic này đằng sau một interface. Đây là SoC giữa việc lấy dữ liệu và tiêu thụ dữ liệu.
DataSource cung cấp sự phân chia sâu hơn: RemoteDataSource chỉ chịu trách nhiệm cho các yêu cầu HTTP, LocalDataSource chịu trách nhiệm làm việc với Room, CoreData hoặc SharedPreferences. Repository kết hợp chúng bằng cách áp dụng các chiến lược lưu đệm. Mỗi DataSource có thể được thay thế độc lập, điều rất quan trọng khi di chuyển giữa các máy chủ hoặc cơ sở dữ liệu.
Hệ thống DataSource đa tầng này triển khai SoC ở cấp độ cơ sở hạ tầng: giao tiếp mạng, lưu trữ cục bộ và lưu đệm là các concerns riêng biệt, mỗi cái có logic và vòng đời riêng. Khi thay thế một HTTP client, chỉ RemoteDataSource thay đổi, trong khi Repository và các tầng trên vẫn không bị ảnh hưởng, xác nhận giá trị thực tế của việc phân chia trách nhiệm.
MVP (Model-View-Presenter) là một trong những mẫu đầu tiên triển khai SoC một cách rõ ràng trong phát triển di động. Presenter chứa logic và điều khiển View thông qua một interface. View thụ động — nó chỉ hiển thị những gì Presenter bảo nó. Sự phân chia đơn giản hóa việc kiểm thử: Presenter được kiểm thử mà không cần trình giả lập và View đơn giản đến mức không có gì để hỏng.
MVVM đã thêm liên kết phản ứng: View đăng ký thay đổi của ViewModel thông qua Observable hoặc StateFlow. ViewModel không giữ tham chiếu đến View, loại bỏ rủi ro rò rỉ bộ nhớ và phân chia concerns mạnh mẽ hơn. Trên Android, MVVM trở thành tiêu chuẩn nhờ Jetpack ViewModel và LiveData; trên iOS — nhờ Combine và RxSwift.
Clean Architecture của Robert Martin đưa SoC đến sự phân chia triệt để thành các vòng. Vòng ngoài (framework và trình điều khiển) phụ thuộc vào vòng trong (thực thể), nhưng không ngược lại. Trong thực tế, các dự án di động hiếm khi triển khai cả bốn vòng — các tầng Domain và Data xung quanh Presentation là đủ. Nhưng nguyên tắc «phụ thuộc vào bên trong» mang lại lợi thế đáng kể khi thay đổi framework.
// View — chỉ hiển thị, không có logic
final class LoginViewController: UIViewController {
let viewModel: LoginViewModel
func loginTapped() {
viewModel.login(emailField.text, passwordField.text)
}
}
// ViewModel — chứa logic màn hình, không biết UIKit
final class LoginViewModel {
private let loginUseCase: LoginUseCase
func login(email: String?, password: String?) {
loginUseCase.execute(email, password)
}
}
// Use Case — logic nghiệp vụ, độc lập nền tảng
final class LoginUseCase {
private let repo: AuthRepository
func execute(email: String?, password: String?) {
guard let e = email, let p = password else { return }
repo.authenticate(e, p)
}
}
Ví dụ cho thấy ba cấp độ của SoC: LoginViewController chỉ chuyển tiếp sự kiện, LoginViewModel quản lý trạng thái, LoginUseCase chứa các quy tắc nghiệp vụ. Mỗi lớp được kiểm thử độc lập và việc thay đổi framework UI không ảnh hưởng đến Use Case.
Massive View Controller là vi phạm SoC phổ biến nhất trong iOS. Một lớp quản lý UI, xử lý yêu cầu mạng, phân tích JSON và lưu trữ dữ liệu vi phạm nguyên tắc ở mọi cấp độ. Giải pháp là trích xuất mỗi trách nhiệm vào một thành phần riêng biệt: NetworkingService, JSONParser, CoreDataStack, để lại ViewController chỉ quản lý View.
Trong Android, vấn đề tương tự là God Activity hoặc God Fragment. Một activity tải dữ liệu, xác thực biểu mẫu, hiển thị hộp thoại và cập nhật UI. Nó được khắc phục bằng cách đưa vào ViewModel và Repository, đảm nhận việc quản lý trạng thái và dữ liệu. ViewModel cũng bảo vệ khỏi mất dữ liệu khi xoay màn hình.
Vi phạm thứ ba là pha trộn mã nền tảng và mã nghiệp vụ. Ví dụ, đặt một yêu cầu HTTP trực tiếp trong SwiftUI View hoặc Android Composable. Điều này làm mã không di động được và khó kiểm thử. Cách tiếp cận đúng là di chuyển yêu cầu vào Repository, được gọi thông qua Use Case, trong khi View chỉ đăng ký kết quả. Mỗi phần tử của hệ thống giải quyết nhiệm vụ riêng của nó và không vượt quá ranh giới của nó.
Các câu hỏi thường gặp
Không. SoC là một nguyên tắc tổng quát hơn về việc chia hệ thống thành các lĩnh vực trách nhiệm. SOLID là một tập hợp năm quy tắc cụ thể cho thiết kế hướng đối tượng. Nguyên tắc đầu tiên của SOLID (Single Responsibility) là một trường hợp đặc biệt của SoC ở cấp độ một lớp đơn lẻ.
Sử dụng quy tắc một lý do duy nhất để thay đổi (Single Responsibility). Nếu một lớp thay đổi do thay đổi UI, định dạng dữ liệu và quy tắc nghiệp vụ — SoC đã bị vi phạm. Các công cụ như ArchTest (Android) và StrictConcurrency (iOS) giúp phát hiện các vi phạm này tự động.
Về mặt lý thuyết, các tầng bổ sung thêm các lời gọi gián tiếp, nhưng trong thực tế, tác động đến hiệu suất của ứng dụng di động là không đáng kể. Trình biên dịch inline nhiều lời gọi và các tối ưu hóa JIT và AOT loại bỏ chi phí. Khả năng bảo trì mã thu được nhiều hơn những gì mất đi trong các lớp trừu tượng.
Bắt đầu bằng cách trích xuất các yêu cầu mạng từ UI vào Repository. Sau đó di chuyển logic nghiệp vụ vào Use Cases. Sử dụng dependency injection để kết nối các tầng. Thực hiện các thay đổi một cách lặp lại, bao phủ mã mới bằng kiểm thử — điều này đảm bảo việc tái cấu trúc không phá vỡ chức năng hiện có.
Trong nguyên mẫu, có thể vi phạm SoC vì tốc độ. Nhưng nếu nguyên mẫu chuyển sang phát triển sản phẩm, chi phí tái cấu trúc có thể vượt quá lợi ích của việc khởi đầu nhanh. Tối ưu là duy trì sự phân chia tối thiểu (UI và dữ liệu) ngay cả trong nguyên mẫu để tránh phải viết lại mọi thứ từ đầu khi ra mắt.
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm