+2

Ví dụ về Clean Architecture trong ứng dụng iOS sử dụng RxSwift

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.

Nguồn: https://github.com/sergdort/CleanArchitectureRxSwift


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í