前言
当我们开发软件时,不仅要使用设计模式,还要使用体系结构模式,这一点很重要。软件工程中有许多不同的架构模式。在移动软件工程中,使用最广泛的是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