При разработке ПО важно использовать не только дизайн-, но и архитектурные паттерны. Их существует довольно много. В мобильной разработке самые распространенные - MVVM, Clean Architecture и Redux.
В этой статье мы покажем на примерах проектов как паттерны MVVM и Clean Architecture могут быть применены в iOS приложении.
Если вы также интересуетесь Redux, зацените эту книгу.
Как мы можем видеть на схеме Clean Architecture, у нас есть различные слои приложения. Главное правило - не делать зависимостей внутренних слоев от внешних. Стрелки, указывающие снаружи внтурь это Dependency Rule. Зависимости могут идти только от внешних слоев внутрь к центру.
После группировки у нас получились следующие слои:
Presentation, Domain и Data
Хорошая архитектура выстроена вокруг Use Cases для того, чтобы разработчики могли безопасно описывать структуры, которые поддерживают Use Cases, не применяя фреймворки и другие тулзы. Это называется Screaming Architecture.
Presentation слой содержит UI (UIViewController, SwiftUI View). Вьюхи координирются вью-моделями (или презентерами), которые выполняют один или несколько Use Cases. Presentation слой зависит только от Domain слоя.
Data слой содержит имплементации репозитория и один/несколько Data Source. Репозитории ответственны за координацию данных из разных дата-сорсов. Дата-сорсы могут быть удаленные или локальные (например, persistent database). Data слой зависит только от Domain слоя. В этом слое мы также можем добавить маппинг Network JSON Data в модели Domain.
На схеме ниже каждый компонент каждого слоя показан с направлением зависимости и Data Flow (Request/Response). Мы можем видеть инверсию зависимостей (Dependency Inversion), которая указывает, где мы используем интерфейс репозитория(протоколы). Объясним каждый слой на примере проект, который упоминали в начале статьи.
Data Flow
1. View(UI) вызывает метод из ViewModel (Presenter).
2. ViewModel выполняет Use Case.
3. Use Case комбинирует данные из User и Repositories.
4. Каждый Repository возвращает данные Remote Data (Network), Persistent DBStorage Source или In-memory Data (удаленную или кэшированную).
5. Информация приходит назад в View(UI), где отображается в списке элементов.
Направление зависимостей
Presentation Layer -> Domain Layer <- Data Repositories Layer
Presentation Layer (MVVM) = ViewModels(Presenters) + Views(UI)
Domain Layer = Entities + Use Cases + Repositories Interfaces
Data Repositories Layer = Repositories Implementations + API(Network) + Persistence DB
Пример - проект “Movies App”
Domain слой
Внутри проекта мы находим Domain Layer. Он содержит Entities, 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)
}
Важно: Еще один способ создать Use Cases это использовать UseCase протокол с функцией start() и подписать на него все имплементации Use Cases. Один из кейсов в нашем примере так и делает: FetchRecentMovieQueriesUseCase. Use Cases также называют Interactors
Важно: UseCase может зависеть от других UseCases
Presentation слой
Этот слой содержит MoviesListViewModel с айтемами, которые надбюдаются из MoviesListView. MoviesListViewModel не импортирует UIKit. Потому что не добавляя во ViewModel такие фрейворки как UIKit, SwiftUI или WatchKit, мы сможем ее лучше переиспользовать и тестировать. В будущем, например, рефакторить Views без UIKit или SwiftUI будет гораздо проще, так как не придется менять ViewModel.
// Важно: Не имортируем 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 MoviesListViewModelActions {
// Важно: если понадобится изменить фильм внутри Details экрана и обновить
// MoviesList экран новым фильмом, используйте этот клоужер:
// showMovieDetails: (Movie, @escaping (_ updated: Movie) -> Void) -> Void
let showMovieDetails: (Movie) -> Void
}
final class DefaultMoviesListViewModel: MoviesListViewModel {
private let searchMoviesUseCase: SearchMoviesUseCase
private let actions: MoviesListViewModelActions?
private var movies: [Movie] = []
// MARK: - OUTPUT
let items: Observable<[MoviesListItemViewModel]> = Observable([])
let error: Observable<String> = Observable("")
init(searchMoviesUseCase: SearchMoviesUseCase,
actions: MoviesListViewModelActions) {
self.searchMoviesUseCase = searchMoviesUseCase
self.actions = actions
}
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 event-методы
extension MoviesListViewModel {
func didSearch(query: String) {
load(movieQuery: MovieQuery(query: query))
}
func didSelect(at indexPath: IndexPath) {
actions?.showMovieDetails(movies[indexPath.row])
}
}
// Важно: Эта вьюмодель - для показа данных, и не содержит какую-либо domain модель, чтобы к ней не обращались view
struct MoviesListItemViewModel: Equatable {
let title: String
}
extension MoviesListItemViewModel {
init(movie: Movie) {
self.title = movie.title ?? ""
}
}
Важно: Мы используем интерфейсы MoviesListViewModelInput и MoviesListViewModelOutput, чтобы сделатьMoviesListViewController тестируемым ( сделав мок ViewModel) . Также у нас есть клоужеры MoviesListViewModelActions, которые сообщают MoviesSearchFlowCoordinator когда показывать другие View. Когда вызовутся эти клоужеры, координатор покажет экран подробностей о фильме. Мы используем структуру для группировки функций, чтобы позднее можно было добавить новые.
Presentation слой также содержит MoviesListViewController который связан с датой(items) из MoviesListViewModel.
У UI нет доступа к бизнес-логике или логике приложения (Business Models и UseCases), он есть только у ViewModel. Это разделение ответственности. Мы не можем прокинуть бизнес модель напрямую во View (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, когда они изменяются. Мы используем здесь Observable, который объясняется в обзоре MVVM ниже.
Также назначаем функцию showMovieDetails(movie:) в Actions нашейMoviesListViewModel внутри MoviesSearchFlowCoordinator, чтобы презентовать экран подробностей из flow coordinator:
protocol MoviesSearchFlowCoordinatorDependencies {
func makeMoviesListViewController() -> UIViewController
func makeMoviesDetailsViewController(movie: Movie) -> UIViewController
}
final class MoviesSearchFlowCoordinator {
private weak var navigationController: UINavigationController?
private let dependencies: MoviesSearchFlowCoordinatorDependencies
init(navigationController: UINavigationController,
dependencies: MoviesSearchFlowCoordinatorDependencies) {
self.navigationController = navigationController
self.dependencies = dependencies
}
func start() {
// Важно: Тут мы сохраняем сильную ссылку через клоужер, таким образов к данному флоу не надо обращаться по сильно ссылке
let actions = MoviesListViewModelActions(showMovieDetails: showMovieDetails)
let vc = dependencies.makeMoviesListViewController(actions: actions)
navigationController?.pushViewController(vc, animated: false)
}
private func showMovieDetails(movie: Movie) {
let vc = dependencies.makeMoviesDetailsViewController(movie: movie)
navigationController?.pushViewController(vc, animated: true)
}
}
Важно: Мы используем Flow Coordinator для логики презентации, сокращая объем View Controllers и снижая их ответственность. У нас strong ссылка на Flow (с клоужерами, self функциями), чтобы Flow не деаллоцировался, пока он нужен.
С этим подходом мы легко используем разные View с одной ViewModel, не меняя ее. Просто проверяем совместимость с iOS 13.0 и потом создаем SwiftUI View вместо UIKit и биндим ее к той же ViewModel (или создаем UIKit View). В этом проекте мы добавили SwiftUI пример для MoviesQueriesSuggestionsList*. Нужен хотя бы Xcode 11 Beta*.
// MARK: - Movies Queries Suggestions List
func makeMoviesQueriesSuggestionsListViewController(didSelect: @escaping MoviesQueryListViewModelDidSelectAction) -> 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. Он подписан на interfaces, определенные внутри Domain Layer (Dependency Inversion). Мы также добавляем маппинг JSON data(Decodable conformance) и 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: - Data Transfer Object (DTO)
// Используется как промежуточный объект для encode/decode JSON response в домен, внутри DataTransferService
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: - Мапинг в Domain
extension MoviesResponseDTO {
func toDomain() -> MoviesPage {
return .init(page: page,
totalPages: totalPages,
movies: movies.map { $0.toDomain() })
}
}
...
Важно: Data Transfer Objects DTO используются как посредник для маппинга из JSON response в Domain. Также, если мы хотим кэшировать endpoint response, мы будем хранить Data Transfer Objects в persistent storage, замапив их в Persistent objects(DTO -> NSManagedObject).
В целом Data Repositories могут быть внедрены с помощью API Data Service и Persistent Data Storage. Data Repository работает с этими двумя зависимостями и возвращает данные. Надо сначала попросить разрешения у persistent storage для аутпута кэшированных данных (NSManagedObject замаплены в Domain с помощью DTO object, и достаются в cached data closure). Потом вызываем API Data Service, который возвращается последние обновления данных. Затем Persistent Storage обновляется этими данными (DTO замаплены в Persistent Objects и сохранены). После этого DTO мапятся в Domain и достаются в updated data/completion closure. Таким образом данные сразу будут показаны пользователю. Даже если нет соединения с интернетом, пользователи все равно увидят последние данные из Persistent Storage. example
Хранилище и API могут быть заменены совершенно разными имплементациями (от CoreData до Realm, например). Все остальные слои приложения не будут затронуты этими изменениями, потому что Storage это просто деталь механизма.
Infrastructure слой (Network)
Это обертка над сетевым фреймворком, она может быть Alamofire (или другой фреймворк). Ее можно сконфигурировать сетевыми параметрами (например, базовым 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 он может помочь разделить ответственность между Presentation и UI слоями.
Разные имплементации view могут быть использованы с одной ViewModel. Например, можно использовать CarsAroundListView и CarsAroundMapView и использовать CarsAroundViewModel для обоих. Вы также можете имплементировать одно View из UIKit, а другое View из SwiftUI. Важно помнить, что не надо импортировать UIKit, WatchKit или SwiftUI внутри вашей ViewModel. Таким образом ее легко можно будет переиспользовать.
Data Binding между View и ViewModel может быть выполнен с помощью closures, delegates или observables (например, RxSwift). Combine и SwiftUI также можно использовать, но только если минимальная поддерживаемая версия это iOS 13. У View есть прямое отношение к ViewModel, оно ей сообщает от каждом событии, произошедшем во View. У ViewModel нет прямого сообщения с View (только Data Binding).
В этом примере мы используем простую комбинацию Closure и didSet, чтобы избежать сторонних зависимостей:
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, чтобы посмотреть на полную имплементацию с несколькими и observer removal: Observable.
Пример data binding из 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 внутри клоужера, это создаст retain cycle memory leak (viewModel.items.value - нельзя)
// self?.tableViewController?.items = viewModel.items.value // Это будет retain cycle. Доступ к viewModel только через self?.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 из observing closure закрыт, он вызывает retain cycle(утечка памяти). Доступ к viewModel только через: self?.viewModel.
Пример data binding в TableViewCell (Reusable Cell):
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)
}
}
Важно: Надо разбиндить, если view переисползуется (например, UITableViewCell)
MVVM шаблоны можно найти здесь: here
MVVMs Communication
Делегирование
ViewModel одного экрана MVVM коммуницирует с другой ViewModel другого экрана MVVM через паттерн делегирования:
Например, у нас есть ItemsListViewModel и ItemEditViewModel. Потом создаем протокол ItemEditViewModelDelegate с методом ItemEditViewModelDidEditItem(item). И подписываем его на этот протокол:
extension ListItemsViewModel: ItemEditViewModelDelegate
// Step 1: Определите делегат и добавьте в первую ViewModel как weak property
protocol MoviesQueryListViewModelDelegate: class {
func moviesQueriesListDidSelect(movieQuery: MovieQuery)
}
...
final class DefaultMoviesQueryListViewModel: MoviesListViewModel {
private weak var delegate: MoviesQueryListViewModelDelegate?
func didSelect(item: MoviesQueryListViewItemModel) {
// Note: Замапим View Item Model к Domain Enity
delegate?.moviesQueriesListDidSelect(movieQuery: MovieQuery(query: item.query))
}
}
// Step 2: Подпишем вторую ViewModel на этот делегат
extension MoviesListViewModel: MoviesQueryListViewModelDelegate {
func moviesQueriesListDidSelect(movieQuery: MovieQuery) {
update(movieQuery: movieQuery)
}
}
Важно: В данном случае Delegates можно назвать Responders: ItemEditViewModelResponder
Closures
Другой способ коммуникации это использование closures, которые инъецируются с помощью FlowCoordinator. В примере проекта мы видим, как MoviesListViewModel использует closure showMovieQueriesSuggestions для показа MoviesQueriesSuggestionsView.Также он прокидывает параметр (**_didSelect: MovieQuery) -> Void так, чтобы его можно было вызывать во View. Коммуникация происходит в MoviesSearchFlowCoordinator:
// MoviesQueryList.swift
// Step 1: Определите кложуер для общения с ViewModel
typealias MoviesQueryListViewModelDidSelectAction = (MovieQuery) -> Void
// Step 2: Вызовите клоужер когда понадобится
class MoviesQueryListViewModel {
init(didSelect: MoviesQueryListViewModelDidSelectAction? = nil) {
self.didSelect = didSelect
}
func didSelect(item: MoviesQueryListItemViewModel) {
didSelect?(MovieQuery(query: item.query))
}
}
// MoviesQueryList.swift
// Step 3: Во время презентации MoviesQueryListView нам надо передать клоужер как параметр (_ didSelect: MovieQuery) -> Void
struct MoviesListViewModelActions {
let showMovieQueriesSuggestions: @escapingg (_ didSelect: MovieQuery) -> Void) -> Void
}
class MoviesListViewModel {
var actions: MoviesListViewModelActions?
func showQueriesSuggestions() {
actions?.showMovieQueriesSuggestions { self.update(movieQuery: $0) }
//or simpler actions?.showMovieQueriesSuggestions(update)
}
}
// FlowCoordinator.swift
// Step 4: Внутри FlowCoordinator мы соединяем две viewModels, инъецируя клоужер как self функцию
class MoviesSearchFlowCoordinator {
func start() {
let actions = MoviesListViewModelActions(showMovieQueriesSuggestions: self.showMovieQueriesSuggestions)
let vc = dependencies.makeMoviesListViewController(actions: actions)
present(vc)
}
private func showMovieQueriesSuggestions(didSelect: @escaping (MovieQuery) -> Void) {
let vc = dependencies.makeMoviesQueriesSuggestionsListViewController(didSelect: didSelect)
present(vc)
}
}
Разделение слоев на фреймворки (модули)
Теперь каждый слой (Domain, Presentation, UI, Data, Infrastructure Network) нашего приложения можно легко разделить на фреймворки.
New Project -> Create Project… -> Cocoa Touch Framework
Далее можно включить эти фреймворки в наше основное приложение, используя CocoaPods. Вот рабочий пример: working example here.
Важно: Вам надо удалить ExampleMVVM.xcworkspace и запустить pod install, чтобы сгенерить новый из-за проблем с доступом.
Dependency Injection Container
Dependency injection это техника, при которой один объект предоставляет зависимости другого объекта. DIContainer в вашем приложении это центральный юнит всех зависимостей.
Используем dependencies factory protocols
Один из вариантов это описать dependencies protocol, который делегирует создание зависимости DIContainer. Чтобы это сделать, надо определить протокол MoviesSearchFlowCoordinatorDependencies и подписать MoviesSceneDIContainer под этот протокол, затем инъецировать его в MoviesSearchFlowCoordinator, которому нужна это инъекция для создания и показа MoviesListViewController. Вот нужные шаги:
// Определите Dependencies protocol для нужного класса или структуры
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: - Flow Coordinators
func makeMoviesSearchFlowCoordinator(navigationController: UINavigationController) -> MoviesSearchFlowCoordinator {
return MoviesSearchFlowCoordinator(navigationController: navigationController,
dependencies: self)
}
}
Используя closures
Другая опция это клоужеры. Определите клоужер внутри нужного класса и потом в него этот клоужер инъецируйте. Например:
// Определите makeMoviesListViewController клоужер, который возвращает MoviesListViewController
class MoviesSearchFlowCoordinator {
private var makeMoviesListViewController: () -> MoviesListViewController
init(navigationController: UINavigationController,
makeMoviesListViewController: @escaping () -> MoviesListViewController) {
...
self.makeMoviesListViewController = makeMoviesListViewController
}
...
}
// Инъецируйте у MoviesSceneDIContainer self.makeMoviesListViewController фунцкию в нужный класс
final class MoviesSceneDIContainer {
...
// MARK: - Flow Coordinators
func makeMoviesSearchFlowCoordinator(navigationController: UINavigationController) -> MoviesSearchFlowCoordinator {
return MoviesSearchFlowCoordinator(navigationController: navigationController,
makeMoviesListViewController: self.makeMoviesListViewController)
}
// MARK: - Movies List
func makeMoviesListViewController() -> MoviesListViewController {
...
}
}
Заключение
Самые используемые архитектуры в мобильной разработке - Clean Architecture(слоями), MVVM, и Redux.
MVVM и Clean Architecture можно, конечно, использовать раздельно, но MVVM предоставляет разделение ответственности только внутри Presentation слоя, тогда как Clean Architecture разделяет код на модульные слои, которые можно легко тестировать, переиспользовать и понимать.
Важно не пропускать создание Use Case, даже если Use Case ничего не делает, кроме вызова Repository. Так ваша архитектура будет понятна для нового разработчика, который увидит Use cases.
Хоть это и хорошая отправная точка, это не панацея. Вы выбираете архитектуру конкретно под свои нужды.
Clean Architecture хорошо работает с TDD (Test Driven Development). Она делает проект пригодным для тестирования и замены слоев (UI and Data).
Domain-Driven Design (DDD) тоже хорошо работает с Clean Architecture(CA).
Еще из software engineering best practices:
Не пишите код без тестов (попробуйте TDD)
Делайте продолжительный рефакторинг
Будьте прагматичными и не переусердствуйте
Старайтесь избегать при любой возможности внедрение зависимостей от сторонних фреймворков