Clean Architecture là một business architecture, nó tách rời những xử lý nghiệp vụ khỏi UI và framework. Clean Architecture phân rõ vai trò và trách nhiệm của từng layer trong kiến trúc của mình.
Về mặt ưu điểm, Clean architecture đạt được:
- Giúp logic nghiệp vụ trở nên rõ ràng.
- Không phụ thuộc vào framework
- Các thành phần UI hoàn toàn tách biệt và độc lập.
- Không phụ thuộc vào nguồn cung cấp dữ liệu.
- Dễ dàng unit test.
Về mặt nhược điểm:
- Clean architecture do phân tách cấu trúc thành nhiều tầng nên dẫn đến việc số lượng code sinh ra là rất lớn.
Về mặt cấu trúc, Clean architecture gồm 3 thành phần chính:
- Domain: Là tầng chứa các thành phần cơ bản của ứng dụng và những gì ứng dụng có thể làm như các Entity, UseCase,... Nó không phụ thuộc vào bất cứ thành phần nào của UI hay bất kỳ Framework nào và cũng không implement bất kỳ một thành phần nào của ứng dụng tại tầng này.
- Platform: Là tầng triển khai các phần cụ thể (concrete implementation) của tầng
Domain. TầngPlatformsẽ che giấu đi những chi tiết được triển khai thực hiện. Bất cứ các task nào liên quan đếncall api, local DB, backend...sẽ thực hiện ở đây. - Application (hoặc Presentation): Là tầng chịu trách nhiệm cung cấp thông tin từ ứng dụng cho user và tiếp nhận những input từ user cho ứng dụng. Nó có thể được triển khai với các mô hình như MVC, MVP, MVVM. Đối với SwiftUI thì đây sẽ là nơi chứa các
View. Trong example project, cácViewhoàn toàn độc lập với tầngPlatform. Nhiệm vụ duy nhất của một View là "bind"UIđếnDomainđể ứng dụng hoạt động.
Domain
Entities là các model
struct GithubRepoModel: Mappable {
var id: Int?
var name: String?
var fullname: String?
}UseCase là nơi xử lý các business logic: Nó có thể sử dụng đến Repository (ở tầng Platform) để triển khai các task liên quan đến api, local DB, backend... (hoặc không) nếu như các use cases là các task không liên quan đến api, db. Tại UseCase sẽ inject Repository của tầng Platform (hoặc không). Như trong ví dụ này thì Repository đã được inject vào UseCase bằng lib Factory.
protocol GithubRepoUseCaseType {
func searchRepo(query: String) -> AnyPublisher<[GithubRepoEntities], Error>
}
class GithubRepoUseCase: GithubRepoRepositoryType {
@LazyInjected(\.githubRepository) var repository
func searchRepo(query: String) -> AnyPublisher<[GithubRepoEntities], Error> {
repository.searchRepo(query: query)
}
}Platform
Tại Platform chúng ta sẽ tiến hành triển khai các task như call api, backend, db như đã nói ở trên, và tiếp nhận data thông qua một Repository. Repository chính là nơi triển khai chi tiết (concrete implementation) các phần cụ thể của những use cases.
protocol GithubRepoRepositoryType {
func searchRepo(query: String) -> AnyPublisher<[GithubRepoEntities], Error>
}
class GithubRepoRepository: GithubRepoRepositoryType {
func searchRepo(query: String) -> AnyPublisher<[GithubRepoEntities], Error> {
let param: [String: Any] = [
"q": query,
"per_page": 10,
"page": 1
]
return APIService
.shared
.request(nonBaseResponse: SearchRepoAPIRouter.searchRepo(param: param))
.tryMap { (response: GithubRepoModel) in
return response.githubRepos ?? []
}
.eraseToAnyPublisher()
}
}Application
Application là tầng chúng ta sẽ triển khai design pattern MVVM-C cùng với Combine, khiến việc binding trở nên dễ dàng hơn. Chữ C trong cụm từ MVVM-C mình sẽ giải thích bên dưới. Tầng Application sẽ chỉ dùng đến UseCase của Domain mà không quan tâm đến những tầng khác.
ViewModel sẽ đóng vai trò chuẩn bị và trung chuyển dữ liệu.
ViewModel sẽ inject UseCase của tầng Domain, chịu trách nhiệm thực hiện các xử lý business logic và Router sẽ chịu trách nhiệm điều hướng ứng dụng (chuyển màn hình, show alert,...). Router trong ví dụ sử dụng lib Stinsen.
class SearchRepoViewModel: ObservableObject {
let activityIndicator = ActivityIndicator()
let errorTracker = ErrorTracker()
private var bag = Set<AnyCancellable>()
@LazyInjected(\.githubUseCase) var useCase
@Published var searchText = ""
@Published var githubRepos = [GithubRepoEntities]()
@RouterObject var router: SearchRepoCoordinator.Router?
init() {
$searchText
.filter({!$0.isEmpty})
.removeDuplicates()
.debounce(for: .seconds(1.0), scheduler: DispatchQueue.main)
.flatMap { [self] query in
return useCase.searchRepo(query: query)
.receive(on: DispatchQueue.main)
.trackError(errorTracker)
.trackActivity(activityIndicator)
}
.assign(to: &$githubRepos)
}
func pushToDetail(repo: GithubRepoEntities) {
router?.route(to: \.pushToDetail, repo)
}
}Chữ C trong MVVM-C
Là Coordinator, một design pattern khá phổ biển ở Swift. Coordinator chứa các Router, đóng vài trò điều hướng ứng dụng.
final class SearchRepoCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \SearchRepoCoordinator.start)
@Root var start = makeStart
@Route(.push) var pushToDetail = makeDetail
}
extension SearchRepoCoordinator {
@ViewBuilder func makeStart() -> some View {
SearchRepoView()
}
@ViewBuilder func makeDetail(repo: GithubRepoEntities) -> some View {
DetailRepoView(repo: repo)
}
}Và cuối cùng: View, nơi user thao tác, nhận đầu vào và hiển thị các đầu ra tương ứng.
struct SearchRepoView: View {
@StateObject var viewModel = SearchRepoViewModel()
var body: some View {
List {
ForEach(viewModel.githubRepos, id: \.id) { repo in
RepoRow(repo: repo)
.onTapGesture {
viewModel.pushToDetail(repo: repo)
}
}
}
.searchable(text: $viewModel.searchText)
.onReceiveError(viewModel.errorTracker.errorPublisher)
.onReceiveLoading(viewModel.activityIndicator.isLoadingPublisher)
.navigationTitle("Github repo")
}
}Luồng chạy thông qua Sequence Diagram:
sequenceDiagram
SearchRepoView ->> SearchRepoViewModel: 1. $searchText:
SearchRepoViewModel ->> GithubRepoUseCase: 2. searchRepo(query:) (UseCase)
GithubRepoUseCase ->> GithubRepoRepository: 3. searchRepo(query:) (Repository)
GithubRepoRepository ->> API: 4. request()
API -->> GithubRepoRepository: 5. response()
GithubRepoRepository -->> GithubRepoUseCase: 6. Get data success -> parser model
GithubRepoUseCase -->> SearchRepoViewModel: 7. Data
SearchRepoViewModel -->> SearchRepoView: 8. Binding data

