关键要点

  • 优秀的架构应该是与特定平台无关的。那些使Android应用程序具备可维护性的设计原则,在iOS平台上也同样适用。
  • 基于动作的ViewModel能够明确各部分的功能职责:通过单一方法处理所有的数据变化,这样就能实现集中化的日志记录、更便捷的测试,并且能清楚地了解ViewModel的实际功能。
  • 明确的状态管理机制可以从一开始就避免出现不合理的状态:Loadable枚举类型的使用,比使用多个@Published属性更为有效——一个属性对应一个唯一的数据来源。
  • 将“负责管理ViewModel”与“负责渲染UI”这两个职责分开,可以使视图代码更具复用性,也便于单独进行预览测试。
  • 响应式数据存储机制能够实现UI的自动同步:当数据存储库负责管理数据并通过相应的接口公开这些数据时,任何更新都会自动传播到所有依赖这些数据的ViewModel中。

对于我们这些iOS开发者来说,从苹果提供的简单单页示例应用中开发出可扩展的架构往往是一件很困难的事情。当然,这种设计方法适用于简单的应用程序,但当我们需要构建更复杂、可扩展的系统时,就常常不知道该从哪里入手。

在经过一番探索后,我接触到了Android开发领域。让我感到惊讶的是,与苹果相比,谷歌为开发者提供了更多有用的资源:Android开发者有清晰的指导文档和设计模式,更重要的是,还有许多实际案例,这些案例展示了如何构建真正的生产环境应用,而不仅仅是一些实验性的项目。

Android社区能够从中受益的地方包括:

相比之下,iOS开发者往往需要从博客文章和苹果提供的示例应用中自行拼凑解决方案。这些方案单独来看确实很有用,但它们很少能反映现实世界中应用程序架构的发展过程,因此我们只能寄希望于应用程序在不断发展过程中其架构不会出现问题。

不过有一点是值得欣慰的:良好的架构是与平台无关的。那些使Android应用程序具备可维护性的设计原则,在iOS平台上也同样适用。

本文探讨了如何运用受现代Kotlin和Android开发理念启发的架构模式来构建iOS应用程序,并说明了这些模式是如何被应用到Swift和SwiftUI中的。

我们首先会讨论一个基本问题:如何在视图内部管理状态。这个问题涉及到如何为状态的变更设定统一的入口点,以及如何实现日志记录、调试等功能。

接下来,我们会将视图与其视图模型分离开来,这样就能提高代码的可重用性、可测试性和可预览性。

最后,我们会引入一个主动存储数据的层,从而真正实现“单一数据源”的理念,并展示数据是如何在整个应用程序中自动传播的。

传统iOS视图模型的问题

如果你曾经使用SwiftUI开发过iOS应用程序,那么你很可能写过类似这样的代码:

class DashboardViewModel: ObservableObject {
    @Published var workouts: [Workout] = []
    @Published var isLoading = false
    @Published var error: Error?
    func loadWorkouts() {
        isLoading = true
        error = nil
        
        Task {
            do {
                workouts = try await api.fetchWorkouts()
                isLoading = false
            } catch {
                self.error = error
                isLoading = false
            }
        }
    }
}

这样的代码对于简单的界面来说确实可行,但当视图模型变得越来越复杂时,问题就会出现了。

状态管理的问题

有时会出现多个属性相互矛盾的情况,而没有任何机制能够阻止这种问题的发生:

viewModel isLoading = true
viewModel.workouts = cachedWorkouts // 现在我们在“加载”数据
viewModel.error = NetworkError.timeout // 又出现了错误?

那么,用户界面应该显示哪种状态呢?编译器在这里并不能提供帮助。开发者会做出不同的选择,从而导致各种错误的出现。

状态变更的管理问题

你会添加更多的方法,比如loadMore()refresh()deleteWorkout()filterWorkout()selectWorkout()。这样一来,就会有很多方法以各自不同的方式来修改状态。如果你想记录每一个状态变化,就需要在多处添加日志记录代码;如果你想要调试程序,找出为什么isLoading会一直保持为“true”的状态,那就需要在十个地方设置断点;如果你想要编写测试用例,就必须弄清楚哪些方法调用的组合能够重现用户实际的操作流程。由于没有集中的管理机制,视图模型就只是一堆方法而已,开发者必须自己去记住这些方法之间是如何相互作用的。

想象一下,你正在开发某个功能。这个功能涉及到一个你已经六个月没有使用过的ViewModel,或者是一个全新的ViewModel。当你打开这个文件时,会发现其中包含六百行代码和二十个方法——这个组件到底是用来做什么的?哪些方法是从视图层被调用的,哪些又是内部辅助方法呢?你必须阅读整个类才能理解它的功能。没有任何总结说明,没有任何文档说明“这个ViewModel能够完成哪些操作”,更没有列出“这些方法的具体用途”。现在,把这种情况再应用到另外100个ViewModel上吧。

解决状态管理问题:明确的状态定义

sealed interface UiState〈out T〉 {
    data object Loading : UiState〈Nothing〉()
    data class Success〈T〉(val data: T) : UiState〈T〉()
    data class Error(val message: String) : UiState〈Nothing〉()
}

val workouts: StateFlow〈UiState〈List〈Workout〉〉>> = ...

状态是由一个唯一的、权威的定义来源来确定的。其类型确保了所有可能的状态都是互斥的,编译器也会强制遵守这一规则。因此,同时处于LoadingSuccess这两种状态是不可能的。

在Swift中,实现方式也非常直接:

enum Loadable〈T, U〉 {
    case loading
    case finished(T)
    case error(U)
}

class DashboardViewModel: ObservableObject {
    @Published var workouts: Loadable〈[Workout〕〉 = .loading
}

解决数据变更问题:单一的入口点

明确的状态定义可以避免出现矛盾的状态,但那么对于那些需要被多次修改的数据来说,该如何处理呢?Kotlin的解决方案是让所有的数据变更操作都通过一个统一的入口点来进行:

fun onAction(action: DashboardAction) {
    when (action) {
        is DashboardActionRefresh -> loadWorkouts()
        is DashboardAction.SelectWorkout -> selectWorkout(action.id)
        is DashboardAction.Delete -> deleteWorkout(action.id)
    }
}

所有的数据变更操作都必须通过onAction()这个方法来进行,没有任何例外。

让我们单独看一下DashboardAction这个类:

sealed class DashboardAction {
    object Refresh : DashboardAction()
    data class SelectWorkout(val id: String) : DashboardAction()
    data class Delete(val id: String) : DashboardAction()
    data class FilterBy(val type: WorkoutType) : DashboardAction()
}

这个类列出了ViewModel中所有的操作类型。新来的工程师只需要打开这个文件,阅读相关代码,就能立即了解这个ViewModel的功能。根本不需要花费时间去浏览那些六百行的代码,也不需要去猜测哪些方法是公共的、哪些是仅在内部使用的。

密封类实际上就相当于一份契约。如果某个操作没有在密封类中明确声明,ViewModel就无法执行该操作。这种设计机制也会迫使你认真思考ViewModel应该承担哪些职责。每当添加新的操作时,都必须首先将其加入密封类中——这是一个经过深思熟虑的决定,而不是随便在文件中的某个地方添加一个方法而已。

但是,DashboardAction这个类里到底应该包含哪些内容呢?如果某个操作可以被视图触发,那么就应该将其声明为一种操作。比如,用户是点击来删除某项内容,还是选择某项内容?而一些内部辅助方法,比如loadWorkouts(),则只能在perform()方法内部被调用——它属于私有方法,并不属于操作类。真正的操作应该是.refresh这种方法,其内部实现细节其实并不重要。

enum Action {
    case refresh  
    case selectWorkout(String)
    case delete(String)
}
// 这些不是操作方法,而是内部实现函数
private func loadWorkouts() async { ... }
private func updateCache(_ workouts: [Workout]) { ... }

如果你已经从事iOS应用开发多年,可能会觉得这种设计模式有些多余。既然可以直接调用某个方法,为什么非要通过一个中间层来处理所有操作呢?其实,当团队规模较小时,只有三到五个屏幕需要管理时,这种设计确实没有太大问题。但当团队规模扩大、代码量增加时,这种模式就会暴露出其局限性。传统的iOS开发模式都是针对简单情况进行优化的,比如使用@StateObject@Published等关键字,以及直接调用方法。这种做法易于理解,编写速度也很快。苹果提供的示例代码也是按照这种方式编写的,因为这些示例代码的规模本来就很小。

但当应用规模扩大后,这些直接的 method 调用就会带来很多问题。每个方法都可能成为状态变化的入口点,而入口点越多,就越难以理解ViewModel的工作原理。因此,在代码量增加的情况下,将操作功能集中起来处理,反而能够更有效地管理各种复杂逻辑,包括日志记录、调试、测试以及数据分析等功能。

日志记录

在基类中添加一行代码,就可以监控所有 ViewModel中的所有操作。这样就无需在多个方法中分别添加打印语句了。

func perform(_ action: Action) {
    print("[\(Self.self)] Operation: \(action)")
    // 处理相应操作……
}

调试

如果状态出现了错误,只需在perform()方法中设置一个断点,就能清楚地看到导致当前状态的一系列操作顺序。相比之下,如果在十个不同的方法中分别设置断点,会显得繁琐得多。

测试

使用这种设计模式后,测试代码会变得更加易于理解。因为所有操作都会经过相同的处理路径,所以你测试的其实就是真实应用中使用的那条代码路径。

viewModel.perform(.refresh)
viewModel.perform(.selectWorkout("123"))
viewModel.perform(.delete("123"))
XCTAssertEqual(viewModel.state.workouts, .finished([]))

分析功能

每个用户操作都会被自动记录下来。

func perform(_ action: Action) {
   analytics.track(action)
   // 处理该操作…
}

这个功能并不是什么新鲜事物。在Android开发中,这是一种标准的做法,谷歌的官方架构指南也推荐使用这种机制。Android开发者将这种方式称为单向数据流:事件向下流动(View → ViewModel → Repository),而状态则向上流动(Repository → ViewModel → View)。onAction()方法就是这种向下数据流的入口点。

谷歌的“Now in Android”示例应用就采用了这种模式,大多数Kotlin开发者也都在使用这种方式。当Android开发者加入一个新项目时,他们通常会看到一个Action枚举类型以及一个onAction()方法。

Swift语言中的实现

下面来看看如何将这种模式应用到iOS系统中:

class ViewModel〈State, Action〉: ObservableObject {
   @Published private(set) var state: State

   init(state: State) {
       self.state = state
   }

   func perform(_ action: Action) {
       // 子类可以重写此方法
   }

   func updateState(changing keyPath: WritableKeyPath〈State, some Any〉, to value: some Any) {
       state[keyPath: keyPath] = value
   }
}

状态可以从任何地方被读取,但只有在ViewModel内部才能被修改。这种设计确保了单向数据流的存在:View可以读取状态,但不能直接修改它;所有的更改都必须通过perform()方法来完成。

你也可以将这种机制定义为一个扩展函数,但使用基类可以让开发者将一些通用的逻辑(如日志记录、分析功能以及状态更新规则)集中放在一个地方。所有ViewModel都会继承这些默认行为。

子类可以通过重写这个基类方法来处理自己特定的操作。

下面是一个使用这种模式的完整 ViewModel示例:

class DashboardViewModel: ViewModel〈DashboardViewModel.State, DashboardViewModel.Action〉 {
   struct State {
       var workouts: Loadable〈[Workout]> = .loading
       var selectedTab: Tab = .dashboard
   }

   enum Action {
       case refresh
       case selectTab(Tab)
       case deleteWorkout(String)
   }

   override func perform(_ action: Action) {
       switch action {
           case .refresh:
               Task { await loadWorkouts() }
           case .selectTab(let tab):
               updateState(\.selectedTab, to: tab)
           case .deleteWorkout(let id):
               Task { await deleteWorkout(id) }
       }
   }

   private func loadWorkouts() async {
       updateState(\.workouts, to: .loading)
       do {
           let workouts = try await repository.fetchWorkouts()
           updateState(\.workouts, to: .finished(workouts))
       } catch {
           updateState(\.workouts, to: .error(error))
       }
   }

   private func deleteWorkout(_ id: String) async {
       // 实现细节
   }
}

请注意其结构:

  • `State`是一个结构体,包含了`ViewModel`中的所有数据。
  • `Action`是一个枚举类型,列出了用户可能的所有操作意图。
  • `perform()`是唯一的入口点,它会将请求路由到相应的私有方法中。
  • 私有方法们负责实际执行工作。

`Action`这个枚举类型代表了公共的接口规范;而私有方法则属于实现细节。看到这样的代码结构,你就能立刻明白它的功能是什么。

屏幕与视图:缺失的那层机制

我们已经解决了状态管理和操作路由的问题,但还有一个问题存在:代码耦合度过高。各个视图都各自拥有自己的`ViewModel`,这种设计不仅影响了预览功能的正常使用,也限制了代码的可复用性。

来看这个标准的视图示例:

struct DashboardView: View {
    @StateObject private var viewModel = DashboardViewModel()

    var body: some View {
        ScrollView {
            switch viewModel.workouts {
            case .loading: ProgressView()
            case .finished(let data): WorkoutList(data)
            case .error(let error): ErrorView(error)
            }
        }
    }
}

这个视图同时承担了两项任务:它既负责创建并管理自己的`ViewModel`,也负责渲染用户界面(包括布局各个视图以及处理不同的逻辑判断)。

预览功能存在的问题

试着在Xcode中预览这个视图:

#Preview {
    DashboardView()
}

这个视图会实际创建一个`ViewModel`对象。该`ViewModel`可能会发起网络请求,或者需要一些在预览环境中并不存在的依赖资源,从而导致程序崩溃。

因此人们不得不采取一些变通措施:

#Preview {
    DashboardView(viewModel: MockDashboardViewModel())
}

但这样一来,要么你需要使用模拟的`ViewModel`,要么就需要修改原始代码中的初始化逻辑;而模拟`ViewModel`的维护成本通常很高。因此很多iOS开发者最终放弃了预览功能。

结果就是,预览功能变成了那种“试过一次但无法可靠地使用,最终被弃用”的存在。

代码复用性方面的问题

假设你想在两个地方显示相同的锻炼列表:仪表盘界面和搜索结果页面。按照目前的代码结构,你无法重复使用`DashboardView`,因为它会自行创建一个`DashboardViewModel`对象。

你可以尝试只提取出列表部分的功能:

struct WorkoutList: View {
    let workouts: [Workout]
    var body: some View { ... }
}

但这样一来,加载状态和错误处理逻辑就被丢失了。于是你又尝试提取更多的内容:

struct WorkoutListContainer: View {
    let state: Loadable<[Workout]>
    var body: some View {
        switch state {
        case .loading: ProgressView()
        case .finished(let data): WorkoutList(data)
        case .error(let error): ErrorView(error)
        }
    }
}

现在我们有了`DashboardView`、`WorkoutList`和`WorkoutListContainer`这三个组件。但这种拆分方式并没有遵循任何明确的规则;其他开发者看到这样的代码结构,也会不知道应该遵循哪种设计模式。

Kotlin是如何解决这个问题的

Now in Android示例应用中,存在一种标准的设计模式:将界面与业务逻辑分离。界面层是一个用于管理ViewModel的包装器,而业务逻辑部分则是由专门用于渲染用户界面的组件构成的。

@Composable
fun DashboardScreen(
    viewModel: DashboardViewModel = hiltViewModel()
) {
    val state = viewModel.state.collectAsState()
   DashboardContent(
      state = state,
      onAction = viewModel::onAction
   )
}
@Composable
fun DashboardContent(
   state: DashboardState,
   onAction: (DashboardAction) -> Unit
) {
   // 纯粹的用户界面渲染代码
   Column {
       when (state.workouts) {
           is Loading -> CircularProgressIndicator()
           is Success -> WorkoutList(state.workouts.data)
           is Error ->ErrorMessage(state.workouts.message)
       }
   }
}
@Preview
@Composable
fun DashboardContentPreview() {
   DashboardContent(
      state = DashboardState(workouts = Success(sampleWorkouts)),
      onAction = {}
   )
}

将这一设计模式应用到iOS上

struct DashboardContent: View {
   let state: DashboardViewModel.State
   let onAction: (DashboardViewModel.Action) -> Void
   var body: some View {
       ScrollView {
           switch state.workouts {
               case .loading:
                   ProgressView()
               case .finished(let workouts):
                   WorkoutList(workouts, onAction: onAction)
               case .error(let error):
                   ErrorView(error, onRetry: { onAction(.refresh) })
           }
       }
   }
}
@ObservedObject或@StateObject注解,也没有引用ViewModel对象,整个组件只负责处理数据并渲染用户界面。

struct DashboardScreen: View {
   @StateObject private var viewModel: DashboardViewModel
   init(viewModel: DashboardViewModel) {
       stateObject(wrappedValue: viewModel)
   }
   var body: some View {
       DashboardContent(
           state: viewModel.state,
           onAction: viewModel.perform
       )
       onAppear {
           viewModel.perform(.refresh)
       }
   }
}

通用屏幕封装组件

这种屏幕组件可以被重复使用,属于通用的设计模式:

protocol ViewModeling: ObservableObject {
    associatedtype State
    var state: State { get }
}
struct Screen〈VM:ViewModeling, Content: View〉: View {
    @ObservedObject var viewModel: VM
    let content: (VM.State) -> Content
    var body: some View {
      content(viewModel.state)
   }
}

现在,任何屏幕都可以被这样实现:

struct DashboardScreen: View {
   @ObservedObject var viewModel: DashboardViewModel
   var body: some View {
      ScreenviewModel: viewModel) { state, onAction in
         WorkoutList(state.workouts) {
           viewModel.perform(.action)
         }
      }
   }
}

数据流链条

我们之前已经讨论过各个单独的设计模式。现在,让我们看看这些模式是如何组合成一个由多个层次构成的完整系统的。

图1:视图与API之间的关系。

图1展示了从视图到API的层次关系:

视图 → 视模型 → 数据库层 → 远程数据源 → API

而状态数据则反向流动:

API → 远程数据源 → 数据库层 → 视模型 → 视图

每一层只了解它下面的那一层。视图并不知道数据库层的存在,数据库层也不知道视图的存在。各种依赖关系都是单向的。

为什么需要这么多层次呢?因为每一层都可以被独立地进行测试、模拟或替换。如果将远程数据源替换为假数据源,数据库层仍然可以离线工作;如果将数据库层替换为模拟对象,那么对视模型的测试就不会涉及网络请求。

// 视图
Button(onClick = { viewModel.onAction(ActionRefresh) })

// 视模型
fun onAction(action: Action) {
   when (action) {
       is Action Refresh -> viewModelScope.launch {
           _state.value = State(workouts = Resource.Loading)
           _state.value = State(workouts = repository.getWorkouts())
       }
   }
}

// 数据库层
suspend fun getWorkouts() = remoteSource.fetchWorkouts()

// 远程数据源层
suspend fun fetchWorkouts() = api.getWorkouts().map { it.toDomain() }

iOS平台的对应实现

// 视图
Button("刷新") { onAction(.refresh) }

// 视模型
class DashboardViewModel: ViewModel〈State, Route, Action〉 {
   private let meRepo: MeRepository

   init(dependencies: Resolver) {
       self.meRepo = dependencies.resolve(MeRepository.self)!
       super.initdependsencies: dependencies, state: State())
   }

   override func perform(_ action: Action) {
       switch action {
       case .refresh:
           updateState(changing: \.workouts, to: .loading)
           Task {
               let data = try await meRepo.fetchTrainingLoad()
               updateState(changing: \.workouts, to: .finished(data))
           }
       }
   }
}

// 数据库层协议
protocol MeRepository {
   func fetchTrainingLoad() async throws -> [TrainingLoad]
}

class MeRepositoryImpl: MeRepository {
   private let remoteSource: MeRemoteSource

   init(dependencies: Resolver) {
       self.remoteSource = dependencies.resolve(MeRemoteSource.self)!
   }

   func fetchTrainingLoad() async throws -> [TrainingLoad] {
       try await remoteSource.fetchTrainingLoad()
   }
}

// 远程数据源层协议
protocol MeRemoteSource {
   func fetchTrainingLoad() async throws -> [TrainingLoad]
}

class MeRemoteSourceImpl: MeRemoteSource {
   private let api: API

   init(api: API) {
       self.api = api
   }

   func fetchTrainingLoad() async throws -> [TrainingLoad] {
       let response = try await api.query(TrainingLoadQuery())
       return response.me?.trainingLoad.map { TrainingLoad(from: $0) } ?? []
   }
}

反应式仓库模式

在这里,前面介绍的架构设计真正展现出了它的优势。

想象这样的场景:你的应用程序有两个界面:一个锻炼计划列表页面和一个锻炼计划详情页面。用户打开某个锻炼计划,修改了其名称,然后返回列表页面,但列表中仍然显示着原来的名称。为什么会出现这种情况呢?因为每个界面都保存着自己的一份数据副本。详情页面修改了数据副本,而列表页面却不知道数据已经发生了变化。SwiftUI中的`@Binding`机制可以解决这类问题,尤其是在处理简单的父子关系时。你也可以通过回调函数来更新数据,或者使用`onAppear`方法来刷新界面。但一旦应用程序包含三个以上的界面,或者有多个独立的功能模块需要使用相同的数据,这些方法就不再适用了。这时,你就需要一个“唯一的数据来源”。

数据由仓库统一管理

如果只存在一份数据副本该多好?所有界面都会引用这份唯一的副本。只要更新一次数据,所有界面都能立即看到变化。

反应式仓库模式正是实现了这一目标。以下是它在Swift中的实现方式:

class WorkoutRepository {
protocol WorkoutRepository {
    var workoutsPublisher: AnyPublisher〈[Workout], Never〉 { get }
    func updateWorkoutName(id: String, newName: String) async throws
}
final class WorkoutRepositoryImpl: WorkoutRepository {
    private let remoteSource: WorkoutRemoteSource
    @Published private var workouts: [Workout] = []
    var workoutsPublisher: AnyPublisher〈[Workout], Never〉 {
      $workouts.eraseToAnyPublisher()
   }
   init(remoteSource: WorkoutRemoteSource) {
      self.remoteSource = remoteSource
   }
   func updateWorkoutName(id: String, newName: String) async throws {
      // 1. 更新后端数据
      try await remoteSource.updateWorkout(id: id, name: newName)
      // 2. 更新本地数据
      if let index = workouts.firstIndex(where: { $0.id == id }) {
         workouts[index].name = newName
      }
      // 所有依赖该数据的界面都会自动收到更新通知
   }
}

仓库负责存储数据,各个视图模型则会订阅这些数据:

class WorkoutListViewModel: ViewModel〈State, Action〉 {
   private let repository: WorkoutRepository
   private var cancellables = Set〈AnyCancellable〉()
   init(repository: WorkoutRepository) {
      self.repository = repository
      super.init(state: State())
      repository.workoutsPublisher
         .sink { [weak self] workouts in
            self?.updateState(\.workouts, to: .finished(workouts))
         }
      store(in: &cancellables)
   }
}

所有的视图模型都订阅着同一个数据来源。当仓库中的数据发生变化时,所有相关界面都会立即得到更新。不需要回调函数、通知机制,也不需要手动刷新界面。而在进行测试时,只需注入模拟数据即可。

“唯一的数据来源”原则

谷歌的架构指南将这种设计称为“唯一的数据来源”原则。对于任何一份数据来说,都只有一个负责管理它的实体,其他所有组件都只是观察这份数据的变动而已。

  • 锻炼记录?由 `WorkoutRepository` 管理
  • 用户资料?由 `UserRepository` 管理
  • 设置信息?由 `SettingsRepository` 管理

视图模型本身并不拥有数据。它们只是观察这些数据,并将它们呈现给用户界面。当需要修改某些数据时,视图模型会向相应的存储库请求更新。存储库会更新其内部状态,然后这种变化会被所有依赖该数据的组件自动接收并显示出来。

完整的数据流处理流程

当用户修改某项锻炼记录的名称时:

  • DetailViewModel.perform(.updateName("新名称"))
  • DetailViewModel 调用 repository.updateWorkoutName(...)
  • 存储库 会与后端系统进行通信并更新数据库
  • 存储库 会更新其内部存储的锻炼记录列表
  • ListViewModel 通过订阅机制获取到更新后的锻炼记录列表
  • DetailViewModel 也通过订阅机制获取到更新后的信息
  • 最终,两个用户界面都会显示更新后的名称

这样的设计能够确保数据变更能够自动传播到所有相关组件。如果你需要添加第三个用于显示锻炼记录的界面,只需让它订阅相同的存储库即可,无需对其他任何代码进行修改。

正是这种设计模式使得大型应用程序得以顺利维护。如果没有这样的模式,你在处理遍布数十个界面的陈旧数据时,将会面临巨大的麻烦。

结论

优秀的架构设计是跨平台通用的。通过采用在 Android 生态系统中被证明有效的设计模式,比如明确的状态管理机制、基于具体操作的更新流程、分层的数据流结构以及响应式存储库模型,我们能够开发出如下特点的 iOS 应用程序:

  • 由于职责划分清晰,这些应用程序易于维护
  • 每一层功能都可以被模拟测试,因此这些应用程序也便于进行测试
  • 无论应用程序包含五个界面还是五十个界面,这种设计模式都能保证其良好的扩展性
  • 由于数据流是单向的,因此调试过程也非常简单明了

我们没有必要重复发明轮子。我们可以借鉴其他平台上的成功经验,并将其适配到自己的开发环境中。这样一来,我们就能编写出结构更清晰、随着应用规模扩大而不会出现运行效率下降问题的应用程序。

Comments are closed.