Ví dụ về Clean Architecture trong ứng dụng iOS sử dụng RxSwift
Bài đăng này đã không được cập nhật trong 3 năm
Tổng quan về các level:
Domain
Domain là cơ bản ứng dụng của bạn là gì và nó có thể làm gì (Entities, UseCase, vv..) Nó không phụ thuộc vào UIKit hay bất cứ khuôn khổ nào cả, và nó không có sự implement ngoài các Entities.
Platform
Platform là việc implement cụ thể của Domain trong một nền tảng cụ thể như iOS. Nó giấu tất cả các chi tiết được implement. Ví dụ việc implement database cho dù đó là CoreData, Realm, SQLite, vv..
Application
Application có trách nhiệm cung cấp thông tin cho người dùng và xử lý đầu vào của người dùng. Nó có thể được thực hiện với bất kỳ pattern nào như MVVM, MVC, MVP. Đây là nơi dành cho tất cả UIView và UIViewController của bạn. Như bạn thấy từ ứng dụng ví dụ, tất cả ViewController hoàn toàn độc lập với Platform. Trách nhiệm duy nhất của ViewController là bind UI với Domain để kết nối mọi thứ. Trong thực tế, trong ví dụ hiện tại, chúng ta đang sử dụng ViewController cho Realm và CoreData.
Tổng quan chi tiết
Module, 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ợi thế của lớp truy cập nội bộ trong Swift để ngăn chặn phơi bày những thứ mà chúng ta không muốn phơi bày ra.
Domain
Entities được thực hiện dưới dạng các kiểu giá trị của Swift.
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 là một service định vị. Trong ví dụ hiện tại, nó giúp giấu đi nội dung implement cụ thể của các UseCase.
Platform
Trong một số trường hợp, chúng tôi không thể sử dụng các struct trong Swift cho các đối tượng Domain do các yêu cầu của DB framework (ví dụ: CoreData, 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 = ""
}
Flatform này cũng chứa các nội dung implement cụ thể của các UseCase, repository hay bất kỳ service nào được định nghĩa trong Domain.
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, nội dung implement là internal, bởi vì chúng tôi không muốn để lộ những thứ phụ thuộc liên quan. Điều duy nhất được phơi bày trong ví dụ hiện tại từ Platform là việc implement cụ thể của 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, ứng dụng được thực hiện với mô hình MVVM và sử dụng RxSwift rất nhiều, làm cho việc binding rất dễ dàng.
Trường hợp ViewModel thực hiện chuyển hóa Input thành Output.
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 {
......
}
Một ViewModel có thể được inject vào một ViewController thông qua property hoặc khởi tạo. Trong ví dụ hiện tại, điều này được thực hiện 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!
...
}
Ví Dụ
Ứng dụng ví dụ là ứng dụng Post/ Todos sử dụng Realm, CoreData và Network đồng thời là bằng chứng về khái niệm rằng Application level không phụ thuộc vào nội dung implement của Platform level.
All rights reserved