swift redux_将Redux架构移植到Swift(好吧,无论如何都适用于Toy macOS应用程序)

swift redux

👋

👋

Oh boy, Medium. Back in my day (let me just sit down in my rocking chair here) we had to install mod_perl and MovableType just to put something on the Internet, and we uppercased “Internet” back then too, and we liked it, and we walked uphill—I’m being told I have to get back to talking about the software architecture in InsertGif.

哦,男孩,中等。 回到我的一天(让我坐在这里的摇椅上),我们不得不安装mod_perl和MovableType以便将某些东西放到Internet上,然后我们也将“ Internet”大写,我们喜欢它,然后我们就走了—有人告诉我我必须回到谈论InsertGif中的软件体系结构。

Software architecture: an exciting topic on which nobody else has ever pontificated about before. One of the nicer experiences I have had programming in the last few years is writing React components using Redux. If you are unfamiliar with web development, we won’t be talking too too much about React and Redux in this little tutorial, but it helps to know about store and actions. Our goals are to architect a toy app that is testable, debuggable, easy to understand, and easy to reason about. The idea of separating a “store” and a set of “actions” — whatever those words may mean, for now — out of a declarative user-interface description (HTML, JSX, Cocoa views and view controllers) is rooted in the older idea of separating data and presentation from one another, the same behind Cascading Style Sheets and having Microsoft PowerPoint be a different app from Microsoft Word. That last one isn’t real. I am just making sure that you’re still paying attention. Don’t just scan the headers and images in a blog post, people: critical reading skills are the only way we get out of the pandemic.

软件体系结构:一个令人振奋的话题,以前没有人对它感兴趣。 在过去的几年中,我编程的一个更好的经历就是使用Redux编写React组件。 如果您不熟悉Web开发,那么在这个小教程中我们不会过多地谈论React和Redux,但是它有助于了解storeaction 。 我们的目标是设计一个可测试可调试易于理解易于推理的玩具应用程序。 从声明性的用户界面描述(HTML,JSX,Cocoa视图和视图控制器)中分离出一个“商店”和一组“动作”的概念(目前这些词现在可能意味着什么)是基于较早的想法的将数据和演示文稿彼此分离的方法,级联样式表后面的方法相同,并且使Microsoft PowerPoint与Microsoft Word不同。 最后一个不是真实的。 我只是确保您仍在注意。 人们不要只扫描博客文章中的标题和图像,重要的阅读技能是我们摆脱大流行的唯一途径。

商店和动作 (Stores and Actions)

We can define the store as the location where the canonical representation of data, true as we know it to be true at the current point in time. It’s not a great definition, but it tries hard not to do what most definitions do, which is be so specific as to tell a lie.

我们可以将商店定义为数据的规范表示的位置,正如我们所知,它在当前时间点是正确的。 这不是一个很好的定义,但它会尽力不做大多数定义所能做的事情,因为它太具体了以至于说谎。

Let’s say we are are writing a GUI component to show your bank account. This is part of our new bank account app, BankAccount™. The store in this app would contain

假设我们正在编写一个GUI组件以显示您的银行帐户。 这是我们新的银行帐户应用程序BankAccount™的一部分。 此应用中的商店将包含

  • All the transactions to be rendered

    所有要呈现的交易
  • The computed account balance, so we don’t have to iterate over the list of transactions each time. O(1) instead of O(n), ya know

    计算出的帐户余额,因此我们不必每次都遍历交易列表。 O(1)而不是O(n),你知道
  • The user’s avatar, which scrolls as the page is scrolled. It covers up other, more valuable information, but the client wanted it. Thought it would spiffy the app up a little

    用户的头像,随着页面滚动而滚动。 它涵盖了其他更有价值的信息,但是客户希望得到它。 以为这会使应用程序有点生气

In the Kingdom of JavaScript, we would represent the store as a giant, and usually immutable, object, with arrays and strings and floats and other objects. (We would want exact decimals for a banking app, but then we would look up how JavaScript works, and we would sigh and resign ourselves to floats.)

在JavaScript王国中,我们将商店表示为一个巨大的,通常是不可变的对象,其中包含数组,字符串,浮点数和其他对象。 (我们想要银行应用程序的精确小数,但是然后我们将查找JavaScript的工作方式,然后叹息并辞职为浮动对象。)

The actions, then, are concrete objects that encode events relevant to the store, either creating new versions of the store or requiring information from the store. This definition is meant to be wide-encompassing and generic, because what constitutes an action really depends on the business logic, your patience to write kinda boilerplate-y code, and the store. An action in a macOS app might be:

然后,这些操作对与商店相关的事件进行编码的具体对象,它们创建商店的新版本或要求商店提供信息。 这个定义意味着广泛而通用的,因为构成一个动作的真正取决于业务逻辑,您耐心地编写样板代码以及商店。 macOS应用中的操作可能是:

  • A button click

    一键点击
  • A menu item click

    菜单项单击
  • The user resized the application window

    用户调整应用程序窗口的大小
  • A keypress happened

    按键发生
  • A keypress on another app happened

    按下另一个应用程式时发生按键

  • An thread monitoring an HTTP request completed on another thread

    监视另一个线程上完成的HTTP请求的线程
  • macOS is about to restart

    macOS即将重启
  • The computer is on fire

    电脑着火了

But the promise is this: if something wants to mutate the store, it must take place as an action first. If the user wants to withdraw money from their bank balance, the app must first encode the action as an action of type “withdrawal” with amount “-70” and a description “had to buy a new AirPod because the old one fell out while I was chewing.” If the user uploads a new avatar, then we must make an action of type “avatar_uploaded” with a buffer containing the JPEG data. A good implementation of this architecture for macOS applications should make it difficult for views and view controllers to violate this promise. That is to say:

但是承诺是这样的: 如果某个东西想要改变商店,则必须首先将其作为动作来进行 。 如果用户要从其银行存款中提取资金,则该应用必须首先将该操作编码为“提取”类型的操作,金额为“ -70”,并说明“必须购买新的AirPod,因为旧的AirPod掉了出来,我在嚼。” 如果用户上传了新的头像,则我们必须使用包含JPEG数据的缓冲区执行“ avatar_uploaded”类型的操作。 对于macOS应用程序,此架构的良好实现应使视图和视图控制器难以违反此承诺。 也就是说:

The app cannot easily mutate the store. As an application scales up to include more and more components, with more data dependencies, it becomes harder to reason about. One nice aspect of the store-and-actions architecture (which I have cheerfully named “the Redux architecture” to get you to click on this link) is that it makes all the changes to the store explicit. First, the change must be encoded as an action. Then, a function (which we might call a reducer) must pattern-match on the stream of actions to create a new store.

该应用无法轻松更改商店。 随着应用程序扩大规模以包含越来越多的组件以及更多的数据依存关系,就很难进行推理了。 store-and-actions架构的一个不错的方面(我将它高高兴兴地命名为“ Redux架构”,可让您单击此链接)是它使对商店的所有更改都明确。 首先,必须将更改编码为动作。 然后,一个函数(我们可能将其称为reducer)必须在动作流上进行模式匹配以创建新的商店。

在Swift中存储和操作 (Store and actions in Swift)

If you have programmed with iOS or macOS, you might, to some degree, recognize the store as just another name for Core Data’s managed object context, which is more or less analogous. The store there is instead represented as an object graph, encoding 1:1, 1:N, M:N relationships and giving us a way to project, insert, update, and delete nodes in the graph. It’s true, you would want to, unless you had good reason, use Core Data in an application, especially if you were looking to build out a team of developers and to scale it up to something approaching a fancy macOS app that costs $99 on the App Store.

如果您使用iOS或macOS进行编程,则在某种程度上您可能会将商店识别为Core Data的托管对象上下文的另一个名称,这或多或少是类似的。 相反,那里的商店以对象图的形式表示,编码1:1、1:N,M:N关系,并为我们提供了一种在图中投影,插入,更新和删除节点的方法。 的确如此,除非您有充分的理由,否则您将希望在应用程序中使用Core Data,尤其是如果您希望组建开发人员团队并将其扩展到接近花哨的macOS应用程序的价格(99美​​元)时,更是如此。应用商店。

However! Let’s say we want to build a toy app quickly, something fun and can be hacked together over the weekend yet still not completely suck at managing data. We can perhaps encode our store as an immutable object, using Swift’s struct:

然而! 假设我们要快速构建一个玩具应用程序,这很有趣,可以在周末一起被黑客入侵,但仍不能完全吸纳数据管理。 我们也许可以使用Swift的struct将商店编码为不可变的对象:

Integer also not a great way to store decimals, huh?
整数也不是存储小数的好方法,是吗?

To this we want to design what a withdrawal, our first action, might look like. We can represent actions using Swift’s enum:

为此,我们要设计撤退(第一个动作)的外观。 我们可以使用Swift的enum表示动作:

We now need to fold over the stream of actions, creating new versions of the store along the way. Redux represents the part of the codebase that takes actions in and produces new versions of the immutable store as “reducers,” and we will borrow that terminology here, even though the diction is more esoteric than we need it to be. Let’s assume that we will be writing multiple reducers over time (I like to structure each new feature ticket on InsertGif as a new reducer) and use a protocol to unify them all at the type level:

现在,我们需要折叠操作流,一路创建商店的新版本。 Redux代表了代码库中采取行动并产生不可变存储的新版本的部分,称为“缩减器”,尽管该用法比我们需要的要深奥得多,但我们仍将在此借用该术语。 假设随着时间的推移,我们将编写多个化简器 (我喜欢将InsertGif上的每个新功能标签构造为新的化器),并使用protocol在类型级别上统一它们:

Protocols are what you would call interfaces in other languages. The term comes from Objective-C. Why is Objective-C so different? Objective-C likes to be different.
协议就是您用其他语言称呼的接口。 该术语来自Objective-C。 为什么Objective-C如此不同? Objective-C喜欢有所不同。

Note that, because we are using Swift’s value types (structs), we first copy the store into nextStore and then update the fields of the new store before returning it. This is so common in our reducer code that we will go ahead and give ourselves a helper to make it easier:

请注意,由于我们使用的是Swift的值类型( struct ),因此我们首先将商店复制到nextStore ,然后在返回新商店之前更新新商店的字段。 这在我们的reducer代码中非常普遍,以至于我们将继续为自己提供帮助,以使其变得更容易:

The reason we need the inout keyword here is so that we can mutate the new, cloned Store. If Swift had a better immutable record update syntax, we would probably not be here.
我们在这里需要inout关键字的原因是,我们可以对新克隆的商店进行突变。 如果Swift有更好的不可变记录更新语法,我们可能不在这里。
This is banking code that I trust.
这是我信任的银行代码。

大中央调度(但不是大中央调度) (Grand, central Dispatch (but not that Grand Central Dispatch))

We now write the layer in our architecture that constructs the store and runs the reducers — a central location to where all actions are dispatched and handled. Imaginatively, we will call it Dispatch:

现在,我们在体系结构中编写该层,以构造商店并运行化简器-调度和处理所有动作的中心位置。 想象中,我们将其称为Dispatch

Now a view controller can hook an NSButton to Dispatch such that, every time the user clicks on the “Withdraw!” button, a withdrawal action occurs. A real banking app would probably also want you to enter the routing and account number of the place you are withdrawing to, but that’s no fun to show so we will blithely ignore this, and all other aspects of real-world banking.

现在,视图控制器可以钩住NSButton进行Dispatch这样,每次用户单击“提现!”时, 按钮,发生撤回操作。 一个真正的银行应用程序将也可能要你输入你退出地方的路由和账户号码,但是这没有乐趣展现,所以我们将愉快地忽略这一点,和真实世界的银行的所有其他方面

Some kinda cool immediate benefits we get from having stores and actions, even in this extremely unrealistic example app:

即使在这个极其不现实的示例应用程序中,我们也可以通过存储和操作获得一些很酷的即时收益:

  • Business logic lives in the reducer, which is eminently testable. No need to construct the Cocoa views to just run a unit test on the reducer; we only need to mock the store.

    业务逻辑存在于reducer中,这是可以测试的。 无需构造Cocoa视图即可在reducer上运行单元测试; 我们只需要模拟商店。
  • A great way to debug the app is to print("\(action\)") in _dispatch, or to place an XCode breakpoint there. All the possible external stimuli from the outside world have to pass through there. Any time something unexpected happens, we should be able to trace it to the exact action that caused it.

    调试应用程序的一种好方法是在_dispatch print("\(action\)")或在其中放置XCode断点。 来自外界的所有可能的外部刺激都必须经过那里。 每当发生意外情况时,我们应该能够将其追溯到引起它的确切动作。

  • A common request, by product managers and bosses alike, is to implement user analytics. When did the user click on a button? Where was the user coming from? How can we funnel users into giving us more of their information and/or money? We can write a Track reducer that convert these user behaviors into a Google Analytics or a Segment event, making the remote calls as the events come in (fire and forget). A minute form of event sourcing, as my friend Sönke points out.

    产品经理和老板的共同要求是实施用户分析。 用户何时单击按钮? 用户来自哪里? 我们如何吸引用户向我们提供更多他们的信息和/或金钱? 我们可以编写一个Track reducer,将这些用户行为转换为Google Analytics(分析)或Segment事件,并在事件进入时进行远程调用(即发即忘)。 正如我的朋友Sönke指出的那样,这是一种小型的事件源。

  • As the app grows, becoming unruly and moody, we will want to refactor features. The adage here is “write code that is easy to delete,” and I believe that reducers written this way satisfies that rule of thumb. We can drop the action and the reducer and then use the compiler’s list of compiler errors to find all the use-sites. Imagine if we had written it so our view controller directly read and modified the array of transactions and the balance: we would have to not only audit each location where the user action led to a mutation, but we would also have to pull out all the business logic entangled into our view code.

    随着应用的增长,变得不规则和喜怒无常,我们将希望重构功能。 这里的格言是“编写易于删除的代码”,我相信以这种方式编写的简化程序可以满足这一经验法则。 我们可以删除操作和reducer,然后使用编译器的编译器错误列表来查找所有使用位置。 想象一下,如果我们编写了它,那么我们的视图控制器将直接读取并修改事务数组和余额:我们不仅必须审核用户操作导致突变的每个位置,而且还必须提取所有业务逻辑纠缠在我们的视图代码中。

订阅内容 (Subscriptions)

With the withdrawal feature now in place, we would like to render a screen in our app that contains the list of transactions, the account balance, and the user’s avatar. In JavaScript, our React components update automatically as their dependencies (the store among them) change. We do not have the benefit of componentized user interfaces in Cocoa, but we still benefit by thinking about the one-way dataflow from the store to the presentation of data from the store.

现在有了提款功能,我们想在我们的应用程序中渲染一个屏幕,其中包含交易列表,帐户余额和用户的头像。 在JavaScript中,我们的React组件会随着其依赖关系(其中的存储)的变化而自动更新。 在Cocoa中,我们没有组件化用户界面的好处,但是我们仍然可以通过考虑从商店到商店的数据呈现的单向数据流而受益。

Dispatch already has a single place through which all actions flow. We can take advantage of this architectural watering hole and use it invoke subscriptions. First, let us model subscriptions as callable objects. (In C++, we would call this a functor object, and I can hear what you are saying into your computers now: Why did you remind me about C++?)

Dispatch已经在所有操作流经的地方。 我们可以利用这个体系结构的漏洞,并使用它来调用订阅。 首先,让我们将订阅建模为可调用对象。 (在C ++中,我们将其称为functor对象,现在我可以听到您在计算机中所说的: 为什么让我想起C ++ ?)

The `@escaping` keyword marks the closures for later use. It constrains what kinds of closures can be passed as parameters, and helps prevent reference cycles. Life is all about escaping the cycles we construct for ourselves.
关键字@escaping标记了闭包以供以后使用。 它限制了可以将哪种闭包作为参数传递,并有助于防止引用循环。 生活就是逃避我们为自己构建的周期。

As subscriptions come in, we store them into an array. When an action occurs, we iterate over the array to invoke the subscription. The only small wrinkle is that we memoize projections of the store to avoid unnecessary UI updates.

随着订阅的到来,我们将它们存储在一个数组中。 当操作发生时,我们遍历数组以调用订阅。 唯一的不足是,我们会记住商店的投影,以避免不必要的UI更新。

This works by breaking subscriptions into two : a map and an action. Like mapStateToProps in Redux, we encourage view controllers to construct an Equatable object containing just the subset of information from the store (a projection) they need, so that the action need not fire each time an action occurs.

这可以通过将订阅分为两部分来进行: mapaction 。 像mapStateToProps在终极版中,我们鼓励视图控制器来构建一个Equatable包含从商店(投影),他们需要的信息只是该子集的对象,这样action需要每一个动作发生的时间不火。

In general, this strikes at a central weakness of the store-and-actions architecture: It can be difficult to write a performant, 60fps app when certain high-frequency actions are encoded into objects and passed through every reducer. However, for our toy app, this suffices fine; and we can also imagine an app where certain parts of the app flee the normal architecture and manage their own state, to squeeze out those extra frames.

通常,这是存储和动作体系结构的一个中心弱点:当某些高频动作编码为对象并通过每个reducer传递时,编写高性能,每秒60帧的应用程序可能会很困难。 但是,对于我们的玩具应用程序,这已经足够了; 我们还可以想象一个应用程序,其中应用程序的某些部分会逃脱正常的体系结构并管理自己的状态,从而挤出那些多余的框架。

It only remains to implement Subscriber:

它仅保留实现Subscriber

The T generic variable exists because we don’t know what kind of type the view controllers will want to represent their state as. We only know that it needs to be Equatable so we can implement the inequality check.
存在T泛型变量是因为我们不知道视图控制器将要表示其状态的哪种类型。 我们只知道它必须是相等的,因此我们可以实施不平等检查。

Now we can use subscriptions to reactively update our Cocoa views:

现在,我们可以使用订阅来动态更新我们的可可视图:

Oh boy, if only view controllers were this easy to write.
哦,天哪,如果仅视图控制器就这么容易编写。

This completes the core loop of our architecture:

这完成了我们架构的核心循环:

  • Some change happens (a user clicks “Withdraw!”) in one view controller

    在一个视图控制器中发生了一些更改(用户单击“提现!”)
  • The change is packaged up into an action

    变更打包成一个动作
  • A reducer (or several) handles the action, and creates a new store

    减速器(或多个)处理该动作,并创建一个新商店
  • Subscribers are invoked with the new store

    用新商店调用订户
  • A second view controller subscribed to the store updates its views

    订阅商店的第二个视图控制器更新其视图

One last thing, though: what if we have multiple reducers?

不过,最后一件事:如果我们有多个异径管怎么办?

高阶减速器 (Higher-order Reducers)

This is the title of a book about programming that would sell a bunch of copies, even though nobody would be able to understand it. Some other alternate universe where I am paid for the content I make on the internet, I suppose. In this universe, we only need a reducer that can multiplex actions to many other reducers. Despite the fancy title, it’s simple to write:

这是一本有关编程的书的标题,该书将出售大量副本,即使没人能理解。 我想在其他替代宇宙中,我可以从网上赚取的内容获得报酬。 在这个世界中,我们只需要一个可以将动作多路复用到许多其他化简器的化简器。 尽管标题花哨,但编写起来很简单:

Now, I could keep going with the bank app, but I think it would be neat to show you what the actual list of reducers looks like in InsertGif:

现在,我可以继续使用银行应用程序,但是我想向您展示在InsertGif中减速器的实际清单是什么样的:

That should give you a hint to the feature tickets we wanted to implement for the 5.0 release of the app. And here are all our actions:

这应该向您提示我们要为该应用程序的5.0版本实施的功能票。 这是我们所有的动作:

Spectacle app, by the way. Spectacle应用程序。

Even for a toy app, it is already kind of complicated! We might posit that writing user interfaces is, essentially, a hobby where you worry about tiny details.

即使是玩具应用程序,也已经很复杂了! 我们可能会认为,编写用户界面本质上是一种业余爱好,您担心细微的细节。

就这些 (That is all)

Go and check out InsertGif if you haven’t already. We also have a Twitter and a ProductHunt, which we will keep updating as long as we remember to. InsertGif is made with love in NYC, and a proud alumnus of the Originate R&D laboratory. I hope you had as much fun reading this as I had while individually creating all the Gists, because Medium, a seven-year old product, doesn’t support syntax highlighting in code blocks.

去看看InsertGif(如果还没有的话)。 我们还有一个Twitter和一个ProductHunt ,只要我们记得,它们就会不断更新。 InsertGif是纽约市的挚爱,也是Originate R&D实验室的骄傲校友。 我希望您能像单独创建所有Gist时一样开心,因为我已经有七年历史了,Medium不支持在代码块中突出显示语法。

翻译自: https://medium.com/insertgif/porting-redux-architecture-to-swift-well-for-a-toy-macos-app-anyway-ed61bad0395c

swift redux

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值