swift 方法中定义回调
Being able to work on diverse projects gave me the chance to be in contact with several types of developers and codebases. Besides their core differences, what stood out to me during this process is that projects with a lower level of maturity will always face similar problems.
能够从事各种项目使我有机会与几种类型的开发人员和代码库保持联系。 除了它们的核心差异之外,在此过程中对我而言突出的是,成熟度较低的项目将始终面临类似的问题。
Perhaps the developers chose the wrong architecture or the lack of unit tests caused a nasty bug to sneak into production, but there’s a specific problem that always draws my attention: callback hell. If not treated from the very beginning, these awful pyramids of braces you get when chaining callbacks inside other callbacks or conditions plague codebases with an eternity of impossible code reviews and distant screams of “What the hell is this method supposed to be doing?”
也许开发人员选择了错误的体系结构,或者缺少单元测试导致了一个讨厌的bug潜入生产环境,但是总有一个特定的问题引起我的注意:回调地狱。 如果不从一开始就进行处理,当在其他回调或条件中链接回调时,这些可怕的花括号金字塔就会使困扰代码库的问题变得千篇一律,其中包括不可能的代码审查和遥远的尖叫:“此方法到底在做什么?”
They are difficult to read, nearly impossible to review but unfortunately super easy to write, cementing their place as the bane of junior developers.
它们难以阅读,几乎无法审查,但不幸的是,它们非常容易编写,巩固了其作为初级开发人员的祸根的地位。
Fortunately for us, Swift offers several options to avoid this behavior. With a bit of patience and a proper style guide, you can prevent this sort of mistake from affecting your productivity. I’ll use this article to share how I personally avoid them, and hopefully, this will help you come up with your own solutions.
对我们来说幸运的是,Swift提供了多种选择来避免这种行为。 稍有耐心和适当的样式指南,就可以防止这种错误影响您的生产率。 我将使用本文分享我个人如何避免使用它们,并希望这将帮助您提出自己的解决方案。
条件地狱:使用警卫代替if (Condition Hell: Use guard Instead of if)
Pyramids of conditions are very common and fortunately easier to deal with. guard
is among my top 10 features in Swift for a good reason. Although it basically works as an inverted if
statement, it gives you a great advantage in terms of code quality. Besides providing a way for you to give an early return to a method, it allows you to put the "good" outcome of a method in the same indentation as the method itself, making your method's intent far easier to be understood by your colleague. The improvement is not difficult to spot in a chain of if
statements:
条件金字塔很常见,幸运的是更容易处理。 有个很好的理由, guard
是我在Swift中的十大功能之一。 尽管它基本上可以用作反向if
语句,但是它在代码质量方面具有很大的优势。 除了为您提供一种使方法早日返回的方法之外,它还允许您将方法的“好”结果放在与方法本身相同的缩进中,从而使您的方法的意图更易于同事理解。 在if
语句链中发现改进并不难:
If you embrace the mindset of putting the good outcome of your method as close as possible to the method’s indentation and the bad outcomes as far as possible from it, you’ll find your code significantly easier to read, as a mere glance at the end of the method will be enough for someone to understand what it’s supposed to do. Use guards
to isolate things that are not supposed to happen and restrict the usage of ifs
to things that aren't necessary for the good outcome to happen, like changing the color of a cell based on a property's value.
如果您怀着这样的心态:将方法的好结果尽可能地靠近方法的缩进,而不良的结果则尽可能地远离它,那么您会发现代码明显更易于阅读,而最后只需一眼该方法足以使某人了解它应该做什么。 使用guards
措施将不应该发生的事情隔离开来,并将ifs
的使用限制在对于实现良好结果不是必需的事情上,例如根据属性的值更改单元格的颜色。
封闭地狱:抽象完成处理程序 (Closure Hell: Abstracting Completion Handlers)
Callback hell caused by asynchronous calls is trickier to solve, as completion handlers can contain pretty much anything, but there are efficient ways to deal with them as well.
由于完成处理程序可以包含几乎所有内容,因此由异步调用引起的回调地狱很难解决,但是也有有效的方法来处理它们。
承诺 (Promises)
The concept of promises is my go-to solution for managing anything that’s asynchronous. If you have never seen them before, promises relate to the concept of a type that may or may not resolve a value at a later time:
许诺的概念是我管理异步事物的首选解决方案。 如果您以前从未看过它们,则promise与可能在以后无法解析值的类型的概念有关:
The Promise
type can receive closures that determine how to proceed depending on the result of resolving the value, which is represented by then(completion:)
and catch(completion:)
in this case. If you're wondering why this helps with callback hell, it's because then
handlers can optionally receive another promise, creating a limitless straight flow of operations:
Promise
类型可以接收闭包,这些闭包根据解析值的结果来确定如何进行处理,在这种情况下,该值由then(completion:)
和catch(completion:)
。 如果你想知道为什么这与回调地狱帮助,这是因为then
的处理程序可以选择接收另一个承诺,建立操作的无限直流量:
By making your async operations return Promise
types instead of receiving completion handlers, you will be able to chain any amount of them together into a nice straight line of code. They are especially great when your operations depend on things returned by previous operations, as more powerful promise implementations will contain several options for transforming values as well.
通过使您的异步操作返回Promise
类型而不是接收完成处理程序,您将能够将它们中的任意数量链接在一起,形成一条漂亮的直线代码。 当您的操作依赖于先前操作返回的内容时,它们特别有用,因为功能更强大的promise实现还将包含用于转换值的多个选项。
I personally use PromiseKit, as it contains tons of features, but there are lightweight libraries around the web and you could certainly develop a simple promise implementation yourself.
我个人使用PromiseKit ,因为它包含许多功能,但是网络上有轻量级的库 ,您当然可以自己开发一个简单的promise实现。
You’ll see people recommending things like RxSwift for this purpose as well. I would personally not do so because I think that anything that holds your entire project hostage is a death sentence in the long term (i.e. every single thing you do has to take RxSwift’s architecture in mind in order to work), but that’s my personal opinion and you can definitely use it if you know what you’re doing.
您还会看到人们为此推荐RxSwift之类的东西。 我个人不会这样做,因为从长远来看,任何将整个项目作为人质的东西都是死刑(即,您所做的每件事都必须考虑RxSwift的体系结构才能起作用),但这是我个人的看法。如果您知道自己在做什么,就可以使用它。
操作队列 (OperationQueue)
If promises aren’t your thing because you’d rather solve things the Apple way, you can use Foundation's native solutions for managing sequential operations.
如果不是要兑现承诺,而是希望以Apple的方式解决问题,则可以使用Foundation的本机解决方案来管理顺序操作。
OperationQueue
is Apple's abstraction of DispatchQueue
that contains additional features to better support synchronizing and canceling operations. If your operations don't rely on data from previous operations, the Operation
family of classes will do the trick. For synchronous operations, this is just a matter of queuing your custom operations:
OperationQueue
是Apple DispatchQueue
的抽象,它包含其他功能以更好地支持同步和取消操作。 如果您的操作不依赖先前操作中的数据,则Operation
系列类可以解决问题。 对于同步操作,这只是排队您的自定义操作的问题:
However, things are trickier for asynchronous operations. To make the queue wait for your operation to truly finish, you’ll either have to use thread-blocking mechanisms such as DispatchGroups
or create/use a custom AsynchronousOperation type that manages an operation's states for this purpose.
但是,对于异步操作而言,事情比较棘手。 为了使队列等待操作真正完成,您要么必须使用诸如DispatchGroups
类的线程阻止机制,要么为此目的创建/使用自定义的AsynchronousOperation类型来管理操作的状态。
If you need an operation to pass data to another one, you’ll find no clean solution with OperationQueue
, as there's no guarantee that an operation's completionBlock
will be called before the next one starts running. There are a few hacks to achieve this, though. You can wrap all your needed data in an external reference type that is accessible to all operations:
如果您需要一项操作来将数据传递给另一个OperationQueue
,则无法使用OperationQueue
找到干净的解决方案,因为无法保证在下一个操作开始运行之前将调用该操作的completionBlock
。 不过,有一些技巧可以实现这一目标。 您可以将所有需要的数据包装在所有操作都可以访问的外部引用类型中:
Alternatively, you can store the necessary data in the operation’s dependency and access it by subclassing the operation and fetching its dependencies when it gets executed:
另外,您可以将必要的数据存储在操作的依赖项中,并通过对操作进行子类化并在执行时获取其依赖项来访问它们:
I dislike having to deal with optional properties everywhere, so I personally wouldn’t use OperationQueue
if my operations depended on data fetched by other operations.
我不喜欢在任何地方处理可选属性,因此,如果我的操作依赖于其他操作提取的数据,那么我个人不会使用OperationQueue
。
使用高阶函数 (Use high-order functions)
If you want to do this without using additional data structures, you can treat callback hell with nothing but pure Swift by applying better coding practices and some principles from functional programming. Because closures are types, they can be passed as arguments to methods — normally as completion handlers. The thing is Swift methods are just glorified closures, so you can pass an entire method as a closure argument. This exact concept can be used to reduce the number of nested closures in a method:
如果您想在不使用其他数据结构的情况下执行此操作,则可以通过应用更好的编码实践和函数式编程中的某些原则,仅使用Swift来处理回调地狱。 因为闭包是类型,所以它们可以作为参数传递给方法-通常作为完成处理程序。 关键是Swift方法只是美化的闭包,因此您可以将整个方法作为闭包参数传递。 这个确切的概念可用于减少方法中嵌套闭包的数量:
let sum = array.reduce(0, +)
//reduce() here is an ((Int, ((Int, Int) -> Int)) -> Int)
//and the + operator is func +(lhs: Int, rhs: Int) -> Int,
//... or ((Int, Int) -> Int), so there's no need to define reduce's closure.
To see how this applies, let’s assume that we have a method that downloads a picture from the web, locally applies a sepia tone filter to it in another thread, and then uploads it as the user’s profile picture:
为了了解这是如何应用的,我们假设有一种方法可以从网络上下载图片,在另一个线程中对其局部应用棕褐色调,然后将其作为用户的个人资料图片上传:
I’ve left out any kind of error management to make this article easier to grasp, but as with any classic callback hell problem, the first problem here is clear: This method does way too much stuff. Before we start thinking about the closures, let’s first apply the single-responsibility principle and divide each part of this workflow into separate methods:
为了使本文更容易理解,我省略了任何类型的错误管理,但是与任何经典的回调地狱问题一样,这里的第一个问题很清楚:此方法处理的东西太多了。 在开始考虑关闭之前,让我们首先应用单一职责原则,并将此工作流的每个部分划分为单独的方法:
Although the callback hell still exists here, we at least have something that’s readable now.
尽管回调地狱在这里仍然存在,但是至少我们现在有了一些可读性。
To reduce the number of nested closures, analyze how this method works. Can you see the pattern in applySepiaFilterAndUpload()
? The key to solving the nesting problem is how each step works: Every method here works in the exact same way. downloadPicture
receives a URL
and provides a Data
completion, applySepiaFilter
receives a Data
and provides another Data
completion, and uploadUserPicture
receives a Data
and provides a User
completion. If you turn these types into generics, you'll end up with:
为了减少嵌套闭包的数量,请分析此方法的工作原理。 您可以在applySepiaFilterAndUpload()
看到模式吗? 解决嵌套问题的关键是每个步骤的工作方式:这里的每个方法都以完全相同的方式工作。 downloadPicture
接收URL
并提供Data
完成, applySepiaFilter
接收Data
并提供另一个Data
完成, uploadUserPicture
接收Data
并提供User
完成。 如果将这些类型转换为泛型,您将得到:
downloadPicture = (T, (U -> Void)) -> Void
applySepiaFilter = (U, (V -> Void)) -> Void
uploadUserPicture = (V, (W -> Void)) -> Void
Because these async operations have the exact same structure and clearly depend on each other, we can completely remove the necessity of having closures by adapting these methods to receive the next one as an argument. This would be trivial to do if each method had an explicit return type, but since we’re dealing with completion handlers, we need to write a little helper to achieve this effect. First, I’ll define this shared behavior as an Operation
alias (with optionals so nobody's forced to do anything):
因为这些异步操作具有完全相同的结构,并且显然彼此依赖,所以我们可以通过调整这些方法以接收下一个作为参数来完全消除使用闭包的必要性。 如果每个方法都有一个显式的返回类型,这将是微不足道的,但是由于我们正在处理完成处理程序,因此我们需要编写一些帮助程序来实现此效果。 首先,我将这种共享行为定义为Operation
别名(具有可选属性,因此没有人被迫做任何事情):
public typealias Operation<T, U> = ((T, ((U) -> Void)?) -> Void)?
With that, we can define a method that “merges” two operations into one as long as they have matching parameters, making (T, (U -> Void)) -> Void
+ (U, (V -> Void)) -> Void
become (T, (V -> Void)) -> Void
:
这样,我们可以定义一种将两个操作“合并”为一个的方法,只要它们具有匹配的参数即可,使(T, (U -> Void)) -> Void
+ (U, (V -> Void)) -> Void
变为(T, (V -> Void)) -> Void
:
This method returns a new closure that performs the first operation method with a given input, uses its output to execute the second one, and finally executes a given completion for the second operation’s result. If all our methods follow the Operation
structure, we can use merge()
to progressively merge all steps into a single operation. We can't really escape the nesting in this helper, but this allows us to rewrite our main method without them:
此方法返回一个新的闭包,该闭包使用给定的输入执行第一个操作方法,使用其输出执行第二个闭包,最后为第二个操作的结果执行给定的完成。 如果我们所有的方法都遵循Operation
结构,则可以使用merge()
逐步将所有步骤合并为一个操作。 我们无法真正逃避此帮助器中的嵌套,但这使我们可以在没有它们的情况下重写我们的主要方法:
Because the signature of our operations matches merge()
's closure arguments, we can skip having to define closures by passing the methods' signatures as the arguments. In the end, job
becomes a unified method that takes a URL
, executes all operations in order, and then finally executes the method's completion handler with the result of the last operation. That's just like the first version, but with no nesting at all!
因为操作的签名与merge()
的闭包参数匹配,所以可以通过将方法的签名作为参数传递来跳过定义闭包的步骤。 最后, job
成为一个统一的方法,该方法采用URL
,依次执行所有操作,然后最后执行最后一个操作的结果的方法的完成处理程序。 就像第一个版本一样,但是根本没有嵌套!
Now, if you’re thinking, “But that looks terrible!” you’re absolutely right. Because we can only merge two operations at a time, we need to call merge()
several times, which will result in something that's probably harder to read than the original callback hell. There's a way to fix this, though. We can define an operator for merge()
's behavior:
现在,如果您在想,“但这看起来很糟糕!” 你是绝对正确的。 因为我们一次只能合并两个操作,所以我们需要多次调用merge()
,这将导致比原始回调地狱更难阅读的内容。 不过,有一种解决方法。 我们可以为merge()
的行为定义一个运算符:
infix operator >>->>: LogicalConjunctionPrecedence // Precedence of &&func >>->>(lhs: Operation, rhs: Operation) -> Operation {
return merge(lhs, rhs)
}
By using &&
's precedence, operations will be progressively merged all the way from the left. With that in place, we can now rewrite our workflow as a nice straight line of operations.
通过使用&&
的优先级,操作将从左开始一直逐步合并。 有了这些,我们现在可以将工作流程重写为一个很好的直线操作。
If you’re into this sort of stuff, the formal name for this very specific merging operation is Kleisli composition.
如果您喜欢这种东西,那么这种非常具体的合并操作的正式名称就是Kleisli composition 。
结论 (Conclusion)
If you take a deep look at it, you’ll notice that the presence of things like callback hell will always boil down to a lack of good coding practices.
如果深入研究它,您会注意到,诸如回调地狱之类的事情总会归结为缺乏良好的编码实践。
Clean code is a big topic, but there are great resources about it around the web. I’ve personally read and highly recommend Robert C. Martin’s Clean Code, as it teaches you how to see your code from the perspective of other developers — a great skill to have when learning how to write better-looking code. You should definitely give it a try if you’re a professional developer.
干净的代码是一个大话题,但是在网络上有很多关于它的资源。 我已经亲自阅读并强烈推荐Robert C. Martin的“ 干净代码” ,它教您如何从其他开发人员的角度看待您的代码,这是学习如何编写外观更好的代码时的一项出色技能。 如果您是专业开发人员,则绝对应该尝试一下。
翻译自: https://medium.com/better-programming/avoiding-callback-hell-in-swift-fb5f2828eb99
swift 方法中定义回调