可组合的建筑

2020-05-05 04:13:49

近9个月前,我们首次开始讨论应用程序架构,在此期间,我们已经建立了一个全面的故事,说明如何以一致且易于理解的方式在SWIFT中为Apple平台构建应用程序。我们集中讨论了几个关键主题:

状态管理:我们如何使用简单的值类型构建应用程序的大部分,以及应用程序的不同部分如何通过共享状态相互通信?

构图:我们如何才能将一个大而复杂的特征分解成小块,然后粘合在一起形成整体?

模块性:将一个大问题分解成几个小问题后,我们如何将每个部分放在各自的模块中,并且它们之间的依赖关系尽可能少,这样我们就可以在不构建整个应用程序的情况下独立运行功能?

副作用:我们如何才能让我们的功能与外部世界进行交流,反之亦然,以一种可理解和可组合的方式?

测试:如何在不牺牲可测试性的情况下完成上述任务?我们想要测试我们的功能,这样设置测试就不需要很多工作,这样我们就可以测试我们系统的非常深层的属性,包括效果是如何执行的,以及它们的输出是如何反馈到功能中的。

适应性:如何一次实现我们功能的核心业务逻辑,同时仍然允许该逻辑在多个平台上使用,如iOS、MacOS、WatchOS和TVOS?

今天,我们很兴奋地宣布,我们终于开源了Composable Architecture,这是一个以一致和可理解的方式构建应用程序的库,考虑到了组合、测试和人体工程学。它可以在SwiftUI、UIKit等应用程序中使用,也可以在任何Apple平台(iOS、MacOS、TVOS和WatchOS)上使用。

要使用Composable Architecture构建功能,您需要定义一些对域进行建模的类型和值:

状态:描述功能执行其逻辑和呈现其UI所需的数据的类型。

操作:表示功能中可能发生的所有操作的类型,例如用户操作、通知、事件源等。

环境:保存功能所需的任何依赖项的类型,如API客户端、分析客户端等。

Reducer:描述如何将应用程序的当前状态演变到给定操作的下一个状态的函数。减少器还负责返回任何应该运行的效果,例如API请求。

Store:实际驱动您的功能的运行时。您将所有用户操作发送到存储区,以便存储区可以运行减速器和效果,并且可以观察存储区中的状态更改,以便更新UI。

这样做的好处是您将立即解锁特性的可测试性,并且您将能够将大型的、复杂的特性分解成可以粘合在一起的较小的域。

作为一个基本示例,考虑一个UI,它显示一个数字以及递增和递减数字的“+”和“−”按钮。为了让事情变得有趣,假设还有一个按钮,当点击该按钮时,它会发出API请求来获取关于该数字的随机事实,然后在警报中显示该事实。

此功能的状态将由当前计数的整数以及表示我们要显示的警报标题的可选字符串组成(可选,因为nil表示不显示警报):

接下来,我们来看一下该功能中的操作。有一些明显的操作,例如轻触减量按钮、递增按钮或数值按钮。但也有一些不太明显的问题,例如用户解除警报的操作,以及当我们收到来自事实API请求的响应时发生的操作:

枚举AppAction{case factAlertDismised案例减量按钮分接案例增量Button分接案例编号FactButton分接案例编号FactResponse(Result<;String,ApiError>;)}struct ApiError:Error{}。

接下来,我们对此功能执行其工作所需的依赖环境进行建模。特别地,要获取数字事实,我们需要构造封装网络请求的Effect值。因此,依赖项是从Int到Effect<;String,ApiError&>函数,其中String表示来自请求的响应。此外,效果通常在后台线程上工作(URLSession就是这种情况),因此我们需要一种在主队列上接收效果值的方法。我们通过主队列调度器来实现这一点,这是一个需要控制的重要依赖项,以便我们可以编写测试。我们必须使用AnyScheduler,这样我们才能在生产中使用活动的DispatchQueue,在测试中使用测试调度器。

接下来,我们实现一个减法器,该减法器实现该域的逻辑。它描述了如何将当前状态更改为下一个状态,并描述了需要执行的效果。有些操作不需要执行Effects,它们可以返回.one来表示这一点:

让appReducer=Reducer<;AppState、AppAction、AppEnvironment>;{切换操作中的state,action,Environment{case.factAlertDismised:state.numFactAlert=nil return.one case.deducmentButtonTaps:state.count-=1 return.one case.incrementButtonTaps:state.count+=1 return.one case.number FactButtonTaps:return Environment.number Fact(state.count).Receive(on:Environmental ment.mainQueue).map(AppAction.number FactResponse).catchch.catchEnter:return Environment.number Fact(state.count).Receive(on:Environmental ment.mainQueue).map(AppAction.number FactResponse).catchch.。无法加载数字事实:(";return.one}}。

最后,我们定义显示该功能的视图。它保留Store<;AppState、AppAction&>,以便它可以观察状态的所有更改并重新呈现,我们可以将所有用户操作发送到存储区,以便状态更改。我们还必须在事实警报周围引入结构包装器以使其可识别,这是.alert视图修饰符所要求的:

struct AppView:View{let Store:Store<;AppState,AppAction&>var Body:Some View{WithViewStore(self.store){VStack中的ViewStore{HStack{Button(";−";){viewStore.send(.downmentButtonTaps)}Text(";\(viewStore.count)";)按钮(";+";){viewStore.send。){viewStore.send(.numFactButtonTaps)}}.alert(Item:viewStore.binding(Get:{$0.numberFactAlert.map(FactAlert.init(title:))},Send:.factAlertDismissed),内容:{Alert(Title:Text($0.title)})}struct FactAlert:可识别{var title:string var id:string{self.title}}

重要的是要注意,我们能够实现整个功能,而手头没有真正的现场效果。这一点很重要,因为它意味着可以隔离构建功能,而无需构建它们的依赖关系,这有助于编译时间。

把UIKit控制器赶出这个商店也很简单。您可以在viewDidLoad中订阅商店,以便更新UI并显示警报。该代码比SwiftUI版本稍长,因此我们将其折叠为:

类AppViewController:UIViewController{let viewStore:ViewStore<;AppState,AppAction>;var Cancellable:set<;AnyCancerable>;=[]init(store:store<;AppState,AppAction>;){self.viewStore=ViewStore(Store)super.init(nibName:nil,bundle:nil)}是否需要初始化?(编码器:NSCoder){defalError。)}覆盖函数viewDidLoad(){super.viewDidLoad()let countLabel=UILabel()let incrementButton=UIButton()let downmentButton=UIButton()let factButton=UIButton()//省略:添加子视图并设置约束.。self.viewStore.Publisher.map{";\($0.count)";}.assign(to:\.text,on:countLabel).store(in:&;self.ancellable)self.viewStore.Publisher.number FactAlert.ink{[弱自我]number FactAlert in let alert tController=UIAlertController(标题:number FactAlert,Message:nil,PerredStyle:.alert)。,style:.default,处理程序:{_in self?.viewStore.send(.factAlertDismissed)}))self?.Present(alert tController,Animated:true,Complete:nil)}).store(in:&;self.ancellable)}@objc私有函数incrementButtonTaps(){self.viewStore.send(.incrementButtonTaps)}@objc私有函数deducmentButtonTaps(){。

一旦我们准备好显示此视图,例如在场景委托中,我们就可以构造一个存储。此时我们需要提供依赖项,目前我们只需使用立即返回模拟字符串的效果即可:

让appView=AppView(store:store(initialState:AppState(),Reducer:appReducer,Environment:AppEnvironment(mainQueue:DispatchQueue.main.eraseToAnyScheduler(),number Fact:{Number in Effect(value:";\(Number)is a Good number Brent";)}))。

这就足以让一些东西在屏幕上玩耍了。这肯定比用普通的SwiftUI方式多做几个步骤,但也有一些好处。它为我们提供了一种一致的方式来应用状态变化,而不是将逻辑分散在一些可观察到的对象和UI组件的各种动作闭包中。它也给了我们一种表达副作用的简明方式。我们可以立即测试这个逻辑,包括效果,而不需要做太多额外的工作。

要进行测试,您首先要使用与创建常规Store相同的信息创建一个TestStore,只不过这次我们可以提供便于测试的依赖项。具体地说,我们使用测试调度器而不是实时DispatchQueue.main调度器,因为这允许我们控制何时执行工作,并且我们不必人为地等待队列赶上。

让Scheduler=DispatchQueue.testSchedulerlet store=TestStore(initialState:AppState(),Reducer:appReducer,Environment:AppEnvironment(mainQueue:Scheduler.eraseToAnyScheduler(),number Fact:{Number in Effect(value:";\(Number)是一个好数字Brent";)})

一旦创建了测试存储,我们就可以使用它来断言整个用户步骤流。每一步我们都需要证明这种状态改变了我们的预期。此外,如果某个步骤导致执行将数据反馈到存储的效果,我们必须断言这些操作已正确接收。

下面的测试让用户递增和递减计数,然后他们要求提供一个数字事实,该效果的响应会触发显示一个警报,然后消除该警报会导致警报消失。

store.assert(//测试点击递增/递减按钮会更改计数.Send(.incrementButtonTaps){$0.count=1},.Send(.deducmentButtonTaps){$0.count=0},//测试点击事实按钮会使我们收到来自效果的响应。注意//我们必须提前调度器,因为我们在Reducer中使用了`.Receive(on:)`。.Send(.numFactButtonTaps),.do{Scheduler.Advance()},.Receive(.numFactResponse(.SUCCESS(";0是好数字布伦特";)){$0.numFactAlert=";0是好数字布伦特";},//并最终关闭警报.Send(.factAlertDismissed){$0.numFactAlert=nil})。

这是在Composable Architecture中构建和测试特性的基础。还有更多的东西需要探索,比如组合、模块化、适应性和复杂的效果。Examples目录有一大堆项目可供浏览,以查看更高级的用法。

这就是可组合体系结构的基础。关于这个库的能力还有很多要说的,repo包含了大量的案例研究和演示,展示了如何孤立地解决常见的应用程序问题。我们希望你能解决这个问题,并让我们知道你的想法!

👋嗨,你好啊!如果你走到这一步,那么你一定很喜欢这个帖子。您可能还想看看关于函数式编程和SWIFT的视频系列--无点(Point-Free)。