1. Introduction
在去年引入可组合体系结构时,我们特别强调了副作用和测试。事实上,我们用了整整8集的时间来讨论这些话题。这是因为每个架构都需要有一个关于副作用的故事:这只是生活中的一个事实。但是,一旦引入副作用,就有破坏可测试性的风险。
然而,我们能够让副作用和可测试性和谐共存,通过使用一种我们在近两年前谈到的老技术:environment。环境提供了一个地方来放置所有可能导致副作用的东西,并且我们禁止自己使用任何依赖项,除非它存储在环境中。这为我们提供了一种一致的访问依赖项的方式,并且使得在测试和playgrounds中将依赖项替换为受控依赖项变得很简单,如果我们愿意,我们甚至可以在运行实际应用程序时使用模拟依赖项。 如果你无网状态,或者想要在一个非常特定的状态下测试你的应用程序,这可能特别有用。
然而,我们给出的解决方案并不是全部。它在很多情况下都工作得很好,但是它有一些问题,我们可以设计一个更加健壮和通用的解决方案来解决可组合体系结构中的依赖关系问题。为了看到这一切,我们所要做的就是对reducer的签名做一个非常小的调整,一切都会自然地随之而来。
但在我们进入这之前,让我们提醒自己我们是如何使用environment技术来控制和测试我们的副作用的。
2. Effects recap
回想一下,我们在可组合体系结构中表示副作用的方式是从我们的reducer返回一个effect值数组,然后在幕后store负责获取所有这些effects并运行它们。
public typealias Reducer<Value, Action> = (inout Value, Action) -> [Effect<Action>]
Effect是我们构建的自定义类型,它符合Combine框架中的Publisher协议:
public struct Effect<Output>: Publisher {
public typealias Failure = Never
let publisher: AnyPublisher<Output, Failure>
public func receive<S>(
subscriber: S
) where S: Subscriber, Failure == S.Failure, Output == S.Input {
self.publisher.receive(subscriber: subscriber)
}
}
这允许我们利用Combine来提升我们的effects。我们可以使用它们的助手来执行网络请求,在模型中解码JSON,定时器,以及所有你可以与publishers做的有趣的转换,如map, zip, flatMap,filter等等。
我们的Effect只是简单地包装了一个现有的publisher,我们这样做是因为我们想添加自己的方便助手,而不污染其他所有人的publisherAPI。我们有像fireAndForget函数这样的助手:
extension Effect {
public static func fireAndForget(work: @escaping () -> Void) -> Effect {
return Deferred { () -> Empty<Output, Never> in
work()
return Empty(completeImmediately: true)
}.eraseToEffect()
}
}
这让我们可以触发一个需要做一些工作的副作用,但不需要向系统反馈任何数据。这方面的例子可以是日志记录和分析跟踪,我们甚至使用这种风格效果将数字列表保存到磁盘上。
我们也有一个helper来执行一些同步工作,在我们需要与外部世界交互的时候,但它不一定需要异步完成:
extension Effect {
public static func sync(work: @escaping () -> Output) -> Effect {
return Deferred {
Just(work())
}.eraseToEffect()
}
}
例如,我们使用它来执行从磁盘加载一些数据的效果。
更普遍的是,我们可以在Combine中自由使用任何api来构造和转换publishers,然后在工作的最后,你可以使用eraseToEffect助手将你的publisher转换成一个effect :
extension Publisher where Failure == Never {
public func eraseToEffect() -> Effect<Output> {
return Effect(publisher: self.eraseToAnyPublisher())
}
}
我们将这些effects引入应用程序的方法是构造并从reducer返回它们。
举个简单的例子,假设我们想在点击减量按钮时在计数器视图中打印一些东西。我们可以简单地返回一个fireAndForget效果来进行打印:
public func counterReducer(state: inout CounterState, action: CounterAction) -> [Effect<CounterAction>] {
switch action {
case .decrTapped:
state.count -= 1
return [
.fireAndForget {
print(state.count)
}
]
🛑 Escaping closure captures ‘inout’ parameter ‘state’
这现在不起作用,因为我们正在试图访问逃逸闭包中的inout状态变量。这是一个非常好的错误,因为如果Swift允许这样做,就意味着我们可以在稍后执行的闭包中改变我们的状态。事实上,这个闭包可以在创建后的10秒内执行,这意味着一些神秘的突变正在reducer权限之外发生,而这正是我们想要在架构中防止的。
然而,如果我们在闭包外部得到一个不可变的计数引用,一切都会正常工作:
case .decrTapped:
state.count -= 1
let count = state.count
return [
.fireAndForget {
print(count)
}
]
如果我们想做一些更复杂的事情呢? 如果在点击减量按钮之后,我们想要等待1秒,然后增加计数呢? 这当然是一件愚蠢的事情,但它是一个相当复杂的效果想要处理。幸运的是,我们可以简单地使用Combine框架:
return [
.fireAndForget {
print("Decr Tapped!", count)
},
Just(.incrTapped)
.delay(for: 1, scheduler: DispatchQueue.main)
.eraseToEffect()
]
在这里,我们将想要发送的动作封装到一个Just publisher中,这个publisher会立即发出它的值。然后我们将它的发射延迟1秒,并最终把publisher 类型擦除为一个effect。
这三行代码很有冲击力,意味着现在如果我们运行应用,每次我们尝试减少它都会在一秒后撤销。即使我们多次点击它,它最终也会抵消掉所有的工作。
3. Environment recap
这就是effects。它们让我们能够完成需要与外界互动的工作,而这些工作是由reducers自己无法完成的。但副作用也很难测试,所以为了控制我们的影响,我们转向了environment技术。
为了采用这种技术,我们首先定义一个environment结构,它包含我们的应用程序需要访问的所有依赖项。例如,Counter模块只需要访问一个用于计算“第n个素数”的函数,该函数通常会向一个强大的计算平台Wolfram Alpha发出网络请求:
struct CounterEnvironment {
var nthPrime: (Int) -> Effect<Int?>
}
我们将这个字段设置为var,因为它可以很容易地在tests、playgrounds和staged applications中将实时依赖项替换为模拟依赖项。
作为这项技术的一部分,我们也喜欢提供对环境的实时和模拟实现的简单访问,表示为静态:
extension CounterEnvironment {
static let live = CounterEnvironment(nthPrime: Counter.nthPrime)
}
extension CounterEnvironment {
static let mock = CounterEnvironment(nthPrime: { _ in .sync { 17 }})
}
到目前为止,这些似乎都没有争议,但接下来我们做了一些非常奇怪的事情:我们定义了这个environment的全局可变实例,默认为live environment:
var Current = CounterEnvironment.live
然后我们强迫自己永远不要接触dependency,除非它存储在environment中。
例如,当我们点击按钮询问“第n个质数”是什么时,我们通过当前environment执行effect:
case .nthPrimeButtonTapped:
state.isNthPrimeButtonDisabled = true
return [
Current.nthPrime(state.count)
.map(CounterAction.nthPrimeResponse)
.receive(on: DispatchQueue.main)
.eraseToEffect()
]
这个可变变量可能看起来很奇怪,但它允许我们在一行中轻松模拟整个环境。例如,在计数器测试中,我们在setUp块中执行了以下操作:
class CounterTests: XCTestCase {
override func setUp() {
super.setUp()
Current = .mock
}
}
这意味着我们可以保证每个测试运行在一个受控制的环境中,但我们也可以通过重写任何依赖项来进一步调整每个测试用例中的环境,例如:
func testNthPrimeButtonHappyFlow() {
Current.nthPrime = { _ in .sync { 17 } }
assert(
4. Current problems
这就是环境技术的工作原理。它非常容易上手,为我们提供了一种单一的、一致的方式来访问我们的依赖关系,同时模拟所有的依赖关系是微不足道的。
然而,这并不是没有问题。
也许最明显的问题是,我们定义的每个environments都位于它们自己的模块中,因此是完全断开连接的。请记住,我们对架构的组合减速器方法的好处之一是,我们可以编写类似集成的测试,一次测试应用程序的多个层。
例如,我们编写了一个测试,表明计数器特征和素数模态特征正确集成。特别地,我们模拟了这样一个想法:用户将计数器加1,然后从他们喜欢的质数中添加或删除这个数字。代码是这样的:
func testPrimeModal() {
assert(
initialValue: CounterViewState(
count: 1,
favoritePrimes: [3, 5]
),
reducer: counterViewReducer,
steps:
Step(.send, .counter(.incrTapped)) {
$0.count = 2
},
Step(.send, .primeModal(.saveFavoritePrimeTapped)) {
$0.favoritePrimes = [3, 5, 2]
},
Step(.send, .primeModal(.removeFavoritePrimeTapped)) {
$0.favoritePrimes = [3, 5]
}
)
}
这是不可思议的强大,我们基本上从我们的可组合reducers免费得到它。
然而,我们并没有在这个步骤脚本中测试任何效果,所以我们没有看到全貌。现在只有Counter模块有任何效果,PrimeModal模块没有任何效果,所以很难看出这里的复杂性。为了模拟所有的effects,我们需要做的就是:
func testPrimeModal() {
Current = .mock
assert(
但是,如果我们考虑为我们的主要应用目标编写集成测试,即将所有内容组合到一个大appReducer中,我们将更好地了解需要什么。让我们首先清理PrimeTimeTests文件:
import XCTest
@testable import PrimeTime
class PrimeTimeTests: XCTestCase {
}
如果我们想为此编写一个集成测试,我们首先需要导入所有的功能:
@testable import Counter
@testable import FavoritePrimes
@testable import PrimeModal
我们可以引入一个测试函数:
class PrimeTimeTests: XCTestCase {
func testIntegration() {
}
}
现在,在这个测试中,我们想要确保我们处于一个完全受控的环境中,因此我们需要模拟每个模块的环境。 这意味着Counter模块和FavoritePrimes模块:
class PrimeTimeTests: XCTestCase {
func testIntegration() {
Counter.Current = .mock
FavoritePrimes.Current = .mock
}
}
这有点令人失望。我们并没有从编译器那里得到任何静态帮助来确保我们正确地执行了这个操作。随着我们向应用程序添加越来越多的特性,我们将创建新的模块来容纳这些特性,每个模块都将拥有environments,没有任何东西会迫使我们确保我们已经模拟出了这些environments。我们冒着在测试期间调用活动依赖项的风险。
这对你和你的团队来说可能不是最大的问题。当然,你可以把一些流程放到合适的地方,帮助捕捉这样的事情。但这是需要记住的,理想情况下应该有一个更普遍的解决方案。
这种方法的另一个问题是,跨模块共享依赖项并不容易。例如,FavoritePrimes模块持有一个FileClient,我们用它来保存和加载数据到磁盘。如果我们想在另一个模块中使用同样的依赖,我们很可能必须构造一个新的文件客户端并在该模块中使用它。否则,我们需要在更高的层次上进行一些协调。例如,在应用程序委托中,我们可以做一些额外的工作,以确保FavoritePrimes模块和其他模块共享相同的文件客户端。我们还必须确保在测试中做同样的工作,以确保所有模块都使用相同的依赖项进行测试。 这是有可能解决的,但理想情况下会有一个更普遍的解决方案,
最后,这种形式的environment技术的另一个问题是,模块中所有事物的实例只能使用该environment中的一个真实environment。例如,我们不能创建一个计数器视图,它使用Wolfram API来做它的计算,但同时也创建一个计数器视图,它使用一些其他API来做它的计算。这使得我们每个模块中的代码的灵活性有所降低。
所有这些问题的解决方案是,我们应该显式地将依赖关系传递给需要这些依赖关系的函数。如果您为一个函数提供了完成其工作所需的一切,那么测试和控制它们就很简单了。
然而,这并不总是可能或容易做到的。在现实世界的代码基础中,可能难以置信地或甚至不可能像您希望的那样传递依赖关系。您可能有遗留的代码使它变得困难,或者您可能有抽象层阻止您做您想做的事情。这就是环境技术真正发挥作用的地方。它允许您在任何代码库中立即获得一些可测试性和对依赖项的一些控制。
5. Environment in the reducer
然而,对于可组合体系结构,情况有些不同。它为我们提供了一种单一、一致的方式来构建整个应用程序的特性。如果我们可以让架构意识到环境,那么我们就不需要接触全局环境,因为我们可以自动地使用它。
首先要做的是reducer,因为reducer负责产生后来由store运行的effects。为了使这些effects是可控的,我们需要以可控的方式访问它的依赖关系。之前我们接触了模块的环境,但是如果reducer内部有一个可用的环境呢?
我们的reducer不是这样的
public typealias Reducer<Value, Action> = (inout Value, Action) -> [Effect<Action>]
我们将使reducer在当前值和动作之外提供一个环境,这意味着引入另一个泛型:
public typealias Reducer<Value, Action, Environment> = (inout Value, Action) -> [Effect<Action>]
我们可以使用这种通用的一种方法是对reducer进行curry处理,这样首先给它一个环境,然后它将返回一个传统的reducer签名:
public typealias Reducer<Value, Action, Environment> = (Environment) -> (inout Value, Action) -> [Effect<Action>]
这是完全有效的,事实上,这是我们如何控制副作用在我们的第二集Point-Free。在那一节中,我们将一个日期值放入函数中,以便我们能够控制它。
然而,对reducer使用这个签名将意味着总是从我们的reducer返回一个函数,这将在一段时间后变得很烦人。 值得庆幸的是,有一个等价的公式,我们只是在环境中通过state和action:
public typealias Reducer<Value, Action, Environment> = (inout Value, Action, Environment) -> [Effect<Action>]
我们甚至可以把函数往另一个方向curry:
//public typealias Reducer<Value, Action, Environment> = (inout Value, Action) -> (Environment) -> [Effect<Action>]
我们不会那样做,但它也是定义reducers的有效方法,我们有练习来探索这个。
这个Environment通用型使我们有机会提供reducer所需的任何依赖项,以产生增强其逻辑的效果。
这当然会破坏很多东西,所以让我们开始修复。让我们首先关注ComposableArchitecture模块。
我们看到的第一个编译器错误是在combine函数中,该函数取一堆相同类型的reducer并将它们组合成一个巨型的reducer。为了解决这个问题,我们只需要引入环境泛型,并将其传递给每一个sub-reducers:
public func combine<Value, Action, Environment>(
_ reducers: Reducer<Value, Action, Environment>...
) -> Reducer<Value, Action, Environment> {
return { value, action, environment in
let effects = reducers.flatMap { $0(&value, action, environment) }
return effects
}
}
很简单。接下来是pullback,它现在遇到了麻烦,因为我们再次需要考虑新环境的通用情况。最简单的解决方法是引入一个Environment泛型,并在执行pullback时将Environment传递给本地reducer。
public func pullback<LocalValue, GlobalValue, LocalAction, GlobalAction, Environment>(
_ reducer: @escaping Reducer<LocalValue, LocalAction, Environment>,
value: WritableKeyPath<GlobalValue, LocalValue>,
action: CasePath<GlobalAction, LocalAction>
) -> Reducer<GlobalValue, GlobalAction, Environment> {
return { globalValue, globalAction, environment in
guard let localAction = action.extract(globalAction) else { return [] }
let localEffects = reducer(&globalValue[keyPath: value], localAction, environment)
return localEffects.map { localEffect in
localEffect.map(action.embed)
.eraseToEffect()
}
}
}
这可以正常build,但并不理想。 reducers上的pullback操作完全是关于能够将在局部域上工作的reducers转换为在全局域上工作的reducers。这对于模块化很方便,因为它允许我们将我们的功能分解成只包含该功能真正关心的域类型的模块,然后应用程序目标可以将这些域组装成完整的应用程序域。
为了维护这种模块化,并将局部环境与全局环境的无关细节隔离开来,我们还希望能够转换环境。
因此,让我们为局部和全局环境引入泛型,并看看环境转换需要成为什么样的形状:
public func pullback<LocalValue, GlobalValue, LocalAction, GlobalAction, LocalEnvironment, GlobalEnvironment>(
_ reducer: @escaping Reducer<LocalValue, LocalAction, LocalEnvironment>,
value: WritableKeyPath<GlobalValue, LocalValue>,
action: CasePath<GlobalAction, LocalAction>,
environment: ???
) -> Reducer<GlobalValue, GlobalAction, GlobalEnvironment> {
在这个函数的函数体中,我们需要返回一个新的reducer,它可以在全局环境下工作:
return { globalValue, globalAction, globalEnvironment in
然后,当我们尝试运行本地reducer时,我们需要为它提供一个本地环境。似乎我们需要一种方法,将现有的全球环境转变为局部环境,而这就决定了我们需要采取的转变方式:
environment: @escaping (GlobalEnvironment) -> LocalEnvironment
然后我们可以简单地通过以下操作来修复回调:
return { globalValue, globalAction, globalEnvironment in
guard let localAction = action.extract(globalAction) else { return [] }
let localEffects = reducer(&globalValue[keyPath: value], localAction, environment(globalEnvironment))
return localEffects.map { localEffect in
localEffect.map(action.embed)
.eraseToEffect()
}
}
就像pullback一样现在编译。关于这一点:
-
请注意,环境转换的方向与价值和行动转换的方向相同。这三种方法都是从全局到局部的。所以这个变换在它的每一个泛型中仍然是逆变的,这就是为什么pullback仍然是这个操作的好名字。
-
我们还想说,沿着普通函数而不是关键路径或case路径pullback环境是完全可以的。关键路径和case路径之所以必不可少,是因为我们需要一个更强大的工具来分离状态和行为并将它们粘在一起。但是对于环境,我们只需要将局部环境从全局环境中投影出来,一个简单的函数就可以做到这一点。
好了,这个模块中的下一个编译器错误是日志高阶减速器,它允许我们向任何减速器添加日志记录功能。我们只需要确保将环境传递给我们正在添加日志的减速机:
public func logging<Value, Action, Environment>(
_ reducer: @escaping Reducer<Value, Action, Environment>
) -> Reducer<Value, Action, Environment> {
return { value, action, environment in
let effects = reducer(&value, action, environment)
let newValue = value
return [.fireAndForget {
print("Action: \(action)")
print("Value:")
dump(newValue)
print("---")
}] + effects
}
}
现在,我们甚至可以通过要求调用者提供一个保存打印函数的环境来改进这个高阶减速器。就像这样,我们更新了reducers和它们的转换函数来适应环境,这是非常简单的。
6. Environment in the store
但我们在Store类型中有一堆错误。
首先,它持有的Reducer现在有3个泛型:
private let reducer: Reducer<Value, Action>
🛑 Generic type ‘Reducer’ specialized with too few type parameters (got 2, but expected 3)
我们需要以某种方式提供这个泛型。似乎没有太多的其他我们可以做,除了添加泛型到我们的商店:
public final class Store<Value, Action, Environment>: ObservableObject {
private let reducer: Reducer<Value, Action, Environment>
这可能是合理的,毕竟我们似乎有相同的形状的Reducer和Store的形状。
下一个错误是在Store的初始化式中,它只需要新的泛型:
public init(initialValue: Value, reducer: @escaping Reducer<Value, Action, Environment>) {
接下来,我们在send方法中出现错误,因为我们试图在不提供环境的情况下调用reducer:
public func send(_ action: Action) {
let effects = self.reducer(&self.value, action)
🛑 Missing argument for parameter #3 in call
我们要从哪里得到这样的环境? 似乎我们应该在创建Store时为其提供环境。这样,它就可以抓住环境,并在任何动作出现时将环境传递给它的Reducer。让我们这样做:
public final class Store<Value, Action, Environment>: ObservableObject {
private let reducer: Reducer<Value, Action, Environment>
private let environment: Environment
@Published public private(set) var value: Value
private var viewCancellable: Cancellable?
private var effectCancellables: Set<AnyCancellable> = []
public init(
initialValue: Value,
reducer: @escaping Reducer<Value, Action, Environment>,
environment: Environment
) {
self.reducer = reducer
self.value = initialValue
self.environment = environment
}
现在我们有一些东西要传递给Reducer:
public func send(_ action: Action) {
let effects = self.reducer(&self.value, action, self.environment)
到目前为止,这些错误都很容易修复。
但下一个有点棘手。它在Store的view方法中,该方法允许我们将Store转换为只公开局部域的Store。 这就是我们如何将Store放在一个SwiftUI视图中,并将其传递给一个更小的视图,该视图只需要父视图的状态和操作的一部分。
现在它正在抱怨,因为我们缺少第三个泛型:
public func view<LocalValue, LocalAction>(
value toLocalValue: @escaping (Value) -> LocalValue,
action toGlobalAction: @escaping (LocalAction) -> Action
) -> Store<LocalValue, LocalAction> {
let localStore = Store<LocalValue, LocalAction>(
🛑 Generic type ‘Store’ specialized with too few type parameters (got 2, but expected 3)
由于这个函数是关于将一个全局域上的Store转换为一个局部域上的Store,我们可能会认为我们应该在这里的值和操作转换之外提供另一个转换。
因此,让我们首先为本地环境引入一个泛型和一个新的转换参数:
public func view<LocalValue, LocalAction, LocalEnvironment>(
value toLocalValue: @escaping (Value) -> LocalValue,
action toGlobalAction: @escaping (LocalAction) -> Action,
environment: ???
) -> Store<LocalValue, LocalAction, LocalEnvironment> {
let localStore = Store<LocalValue, LocalAction, LocalEnvironment>(
…
environment: ???
)
如果我们想提供这样的论据,我们会看到我们需要提供一个局部环境,我们手边有一个全局环境,所以这似乎决定了我们的转变应该走向:
public func view<LocalValue, LocalAction, LocalEnvironment>(
value toLocalValue: @escaping (Value) -> LocalValue,
action toGlobalAction: @escaping (LocalAction) -> Action,
environment toLocalEnvironment: @escaping (Environment) -> LocalEnvironment
) -> Store<LocalValue, LocalAction, LocalEnvironment> {
let localStore = Store<LocalValue, LocalAction, LocalEnvironment>(
initialValue: toLocalValue(self.value),
reducer: { localValue, localAction in
self.send(toGlobalAction(localAction))
localValue = toLocalValue(self.value)
return []
},
environment: toLocalEnvironment(self.environment)
)
越来越近了,这个方法中又多了一个编译器错误
🛑 Contextual closure type ‘(inout LocalValue, LocalAction, LocalEnvironment) -> [Effect]’ expects 3 arguments, but 2 were used in closure body
我们在这里实现的reducer使用了reducer的老签名,只有两个参数。我们现在有第三个参数,环境,也就是这里的本地环境
reducer: { localValue, localAction, localEnvironment in
奇怪的是,这就得到了编译的结果。 奇怪的是,我们甚至不需要使用局部环境。实际上,我们可以使用下划线来表示它根本没有被使用:
reducer: { localValue, localAction, _ in
这意味着我们并没有以任何有意义的方式利用环境。我们所做的只是将其转换并将其传递给新Store,但新Store并没有将其全部使用。
7. Erasing the environment from the store
可组合架构模块终于开始构建了,但是我们在构建过程中遇到了一些奇怪的事情。Store上的view方法的实现非常奇怪,这让我怀疑我们是否在做正确的事情。我们需要在视图方法中考虑环境,即使它没有以任何有意义的方式使用。
事实上,Store作为一个整体概念与环境几乎没有关系。Store的用户只关心从它获取状态值并向它发送操作。他们从不访问环境,甚至不需要了解正在底层使用的环境。
我们想要从Store类中“删除”这个类型。也就是说,我们希望从Store中删除泛型,并将环境细节隐藏在类的实现中,而不是将其公开。
关于Swift的类型擦除有很多可说的,但我们足够幸运的是,对于Store类型,擦除这种类型非常容易。让我们从删除环境泛型开始,这样我们可以看到我们需要做什么来解决这个问题:
public final class Store<Value, Action>: ObservableObject {
随着泛型类型的消失,我们不能再保留指定了environment类型的Reducer或环境。然而,我们可以将这些类型替换为Any,这样我们现在就不需要放入具体的类型,但在运行时可以使用任何值:
public final class Store<Value, Action>: ObservableObject {
private let reducer: Reducer<Value, Action, Any>
private let environment: Any
然后,对于初始化器,这是一个我们实际上需要环境信息的地方,我们可以引入一个泛型并将其用于reducer和环境参数:
public init<Environment>(
initialValue: Value,
reducer: @escaping Reducer<Value, Action, Environment>,
environment: Environment
) {
self.reducer = reducer
self.value = initialValue
self.environment = environment
}
这会导致编译器错误,因为它不知道如何在Any环境的reducer和特定环境的reducer之间进行转换:
🛑 Cannot assign value of type ‘(inout Value, Action, Environment) -> [Effect]’ to type ‘(inout Value, Action, Any) -> [Effect]’
我们可以通过在这个初始化器中实现一个自定义的reducer来解决这个问题,该reducer强制将Any值转换为一个合适的Environment值。这听起来可能很危险,但如果我们小心,我们永远不会意外地得到一个我们不期望的环境。我们来试一下:
public init<Environment>(
initialValue: Value,
reducer: @escaping Reducer<Value, Action, Environment>,
environment: Environment
) {
self.reducer = { value, action, environment in
reducer(&value, action, environment as! Environment)
}
self.value = initialValue
self.environment = environment
}
这会得到初始化器的编译,但view方法又有问题了,但我们所要做的就是删除环境泛型并将我们自己的环境传递给新的store:
public func view<LocalValue, LocalAction>(
value toLocalValue: @escaping (Value) -> LocalValue,
action toGlobalAction: @escaping (LocalAction) -> Action
) -> Store<LocalValue, LocalAction> {
let localStore = Store<LocalValue, LocalAction>(
initialValue: toLocalValue(self.value),
reducer: { localValue, localAction, _ in
self.send(toGlobalAction(localAction))
localValue = toLocalValue(self.value)
return []
},
environment: self.environment
)
localStore.viewCancellable = self.$value.sink { [weak localStore] newValue in
localStore?.value = toLocalValue(newValue)
}
return localStore
}
事情正在发展中!强制施法可能看起来很可怕,但也可以不使用强制施法而进行这种类型的擦除。
我们不需要在视图中显式地转换环境,这可能看起来很奇怪,但请记住,环境纯粹是存储中的一个实现细节,而且所有的环境转换都已经应用到reducer上了。
幸运的是,应用程序是完全模块化的,所以我们可以从最简单、依赖最少的模块开始,然后返回到主应用目标。