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<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))
   }
}

Data数据层

数据层包含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

MVVM

Model-View-ViewModel 模式(MVVM)在UI和Domain之间提供一个干净的关注分离。

与Clean Architecture结合使用时,它可以帮助分离表示层和UI层之间的关注点。

相同的ViewModel可以使用不同的视图实现。例如,您可以使用CarsAroundListView和CarsAroundMapView,并同时使用CarsAroundViewModel 。您还可以实现一个UIKit视图和另一个SwiftUI视图。重要的是要记住不要在ViewModel中导入UIKit,WatchKit和SwiftUI。这样,如果需要,可以轻松地在其他平台中重用它。
在这里插入图片描述

ViewViewModel之间的数据绑定可以使用闭包,委托或RxSwift完成。合并和SwiftUI也可以使用,但只有当你的最低支持的iOS系统是13。ViewViewModel有直接的关系并且在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通信

代理

一个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生成一个新的。
在这里插入图片描述在这里插入图片描述

依赖注入容器(Dependency Injection Container)

依赖注入是一种技术,通过该技术一个对象可以提供另一个对象的依赖。您的应用中的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

更多软件工程最佳实践:

  • 未经测试请勿编写代码(尝试TDD)
  • 进行连续重构
  • 不要过度构建并且要务实
  • 尽可能避免在项目中使用第三方框架依赖

通过将应用分离到完全隔离的模块中,您还能进一步改善项目吗?以及所有团队如何快速,独立地使用这些模块?

请留意本文的下一部分,有关应用的模块化


  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
iOS MVC和MVVM是两种常见的应用程序架构模式。MVC(Model-View-Controller)是构建iOS应用程序的标准模式,其中Model负责处理数据,View负责显示界面,而Controller负责协调Model和View之间的交互。然而,随着应用程序的复杂性增加,MVC模式可能会导致代码紧密耦合和难以维护的问题。因此,MVVM(Model-View-ViewModel)在iOS开发中逐渐崭露头角,它在设计思路上与MVC相似,但引入了新的组件ViewModel来解决MVC的缺点。 MVVM是由微软提出的一种架构模式。它规范了视图和控制器之间紧密耦合的性质,并引入了ViewModel组件。在MVVM中,ViewModel起到了连接Model和View的角色,它负责处理业务逻辑和数据转换,并通过数据绑定将数据传递给View。通过使用MVVM,我们可以更好地分离界面逻辑和业务逻辑,使代码更加模块化和可测试。 总结来说,iOS MVC和MVVM都是常见的应用程序架构模式,用于组织和管理iOS应用程序的代码。MVC是传统的模式,而MVVM是一种更现代化且具有更好可扩展性和可维护性的模式。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [iOS MVC、MVVM、MVP详解](https://blog.csdn.net/u013712343/article/details/106686276)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* *3* [iOS中MVC已死MVVM当立??](https://blog.csdn.net/weixin_33811539/article/details/88017811)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值