当我们开发软件时,不仅要使用设计模式,还要使用体系结构模式,这一点很重要。软件工程中有许多不同的架构模式。在移动软件工程中,使用最广泛的是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层 。它包含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)。
表示层包含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<String> = Observable("")
init(searchMoviesUseCase: SearchMoviesUseCase,
closures: MoviesListViewModelClosures) {
self.searchMoviesUseCase = searchMoviesUseCase
self.closures = closures
}
private func load(movieQuery: MovieQuery) {
searchMoviesUseCase.execute(movieQuery: movieQuery) { result in
switch result {
case .success(let moviesPage):
//注意:我们必须在此处将Domain Entities映射到Item View Models。Domain和View的分离
self.items.value += moviesPage.movies.map(MoviesListItemViewModel.init)
self.movies += moviesPage.movies
case .failure:
self.error.value = NSLocalizedString("Failed loading movies", comment: "")
}
}
}
}
// MARK: - INPUT. View事件方法
extension MoviesListViewModel {
func didSearch(query: String) {
load(movieQuery: MovieQuery(query: query))
}
func didSelect(at indexPath: IndexPath) {
closures?.showMovieDetails(movies[indexPath.row])
}
}
//注意:此item view model用于显示数据,并且不包含任何domain model以防止views访问它
struct MoviesListItemViewModel: Equatable {
let title: String
}
extension MoviesListItemViewModel {
init(movie: Movie) {
self.title = movie.title ?? ""
}
}
注意: 我们使用接口MoviesListViewModelInput和MoviesListViewModelOutput使MoviesListViewController可测试,通过轻松地模拟(mocking)ViewModel( 示例 )。另外,我们还有MoviesListViewModelClosures,它告诉协调器 MoviesSearchFlowCoordinator 何时显示其他视图。调用闭包时,协调器将显示电影详细信息屏幕。我们使用一个结构体对闭包进行分组,因为如果需要,我们可以在以后轻松添加更多的闭包。
表示层还包含MoviesListViewController,该控制器绑定到MoviesListViewModel的数据 (items)。
UI无法访问业务逻辑或应用逻辑(Business Models和UseCases),只有ViewModel可以做到。这就是关注分离(separation of concerns) 。我们不能将业务Models直接传递到视图(UI)。这就是为什么我们将Business Models映射到ViewModel内的ViewModel并将它们传递给View的原因。
我们还从View向ViewModel添加了一个搜索事件调用,以开始搜索电影:
import UIKit
final class MoviesListViewController: UIViewController, StoryboardInstantiable, UISearchBarDelegate {
private var viewModel: MoviesListViewModel!
final class func create(with viewModel: MoviesListViewModel) -> MoviesListViewController {
let vc = MoviesListViewController.instantiateViewController()
vc.viewModel = viewModel
return vc
}
override func viewDidLoad() {
super.viewDidLoad()
bind(to: viewModel)
}
private func bind(to viewModel: MoviesListViewModel) {
viewModel.items.observe(on: self) { [weak self] items in
self?.moviesTableViewController?.items = items
}
viewModel.error.observe(on: self) { [weak self] error in
self?.showError(error)
}
}
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
guard let searchText = searchBar.text, !searchText.isEmpty else { return }
viewModel.didSearch(query: searchText)
}
}
当我们从View观察到ViewModel的属性时,我们仅需要从主线程更新UI。这里的Observable ,是为方便通知主线程上的观察者,我们将在下面的MVVM部分中进行了说明。
在MoviesSearchFlowCoordinator 里面,我们还可以指派函数 showMovieDetails(movie:)给 MoviesListViewModel的闭包,以方便从流程协调器显示电影细节屏幕:
protocol MoviesSearchFlowCoordinatorDependencies {
func makeMoviesListViewController() -> UIViewController
func makeMoviesDetailsViewController(movie: Movie) -> UIViewController
}
class MoviesSearchFlowCoordinator {
private weak var navigationController: UINavigationController?
private let dependencies: MoviesSearchFlowCoordinatorDependencies
init(navigationController: UINavigationController,
dependencies: MoviesSearchFlowCoordinatorDependencies) {
self.navigationController = navigationController
self.dependencies = dependencies
}
func start() {
//注意:这里我们对闭包保持强引用,这样此flow就不需要强引用
let closures = MoviesListViewModelClosures(showMovieDetails: showMovieDetails)
let vc = dependencies.makeMoviesListViewController(closures: closures)
navigationController?.pushViewController(vc, animated: false)
}
private func showMovieDetails(movie: Movie) {
let vc = dependencies.makeMoviesDetailsViewController(movie: movie)
navigationController?.pushViewController(vc, animated: true)
}
}
注意: 我们将流程协调器(Flow Coordinator) 用于表示逻辑,以减小View Controller的大小和职责。
通过这种方法,我们可以轻松地对同一ViewModel使用不同的视图,而无需对其进行修改。我们可以检查是否iOS13,然后创建一个SwiftUI视图并将其绑定到相同的ViewModel,低于iOS13我们将创建UIKit视图。在示例项目中, 我还添加了SwiftUI示例,并在以下项下对其进行了注释:SwiftUI 。 必需使用Xcode 11 Beta。
// MARK: - 电影查询建议列表
func makeMoviesQueriesSuggestionsListViewController(didSelect: @escaping MoviesQueryListViewModelDidSelectClosure) -> UIViewController {
if #available(iOS 13.0, *) { // SwiftUI
let view = MoviesQueryListView(viewModelWrapper: makeMoviesQueryListViewModelWrapper(didSelect: didSelect))
return UIHostingController(rootView: view)
} else { // UIKit
return MoviesQueriesTableViewController.create(with: makeMoviesQueryListViewModel(didSelect: didSelect))
}
}
数据层包含DefaultMoviesRepository。 它遵循领域层内部定义的接口(Dependency Inversion)。我们还在此处添加JSON数据(遵循Decodable)和CoreData Entities到Domain Models的映射。
final class DefaultMoviesRepository {
private let dataTransferService: DataTransfer
init(dataTransferService: DataTransfer) {
self.dataTransferService = dataTransferService
}
}
extension DefaultMoviesRepository: MoviesRepository {
public func fetchMoviesList(query: MovieQuery, page: Int, completion: @escaping (Result<MoviesPage, Error>) -> Void) -> Cancellable? {
let endpoint = APIEndpoints.getMovies(with: MoviesRequestDTO(query: query.query,
page: page))
return dataTransferService.request(with: endpoint) { (response: Result<MoviesResponseDTO, Error>) in
switch response {
case .success(let moviesResponseDTO):
completion(.success(moviesResponseDTO.toDomain()))
case .failure(let error):
completion(.failure(error))
}
}
}
}
// MARK: - 数据传输对象DTO(Data Transfer Object)
//在DataTransferService内部,它用作中间对象以将JSON response编码/解码到domain中
struct MoviesRequestDTO: Encodable {
let query: String
let page: Int
}
struct MoviesResponseDTO: Decodable {
private enum CodingKeys: String, CodingKey {
case page
case totalPages = "total_pages"
case movies = "results"
}
let page: Int
let totalPages: Int
let movies: [MovieDTO]
}
...
// MARK: - Mappings to Domain
extension MoviesResponseDTO {
func toDomain() -> MoviesPage {
return .init(page: page,
totalPages: totalPages,
movies: movies.map { $0.toDomain() })
}
}
...
注意: 数据传输对象DTO(Data Transfer Objects)用作从JSON response映射到Domain的中间对象。同样,如果我们想缓存末端(endpoint)response,我们可以通过将数据传输对象DTO映射到持久对象(例如DTO-> NSManagedObject)中来将它们存储在持久存储中。
通常,可以使用API 数据服务和持久性数据存储来注入数据Repositories仓库。数据Repository仓库使用这两个依赖项来返回数据。规则是先请求持久性存储以进行数据输出(NSManagedObject通过DTO对象映射到Domain,并在 缓存的数据闭包 中恢复),然后调用API数据服务,该服务将输出最新的更新数据。然后,它将使用此最新数据更新持久存储(DTOs被映射到持久性对象并保存),然后将DTO映射到Domain中,并在 更新的数据/完成的闭包 中进行恢复。这样,用户将立即看到数据。即使没有互联网连接,用户仍然可以从持久存储中看到最新数据。例子
可以通过完全不同的实现方式来替换存储和API(例如,从CoreData到Realm)。而应用程序的所有其余层都不会受到此更改的影响。这是因为DB是一个细节。
它是网络framework的包装,可以是Alamofire(或其他framework)。可以使用网络参数(例如基本URL)进行配置。它还支持定义末端(endpoints)并包含数据映射方法(使用Decodable)。
struct APIEndpoints {
static func getMovies(with moviesRequestDTO: MoviesRequestDTO) -> Endpoint<MoviesResponseDTO> {
return Endpoint(path: "search/movie/",
method: .get,
queryParametersEncodable: moviesRequestDTO)
}
}
let config = ApiDataNetworkConfig(baseURL: URL(string: appConfigurations.apiBaseURL)!,
queryParameters: ["api_key": appConfigurations.apiKey])
let apiDataNetwork = DefaultNetworkService(session: URLSession.shared,
config: config)
let endpoint = APIEndpoints.getMovies(with: MoviesRequestDTO(query: query.query,
page: page))
dataTransferService.request(with: endpoint) { (response: Result<MoviesResponseDTO, Error>) in
let moviesPage = try? response.get()
}
注意 :您可以在这里阅读更多信息: https : //github.com/kudoleh/SENetworking
在 Model-View-ViewModel 模式(MVVM)在UI和Domain之间提供一个干净的关注分离。
与Clean Architecture结合使用时,它可以帮助分离表示层和UI层之间的关注点。
相同的ViewModel可以使用不同的视图实现。例如,您可以使用CarsAroundListView和CarsAroundMapView,并同时使用CarsAroundViewModel 。您还可以实现一个UIKit视图和另一个SwiftUI视图。重要的是要记住不要在ViewModel中导入UIKit,WatchKit和SwiftUI。这样,如果需要,可以轻松地在其他平台中重用它。
View和ViewModel之间的数据绑定可以使用闭包,委托或RxSwift完成。合并和SwiftUI也可以使用,但只有当你的最低支持的iOS系统是13。View与ViewModel有直接的关系并且在View里面的事件发生时通知它。在ViewModel中,没有对View的引用(仅数据绑定)
在此示例中,我们将使用Closure和didSet(Observable)的简单组合来避免第三方依赖性。
public final class Observable<Value> {
private var closure: ((Value) -> ())?
public var value: Value {
didSet { closure?(value) }
}
public init(_ value: Value) {
self.value = value
}
public func observe(_ closure: @escaping (Value) -> Void) {
self.closure = closure
closure(value)
}
}
注意 :这是Observable的非常简化的版本,以查看具有多个观察者和移除观察者的完整实现:Observable 。为了方便起见,它在主线程上调用观察者block,因为它由包含UI的Presentation层使用。
来自ViewController的数据绑定示例:
final class ExampleViewController: UIViewController {
private var viewModel: MoviesListViewModel!
private func bind(to viewModel: ViewModel) {
self.viewModel = viewModel
viewModel.items.observe(on: self) { [weak self] items in
self?.tableViewController?.items = items
//重要说明:您不能在此闭包内使用viewModel,这将导致循环引用内存泄漏(不允许viewModel.items.value)
// self?.tableViewController?.items = viewModel.items.value // 这将是循环引用。您只能使用self?.viewModel访问viewModel
}
// 或一行
viewModel.items.observe(on: self) { [weak self] in self?.tableViewController?.items = $0 }
}
override func viewDidLoad() {
super.viewDidLoad()
bind(to: viewModel)
viewModel.viewDidLoad()
}
}
protocol ViewModelInput {
func viewDidLoad()
}
protocol ViewModelOutput {
var items: Observable<[ItemViewModel]> { get }
}
protocol ViewModel: ViewModelInput, ViewModelOutput {}
注意 :不允许通过观察闭包访问viewModel,这会导致循环引用(内存泄漏)。您只能使用self:self?.viewModel访问viewModel。
TableViewCell(可重用单元)上的数据绑定示例:
final class MoviesListItemCell: UITableViewCell {
private var viewModel: MoviesListItemViewModel! { didSet { unbind(from: oldValue) } }
func fill(with viewModel: MoviesListItemViewModel) {
self.viewModel = viewModel
bind(to: viewModel)
}
private func bind(to viewModel: MoviesListItemViewModel) {
viewModel.posterImage.observe(on: self) { [weak self] in self?.imageView.image = $0.flatMap(UIImage.init) }
}
private func unbind(from item: MoviesListItemViewModel?) {
item?.posterImage.remove(observer: self)
}
}
注意 :如果视图可重用,我们必须取消绑定(例如,UITableViewCell)
MVVM模板 可以在这里找到
一个MVVM(屏幕)的ViewModel使用委托模式与另一个MVVM(屏幕)的另一个ViewModel通信:
例如,我们有ItemsListViewModel和ItemEditViewModel。然后我们创建协议ItemEditViewModelDelegate,带有方法ItemEditViewModelDidEditItem(item)。并且让它遵循此协议:extension ListItemsViewModel:ItemEditViewModelDelegate
//步骤1:定义代理并将其作为弱属性添加到第一个ViewModel中
protocol MoviesQueryListViewModelDelegate: class {
func moviesQueriesListDidSelect(movieQuery: MovieQuery)
}
...
final class DefaultMoviesQueryListViewModel: MoviesListViewModel {
private weak var delegate: MoviesQueryListViewModelDelegate?
func didSelect(item: MoviesQueryListViewItemModel) {
//注意:我们必须在这里将“View Item Model”映射到“Domain Enity”
delegate?.moviesQueriesListDidSelect(movieQuery: MovieQuery(query: item.query))
}
}
//步骤2:让第二个ViewModel使其遵循此委托
extension MoviesListViewModel: MoviesQueryListViewModelDelegate {
func moviesQueriesListDidSelect(movieQuery: MovieQuery) {
update(movieQuery: movieQuery)
}
}
注意: 在这种情况下,我们也可以将Delegates命名为Responders:ItemEditViewModelResponder
另一种方式来通信是使用由FlowCoordinator分配或注入的闭包。在示例项目中,我们可以看到MoviesListViewModel如何使用闭包showMovieQueriesSuggestions 来显示MoviesQueriesSuggestionsView 。它还传递参数 ( _ didSelect:MovieQuery) -> Void ,以便可以从该View调用它。通信在MoviesSearchFlowCoordinator内部被连接:
//步骤1:定义闭包以与其他ViewModel进行通信,例如,在此处,当选择查询时,我们会通知MovieList
typealias MoviesQueryListViewModelDidSelectClosure = (MovieQuery) -> Void
//步骤2:在需要时调用闭包
class MoviesQueryListViewModel {
init(didSelect: MoviesQueryListViewModelDidSelectClosure? = nil) {
self.didSelect = didSelect
}
func didSelect(item: MoviesQueryListItemViewModel) {
didSelect?(MovieQuery(query: item.query))
}
}
//步骤3:在呈现MoviesQueryListView时,我们需要将此闭包作为参数传递(_ didSelect: MovieQuery) -> Void
struct MoviesListViewModelClosures {
let showMovieQueriesSuggestions: (@escaping (_ didSelect: MovieQuery) -> Void) -> Void
}
class MoviesListViewModel {
func showQueriesSuggestions() {
closures?.showMovieQueriesSuggestions { self.update(movieQuery: $0) }
//或更简单的closures?.showMovieQueriesSuggestions(update)
}
}
//步骤4:在FlowCoordinator内部,我们通过将注入闭包作为自函数来连接两个viewModel的通信
class MoviesSearchFlowCoordinator {
func start() {
let closures = MoviesListViewModelClosures(showMovieQueriesSuggestions: self.showMovieQueriesSuggestions)
let vc = dependencies.makeMoviesListViewController(closures: closures)
...
}
private func showMovieQueriesSuggestions(didSelect: @escaping (MovieQuery) -> Void) {
let vc = dependencies.makeMoviesQueriesSuggestionsListViewController(didSelect: didSelect)
...
}
}
现在,示例应用程序的每个层(Domain,Presentation,UI,数据,基础结构网络)都可以轻松地分成单独的frameworks。
New Project -> Create Project… -> Cocoa Touch Framework
然后,您可以使用CocoaPods将这些frameworks包含到您的主应用程序中。您可以在此处看到此工作示例 。 注意: 由于权限问题,您将需要删除ExampleMVVM.xcworkspace并运行pod install生成一个新的。
依赖注入是一种技术,通过该技术一个对象可以提供另一个对象的依赖。您的应用中的DIContainer是所有注入的核心单元。
使用工厂协议
选项之一是声明一个工厂协议,该协议将依赖创建委派给DIContainer 。为此,我们需要定义MoviesListViewControllersFactory协议,并使您的MoviesSceneDIContainer遵循此协议,然后将其注入到MoviesListViewController中 ,当用户开始搜索电影时,需要通过该注入来创建和呈现MoviesQueriesSuggestionsListViewController。步骤如下:
//为需要它的类或结构体定义依赖协议
protocol MoviesSearchFlowCoordinatorDependencies {
func makeMoviesListViewController() -> MoviesListViewController
}
class MoviesSearchFlowCoordinator {
private let dependencies: MoviesSearchFlowCoordinatorDependencies
init(dependencies: MoviesSearchFlowCoordinatorDependencies) {
self.dependencies = dependencies
}
...
}
//让DIContainer遵循此协议
extension MoviesSceneDIContainer: MoviesSearchFlowCoordinatorDependencies {}
//然后将MoviesSceneDIContainer`self`注入需要它的类中
final class MoviesSceneDIContainer {
...
// MARK: - 流程协调器
func makeMoviesSearchFlowCoordinator(navigationController: UINavigationController) -> MoviesSearchFlowCoordinator {
return MoviesSearchFlowCoordinator(navigationController: navigationController,
dependencies: self)
}
}
使用闭包
另一种选择是使用闭包。为此,您需要在需要注入的类中声明闭包,然后再注入此闭包。例如:
//定义makeMoviesListViewController闭包,该闭包返回MoviesListViewController
class MoviesSearchFlowCoordinator {
private var makeMoviesListViewController: () -> MoviesListViewController
init(navigationController: UINavigationController,
makeMoviesListViewController: @escaping () -> MoviesListViewController) {
...
self.makeMoviesListViewController = makeMoviesListViewController
}
...
}
//然后将MoviesSceneDIContainer的`self`.makeMoviesListViewController函数注入需要它的类中
final class MoviesSceneDIContainer {
...
// MARK: - 流程协调器
func makeMoviesSearchFlowCoordinator(navigationController: UINavigationController) -> MoviesSearchFlowCoordinator {
return MoviesSearchFlowCoordinator(navigationController: navigationController,
makeMoviesListViewController: self.makeMoviesListViewController)
}
// MARK: - 电影列表
func makeMoviesListViewController() -> MoviesListViewController {
...
}
}
kudoleh/iOS-Clean-Architecture-MVVM
Advanced iOS App Architecture
The Clean Architecture
The Clean Code
移动开发中最常用的架构模式是Clean Architecture(分层)、MVVM和Redux。
MVVM和Clean Architecture当然可以分开使用,但是MVVM仅在表示层内部提供关注分离,而Clean Architecture将您的代码分为易于 测试,重用 和 理解的 模块化层。
重要的是不要跳过用例的创建,即使用例除了调用Repository仓库之外没有做其他事情。这样,当新开发人员看到您的用例时,您的架构将变得不言自明。
尽管这应该作为起点, 但没有灵丹妙药。您可以选择满足项目需求的架构。
Clean architecture与TDD(Test Driven Development)一起使用非常好。这种架构使项目可测试,并且可以轻松替换层(UI和数据)。
Domain-Driven Design(DDD)与Clean Architecture(CA)一起也可以很好地工作。
在软件开发中, 您需要了解 更多不同的架构: The 5 Patterns You Need to Know
更多软件工程最佳实践:
通过将应用分离到完全隔离的模块中,您还能进一步改善项目吗?以及所有团队如何快速,独立地使用这些模块?
请留意本文的下一部分,有关应用的模块化