iOS上的干净架构(Clean Architecture)和MVVM

前言

当我们开发软件时,不仅要使用设计模式,还要使用体系结构模式,这一点很重要。软件工程中有许多不同的架构模式。在移动软件工程中,使用最广泛的是MVVM,Clean Architecture和Redux模式。

我们将在 工作示例项目中 看到如何在iOS中应用两种架构模式MVVM和Clean Architecture。如果您有兴趣学习Redux,请阅读这本很棒的书: Advanced iOS App Architecture

更多信息关于 Clean Architecture

概述

层次结构

在这里插入图片描述
正如我们在“Clean Architecture” 图中所看到的,应用程序中有不同的层。主要规则是从内层到外层不具有依赖关系。我们在这里可以看到箭头也从外部指向内部,这是 依赖规则 。我们只能从外层向内层有依赖关系。

将所有层分组后,我们得到: Presentation,Domain和Data层。
在这里插入图片描述

Domain领域层 是上面类似洋葱图的最内层部分(不依赖于其他层,它是完全隔离的)。它包含 Entities,Use cases和Repository Interfaces。 该层可能会在不同项目中重用。真正的好处是,Domain用例测试将在几秒钟内运行。这是因为对于测试目标,不需要host app(不需要访问网络), 也没有依赖关系 (也没有第三方依赖关系)。注意: Domain层不应包含其他层的任何内容(例如 Presentation-UIKit或SwiftUI 或Data Layer-Mapping Codable

好的体系结构以 用例 为中心的原因是,架构师可以安全地描述支持这些 用例 的结构,而无需 使用 框架,工具和环境。它被称为Screaming Architecture

Presentation表示层 包含 UI(UIViewControllers或SwiftUI视图)。视图执行一个或多个用例ViewModel(Presenters)协调 表示层只依赖领域层

Data数据层 包含 Repository仓库实现和一个或多个数据源。 Repositories仓库负责协调来自不同数据源的数据。数据源可以来自远程或本地持久数据库。数据层只取决于领域层 。在这一层中,我们还可以将网络JSON数据(例如, Decodable conformance )映射到Domain的Models中。

在此图的此处,我们可以看到来自每个具有依赖方向(Dependency Direction) 的层中的每个组件,以及数据如何流动 (请求/响应)。我们可以看到使用Repository接口(协议)的依赖倒置(Dependency Inversion) 的点。每层的解释将基于本文开头提到的 示例项目
在这里插入图片描述

数据流

1. UI从ViewModel(Presenter)调用方法

2. ViewModel执行Use case用例

3.Use case用例结合了用户和Repositories仓库中的数据。

4.每个Repositories仓库从远程数据(网络),持久性数据库存储源或内存数据(远程或缓存)中返回数据。

5.信息流回到UI,在其中显示items列表。

依赖方向

表示层 - > 领域层 < - 数据仓库层

表示层(MVVM) = ViewModels(Presenters)+ Views(UI)

领域层 = Entities实体 + 用例 + 仓库接口

数据仓库层 = 仓库实现 + API(网络)+ 持久性数据库

示例

在这里插入图片描述

Domain领域层

示例项目中, 您可以找到 Domain层 。它包含SearchMoviesUseCase ,用于搜索电影并存储最近成功的查询。而且,它包含依赖倒置所需的 数据仓库接口(Data Repositories Interfaces)

protocol SearchMoviesUseCase {
   
    func execute(requestValue: SearchMoviesUseCaseRequestValue,
                 completion: @escaping (Result<MoviesPage, Error>) -> Void) -> Cancellable?
}

final class DefaultSearchMoviesUseCase: SearchMoviesUseCase {
   

    private let moviesRepository: MoviesRepository
    private let moviesQueriesRepository: MoviesQueriesRepository
    
    init(moviesRepository: MoviesRepository, moviesQueriesRepository: MoviesQueriesRepository) {
   
        self.moviesRepository = moviesRepository
        self.moviesQueriesRepository = moviesQueriesRepository
    }
    
    func execute(requestValue: SearchMoviesUseCaseRequestValue,
                 completion: @escaping (Result<MoviesPage, Error>) -> Void) -> Cancellable? {
   
        return moviesRepository.fetchMoviesList(query: requestValue.query, page: requestValue.page) {
    result in
            
            if case .success = result {
   
                self.moviesQueriesRepository.saveRecentQuery(query: requestValue.query) {
    _ in }
            }

            completion(result)
        }
    }
}

// Repository Interfaces
protocol MoviesRepository {
   
    func fetchMoviesList(query: MovieQuery, page: Int, completion: @escaping (Result<MoviesPage, Error>) -> Void) -> Cancellable?
}

protocol MoviesQueriesRepository {
   
    func fetchRecentsQueries(maxCount: Int, completion: @escaping (Result<[MovieQuery], Error>) -> Void)
    func saveRecentQuery(query: MovieQuery, completion: @escaping (Result<MovieQuery, Error>) -> Void)
}

注意 :创建用例的另一种方法是将 UseCase 协议与 start() 函数一起使用,并且所有用例实现都将遵循此协议。示例项目中的一种用例遵循以下方法: FetchRecentMovieQueriesUseCase 。用例也称为 交互器(Interactors)

Presentation表示层

表示层包含MoviesListViewModel ,其中包含在MoviesListView中被观察的items。MoviesListViewModel 不会导入UIKit。因为让ViewModel不导入任何UI框架(如UIKit,SwiftUI或WatchKit),考虑到重用和重构。例如,将来,从UIKit到SwiftUI 的View重构将更加容易,因为不需要更改ViewModel

//注意:此处不能导入任何UI框架(如UIKit或SwiftUI)。
protocol MoviesListViewModelInput {
   
    func didSearch(query: String)
    func didSelect(at indexPath: IndexPath)
}

protocol MoviesListViewModelOutput {
   
    var items: Observable<[MoviesListItemViewModel]> {
    get }
    var error: Observable<String> {
    get }
}

protocol MoviesListViewModel: MoviesListViewModelInput, MoviesListViewModelOutput {
    }

struct MoviesListViewModelClosures {
   
    //注意:如果您需要在“详细信息”屏幕中编辑电影并进行更新
    //具有更新的电影的MoviesList屏幕,那么您将需要以下closure:
    //showMovieDetails: (Movie, @escaping (_ updated: Movie) -> Void) -> Void
    let showMovieDetails: (Movie) -> Void
}

final class DefaultMoviesListViewModel: MoviesListViewModel {
   
    
    private let searchMoviesUseCase: SearchMoviesUseCase
    private let closures: MoviesListViewModelClosures?
    
    private var movies: [Movie] = []
    
    // MARK: - OUTPUT
    let items: Observable<[MoviesListItemViewModel]> = Observable([])
    let error: Observable
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值