好吧,也许并没有全错,但至少没有充分发掘枚举的所有、炫酷的潜力。来,让我来向你们展示如何写一个超级简单、超级安全而且超级易读的基础网络层。首先,我们先来看看一些基础内容,如果你觉得自己已经熟悉这部分了,那就可以直接跳到下一节看主要内容。我不会怨你的。
Well, maybe not wrong exactly, just probably not to their full, awesome potential. Here, I’ll show you how you can build a basic networker that’s super simple, super safe, and pretty much human readable! I’ll skim over some basics first, so if you feel like you’re good, feel free to skip down to the meat in the next section. I won’t blame you.
枚举即是目录
Enumerations are lists of things
枚举是超级好用的,尤其是当你只有有限个明确的值或者状态需要记录的时候。
enum Stoplight {
case Green, Yellow, Red
}
上面给出了一个简单的枚举描述了信号灯的几种不同的状态。我们知道,在任何时刻,一个信号灯只存在这三种状态。现在,如果我们想在一个简单逻辑里面使用这个枚举,它看上去会是这样的:
switch cornerStoplight { //cornerStoplight是Stoplight类型
case .Green:
println("可通行")
case .Yellow:
println("路口到此结束/快停下")
case .Red:
println("不可通行")
}
The enum is super useful for when you have a finite list of pre-determined values or states that you need to keep track of.
enum Stoplight {
case Green, Yellow, Red
}
Here we have an simple enum that represents the various states of a traffic stoplight. We know that there are only three states that a stoplight can exist in at any given moment. Now if we wanted to use this enum in a bit of logic it might look something like this:
switch cornerStoplight { // where cornerStoplight is type Stoplight
case .Green:
println("go")
case .Yellow:
println("finish crossing intersection or prepare to stop")
case .Red:
println("stop")
}
Switch-cases和枚举紧密联系在一起以确保每个单独的信号灯状态都能被控制否则就会编译器会抛出编译异常确保我们输入的代码是安全的。
你大概会想:“pfffft,我搞定这个了!” 太赞了!像这样简单的使用情况成千上百万,这些代码仍可以再简化、可以更已读。如果你是不是非常清楚上面的内容呢,我强烈建议你去Apple's Swift Language Guide阅读关于枚举的部分章节内容。当看完之后,回到这里,你将会感到廓然开朗。
Switch cases go hand in hand with enums by making sure that every single state that Stoplight could be in is handled or it throws a complier error and helps keep our code safe.
You’re probably thinking: “pfffft. I got this!” Awesome! There are a million simple use cases like this that could simplify your code and make it more readable. If you’re still not super clear on what’s going on above, I recommend reading through the section on enumerations in Apple's Swift Language Guide. It’s a good read, but when you’re done, come back here to have your mind blown!
有什么大不了的?
What’s the big deal?
如果你跟我上面一样,几年来在代码中使用用了数不胜数的枚举。当然,这样做不是没用的,但是很蠢。我当然可以用枚举来关联任意种类的数据类型,这又能怎样?让我们先来看看下面这段引用:
Swift中的枚举用在它们自身的规则下的话,是非常优秀的。它们吸收了非常多通常只被classes支持的特性。诸如:计算属性-提供枚举当前值的附加信息;实例方法-使得可以与枚举描述的值相关联起来。枚举可以声明初始化方法提供一个初始化的成员变量;可以被拓展增加它们本来所没有的功能的实现;还可以遵循协议提供标准功能。
等下,你说上什么?如果需要的话,赶紧翻回去读多几遍。我会在这里等你回来的。在文档、例子里面有非常多繁重的东西我们是需要扔掉然后继续前行,不再回顾的。这意味着,我们可以只用枚举来编程对吧?不完全对,但是我们的生活宗旨是化繁为简。
If you’re like me, you’ve used tons of enums in your code over the years like the one above. They’re useful, but kind of dumb too. Sure I can associate any kind of data type with them, but so what? Let’s look at the fourth paragraph on the page I linked above:
Enumerations in Swift are first-class types in their own right. They adopt many features traditionally supported only by classes, such as computed properties to provide additional information about the enumeration’s current value, and instance methods to provide functionality related to the values the enumeration represents. Enumerations can also define initializers to provide an initial member value; can be extended to expand their functionality beyond their original implementation; and can conform to protocols to provide standard functionality.
Wait what? Go ahead and read that again if you have to. I’ll still be here. That’s some pretty heavy stuff to drop on us and then move on and not mention any of this again in the documentation or examples. So this means we can just program using enumerations right? Not quite, but our lives are about to get a little bit easier!
开发一个用枚举实现的网络层
Building an enumerated networker
每当开始一个新项目的时候,我都会面临这么一个问题:“通过一个Api用Get 和/或 Post方法提交数据到服务器的时候,什么样的形式对于我来说是最好的呢?”每次我都觉得我在开发中最重要的这一环上面所写的代码有点太过于糟糕。所以我通常都只会让网络层可以工作,其余的都埋在了空想里面,然后不再管它了。但是,它还是会在那里,简直就是代码版的《泄密的心》,就在那等着出现问题。我不会去细究细节的了,不必多言,我讨厌开发网络层。
等等,我想到一个更好的方式!
我们可以用……你猜对了!……协议!??没错,就是协议:
protocol NetworkProtocol {
func go(completionHandler: () -> ())
}
在网络层协议,我们定义一个接收一个完成时处理者的参数的go方法。我的期望是我可以用这个方法来把网络请求“踢走”然后当请求结束的时候处理一些其他事情。但是,为什么选择协议?如果你喝酷爱的话,Swift是面向协议语言,在读了上面的引言之后,你会发现,枚举其实也是可以遵循协议的!我爱死酷爱了!
So here’s a problem I face at the start of nearly every project: “What’s the best pattern for me get and/or post data to an API on a server somewhere?” Every time I feel like I’m writing some code that feels a little too fragile for such an important piece of the program. So I usually get it working, bury it a bit through abstraction and leave it alone. But it’s still there, like a code Tell Tale Heart, just waiting for something to go wrong. I’m not going to go into it in detail, but suffice it to say: I hate working on networking.
There’s a better way!
We’ll start with an…you guessed it!…a protocol!?? Yes, a protocol:
protocol NetworkProtocol {
func go(completionHandler: () -> ())
}
In our Networkable protocol, we define a function go
which takes a completion handler parameter. My hope is that I can use this function to kick off a network request and then use the completion handler to do something when it’s done. So why a protocol? Well, if you drink the Kool-Aid, Swift is a “protocol oriented” language and if you read the excerpt above, you may have realized that even enumerations can conform to a protocol! I love Kool-Aid!
那就让我们一起来弄一个遵从Networkable protocol的枚举吧:
enum Network: NetworkProtocol {
case GET(url: NSURL)
case POST(url: NSURL, data: String)
func go(completionHandler: () -> ()) {…}
}
我虚构的REST API如你们在名为Network的枚举里面看到的那样,只接有GET和POST这两个情况以及各自的关联数值。毫无疑问地,这让我感到暖和舒适因为我知道用这样式的时候,我只能够尝试GET或者POST!
在实现go方法之后,代码如下:
enum Network: NetworkProtocol {
case GET(url: NSURL)
case POST(url: NSURL, data: String)
func go(completionHandler: () -> ()) {
switch self {
case .GET(let url):
let request = NSURLRequest(URL: url)
/// do some async request stuff here
/// call the completion handler when the request resolves
completionHandler()
case .Post(let url, let data):
let request = NSURLRequest(URL: url)
/// do some async request stuff here
/// call the completion handler when the request resolves
completionHandler()
}
}
}
So let’s take advantage of that and make a Networkable protocol conforming enum:
enum Network: NetworkProtocol {
case GET(url: NSURL)
case POST(url: NSURL, data: String)
func go(completionHandler: () -> ()) {…}
}
My imaginary REST API only has the ability to do do GETs and POSTs as you can see in my enum named Network which has two matching cases with associated values. Right off the bat this give me warm and fuzzies because I know I by using this pattern, I’ll only be able to attempt a GET or a POST!
When I fill in the go
function, it looks something like this:
enum Network: NetworkProtocol {
case GET(url: NSURL)
case POST(url: NSURL, data: String)
func go(completionHandler: () -> ()) {
switch self {
case .GET(let url):
let request = NSURLRequest(URL: url)
/// do some async request stuff here
/// call the completion handler when the request resolves
completionHandler()
case .Post(let url, let data):
let request = NSURLRequest(URL: url)
/// do some async request stuff here
/// call the completion handler when the request resolves
completionHandler()
}
}
}
很酷有木有!在go方法里面,对每个可能的状态,实例化一个NSURLRequest和调用completionHandler。那么该怎么使用它们呢?
let dummyURL = NSURL(string: "http://foo.bar")!
let getRequest = Network.GET(url: dummyURL)
getRequest.go {
println("你好!")
}
到目前为止看着还是很不错的,但还缺少了一些基础功能,也就是说API的响应。
你像下面这样在网络响应里面使用闭包的频繁度度如何?:
{(response, error?) in
if error != nil {
println("error!")
} else {
println("success")
}
}
这样太糟糕了好不好!
Very cool! Now in the go function we case through each possible type (GET, POST), create an NSURLRequest, and call the completion handler. What does it look like to use this?
let dummyURL = NSURL(string: "http://foo.bar")!
let getRequest = Network.GET(url: dummyURL)
getRequest.go {
println("hello!")
}
This looks pretty good so far, but it’s lacking some basic functionality, namely a response from the API.
How often have you used a pattern like this in a closure from a network response?:
{(response, error?) in
if error != nil {
println("error!")
} else {
println("success")
}
}
This sucks.
这样用是错的。如果你没发现这样打印信息是在倒退的话,你大概不是独自一人!如果不放慢速度读懂If error != nil的意味着什么,你是很难快速读懂上面这段代码的。而且这还还容易出现书写错误、理解错误。这当然是可以挽救的!我知道服务器会返回两种结果:成功 或 失败。这太简单了!用枚举来挽救:
enum NetworkResponse {
case Success(response: String)
case Error(error: String)
}
这里两个状态和各自对应的值类型弄好了。NetworkResponse必须是这俩其中之一的状态这样就不容易造成理解错误了。这样好太多了!现在让我们把这些代码组合起来看看会是怎样的:
protocol NetworkProtocol {
func go(completionHandler: (NetworkResponse) -> ())
}
enum NetworkResponse {
case Success(response: String)
case Error(error: NSError)
}
enum Network: NetworkProtocol {
case GET(url: NSURL)
case POST(url: NSURL, data: String)
func go(completionHandler: (NetworkResponse) -> ()) {
switch self {
case .GET(let url):
let request = NSURLRequest(URL: url)
/// do some async request stuff here
/// call the completion handler when the request resolves
let success = NetworkResponse.Success("呀!!数据!")
completionHandler(success)
case .Post(let url, let data):
let request = NSURLRequest(URL: url)
/// do some async request stuff here
/// call the completion handler when the request resolves
let error = NetworkResponse.Error("无结果 😬")
completionHandler(error)
}
}
}
It’s also wrong. If you didn’t catch that the print lines are backwards, you’re probably not alone! It’s difficult to read that quickly without slowing down to understand what is meant by if error != nil. It’s easy to write wrong. It’s easy to read incorrectly. This can definitely be improved! I know I have two kinds of results from my server: Success or Error. This is easy! Enum to the rescue:
enum NetworkResponse {
case Success(response: String)
case Error(error: String)
}
I’ve got my two types, and each has an associated value appropriate for its type. NetworkResponse must be one of these two types, which is unmistakable in what it means. I feel a lot better about this. So lets plug this all in together and see what it looks like:
protocol NetworkProtocol {
func go(completionHandler: (NetworkResponse) -> ())
}
enum NetworkResponse {
case Success(response: String)
case Error(error: NSError)
}
enum Network: NetworkProtocol {
case GET(url: NSURL)
case POST(url: NSURL, data: String)
func go(completionHandler: (NetworkResponse) -> ()) {
switch self {
case .GET(let url):
let request = NSURLRequest(URL: url)
/// do some async request stuff here
/// call the completion handler when the request resolves
let success = NetworkResponse.Success("YAAAS YOU GOT DATA!!")
completionHandler(success)
case .Post(let url, let data):
let request = NSURLRequest(URL: url)
/// do some async request stuff here
/// call the completion handler when the request resolves
let error = NetworkResponse.Error("No dice 😬")
completionHandler(error)
}
}
}
如下是如何完成一个GET请求的例子:
let dummyURL = NSURL(string: "http://foo.bar")!
let getRequest = Network.GET(url: dummyURL)
getRequest.go {(res) -> () in
switch res {
case .Success(let response):
println("GET Success: \(response)")
case .Error(let error):
println("GET Error: \(error)")
}
}
同样的,一个POST请求例子:
let dummyURL = NSURL(string: "http://foo.bar")!
let postRequest = Network.POST(url: dummyURL, data: "Hello 🌎")
postRequest.go {(res) -> () in
switch res {
case .Success(let response):
println("POST Success: \(response)")
case .Error(let error):
println("POST Error: \(error)")
}
}
And here’s how you’d use it to make a GET request:
let dummyURL = NSURL(string: "http://foo.bar")!
let getRequest = Network.GET(url: dummyURL)
getRequest.go {(res) -> () in
switch res {
case .Success(let response):
println("GET Success: \(response)")
case .Error(let error):
println("GET Error: \(error)")
}
}
and similarly how you make a POST:
let dummyURL = NSURL(string: "http://foo.bar")!
let postRequest = Network.POST(url: dummyURL, data: "Hello 🌎")
postRequest.go {(res) -> () in
switch res {
case .Success(let response):
println("POST Success: \(response)")
case .Error(let error):
println("POST Error: \(error)")
}
}
快看看啊!这样代码就很易读也很容易明白了! switch case确保你控制着所有类型同时很容易就可以定位到具体哪一个上。没有比这更好的了!我知道你现在非常激动!我知道的,尽情激动吧!
Look at it! LOOK AT IT! It’s so easy to read and understand what’s going on! The switch case makes sure you’re handling all the response types, and it’s really easy to tell which one is which. It doesn’t get much better than that. I know you’re excited. I can tell. Let it out.
![]()
There it is! Type safe, readable code that’s easy to write, and easy to understand exactly what’s expected and what you’re getting back! Now I’ve obviously skipped over some of the ugliness involved in communicating with an API, but what I’ve demonstrated can help you isolate that ugliness and wrap it in an enumeration much like a delicious flour tortilla wraps the tasteless filling in a Chipotle “burrito.”
You can download and play around with the code snippets above from my gist here: https://gist.github.com/jbergen/b10d78002b7aa55048ba
If anyone has any other interesting uses for enumerations, I’d love to hear!
Joseph Bergen is a Mobile Developer at BuzzFeed and is still pretty much a n00b. Find on Twitter!