Clean architecture with RxSwift
Bài đăng này đã không được cập nhật trong 6 năm
Introduction
Trong bài viết iOS Clean Architecture(P1) Tôi đã giới thiệu với các bạn về Clean Architecture, hôm nay tôi xin giới thiệu với các bạn về việc kết hợp Clean architecture sử dụng RxSwift - Một thư viện nổi tiếng về Reactive Programming trong Swift
Chúng ta cùng tìm hiểu nhé!
Tổng quan Clean architecture
Domain
Domain cơ bản chính là App của bạn là gì, nó có thể làm gì(Entities, UseCase, vv). Nó không phụ thuộc vào UIKit hoặc bất kể thư viện nào và nó không được implement ngoài các entities
Platform
Platform implement cụ thể những khai báo từ Domain. Nó ẩn tất cả các details của implementation. Ví dụ: Database implement có thể là CoreDate, Realm hay Sqlite...
Application
Application có trách nhiệm cung cấp thông tin cho người dùng và xử lý thao tác của người dùng. Nó có thể được implement với bất kỳ mô hình nào(MVVM, MVC, MVP).
Detail overview
Để việc module hóa Domain, Platform và Application là các target riêng biệt trong ứng dụng, cho phép chúng ta tận dụng lợi thế của internal access layer trong Swift để ngăn chặn việc loại bỏ các loại mà chúng ta không muốn expose.
Để thực thi mô đun, Domain, Platform và Application là các mục tiêu riêng biệt trong ứng dụng, cho phép chúng ta tận dụng lớp truy cập nội bộ trong Swift để ngăn chặn việc loại bỏ các loại mà chúng ta không muốn phơi bày.
Domain
Entities được implement là kiểu Swift value types
public struct Post {
public let uid: String
public let createDate: Date
public let updateDate: Date
public let title: String
public let content: String
}
UseCases là các protocol thực hiện một việc cụ thể:
public protocol PostsUseCase {
func posts() -> Observable<[Post]>
func save(post: Post) -> Observable<Void>
}
UseCaseProvider is a service locator. Trong ví dụ hiện tại, nó giúp việc ẩn các việc triển khai cụ thể các use cases.
Platform
Trong nhiều trường hợp chúng ta không thể sử dụng Swift Structs cho domain objects vì DB framework yêu cầu (Ví dụ: Cordata, Realm)
final class CDPost: NSManagedObject {
@NSManaged public var uid: String?
@NSManaged public var title: String?
@NSManaged public var content: String?
@NSManaged public var createDate: NSDate?
@NSManaged public var updateDate: NSDate?
}
final class RMPost: Object {
dynamic var uid: String = ""
dynamic var createDate: NSDate = NSDate()
dynamic var updateDate: NSDate = NSDate()
dynamic var title: String = ""
dynamic var content: String = ""
}
final class PostsUseCase: Domain.PostsUseCase {
private let repository: AbstractRepository<Post>
init(repository: AbstractRepository<Post>) {
self.repository = repository
}
func posts() -> Observable<[Post]> {
return repository.query(sortDescriptors: [Post.CoreDataType.uid.descending()])
}
func save(post: Post) -> Observable<Void> {
return repository.save(entity: post)
}
}
final class Repository<T: CoreDataRepresentable>: AbstractRepository<T> where T == T.CoreDataType.DomainType {
private let context: NSManagedObjectContext
private let scheduler: ContextScheduler
init(context: NSManagedObjectContext) {
self.context = context
self.scheduler = ContextScheduler(context: context)
}
override func query(with predicate: NSPredicate? = nil,
sortDescriptors: [NSSortDescriptor]? = nil) -> Observable<[T]> {
let request = T.CoreDataType.fetchRequest()
request.predicate = predicate
request.sortDescriptors = sortDescriptors
return context.rx.entities(fetchRequest: request)
.mapToDomain()
.subscribeOn(scheduler)
}
override func save(entity: T) -> Observable<Void> {
return entity.sync(in: context)
.mapToVoid()
.flatMapLatest(context.rx.save)
.subscribeOn(scheduler)
}
}
Như bạn thấy, việc implementation chỉ là nội bộ vì chúng tôi không muốn để lộ dependecies của chúng tôi. Điểu duy nhất có thể được expose là từ Platform triển khai thực hiện cụ thể UseCaseProvider
public final class UseCaseProvider: Domain.UseCaseProvider {
private let coreDataStack = CoreDataStack()
private let postRepository: Repository<Post>
public init() {
postRepository = Repository<Post>(context: coreDataStack.context)
}
public func makePostsUseCase() -> Domain.PostsUseCase {
return PostsUseCase(repository: postRepository)
}
}
Application
Trong ví dụ hiện tại, Application được implement với MVVM pattern và sử dụng RxSwift làm việc binding rất dễ dàng.
protocol ViewModelType {
associatedtype Input
associatedtype Output
func transform(input: Input) -> Output
}
final class PostsViewModel: ViewModelType {
struct Input {
let trigger: Driver<Void>
let createPostTrigger: Driver<Void>
let selection: Driver<IndexPath>
}
struct Output {
let fetching: Driver<Bool>
let posts: Driver<[Post]>
let createPost: Driver<Void>
let selectedPost: Driver<Post>
let error: Driver<Error>
}
private let useCase: AllPostsUseCase
private let navigator: PostsNavigator
init(useCase: AllPostsUseCase, navigator: PostsNavigator) {
self.useCase = useCase
self.navigator = navigator
}
func transform(input: Input) -> Output {
......
}
ViewModel có thể được injected vào trong ViewController thông qua property injection hoặc cài đặt. Trong ví dụ hiện tại nó đã hoàn thành bởi Navigator.
protocol PostsNavigator {
func toCreatePost()
func toPost(_ post: Post)
func toPosts()
}
class DefaultPostsNavigator: PostsNavigator {
private let storyBoard: UIStoryboard
private let navigationController: UINavigationController
private let services: ServiceLocator
init(services: ServiceLocator,
navigationController: UINavigationController,
storyBoard: UIStoryboard) {
self.services = services
self.navigationController = navigationController
self.storyBoard = storyBoard
}
func toPosts() {
let vc = storyBoard.instantiateViewController(ofType: PostsViewController.self)
vc.viewModel = PostsViewModel(useCase: services.getAllPostsUseCase(),
navigator: self)
navigationController.pushViewController(vc, animated: true)
}
....
}
class PostsViewController: UIViewController {
private let disposeBag = DisposeBag()
var viewModel: PostsViewModel!
...
}
Conclusion
Sự kết hợp Clean Architecture và RxSwift phần nào đã cho chúng ta thấy lợi ích của việc làm rõ từng layer trong project ngoài ra khi sử dụng RxSwift sẽ giúp cho việc binding dữ liệu trở nên rất dễ dàng và nhanh chóng.
Cám ơn bạn đã dành thời gian cho bài viết này!
Nguồn:
All rights reserved