你用枚举的姿势不太对啊!--You've been using enums in Swift all wrong!

翻译自:You've been using enums in Swift all wrong!


好吧,也许并没有全错,但至少没有充分发掘枚举的所有、炫酷的潜力。来,让我来向你们展示如何写一个超级简单、超级安全而且超级易读的基础网络层。首先,我们先来看看一些基础内容,如果你觉得自己已经熟悉这部分了,那就可以直接跳到下一节看主要内容。我不会怨你的。

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!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,290评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,399评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,021评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,034评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,412评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,651评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,902评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,605评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,339评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,586评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,076评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,400评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,060评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,851评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,685评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,595评论 2 270

推荐阅读更多精彩内容