探索 Swift 中的 MVC-N 模式
<p> </p> <p>Marcus 将会为大家介绍一种设计模式,他曾经在那些需要从互联网进行大量频繁数据请求的 iOS 应用当中使用此设计模式。这个设计采用了著名的 MVC (Model View Controller) 模式,并且在此基础之上对其进行了扩展,从而允许使用异步网络调用并与用户界面控制器相互隔离。</p> <p>See the discussion on Hacker News .</p> <p>Transcription below provided by Realm: a replacement for SQLite & Core Data that makes building iOS apps a joy. <a href="/misc/goto?guid=4958983978053241970" rel="nofollow,noindex">Check out the docs!</a></p> <p><a href="/misc/goto?guid=4958972548724433060" rel="nofollow,noindex">Get new videos & tutorials</a> — we won’t email you for any other reason, ever.</p> <p>About the Speaker: Marcus Zarra</p> <p>Marcus S.Zarra 自从 2003 年开始就开始开发 Cocoa 应用了,自 1996 年开始开发 Java 软件,并且自 1985 年开始进入到这个行业当中了。目前,Marcus 正在为 iOS 和 OS X 开发软件。除了编写软件之外,他还在 <a href="/misc/goto?guid=4959556017526956391" rel="nofollow,noindex">Cocoa Is My Girlfriend</a> 博客上发表关于开发的相关博文,并提供相应的代码示例,以帮助其他开发者。Marcus 同样还是 <a href="/misc/goto?guid=4959671323909066265" rel="nofollow,noindex">Core Data (3rd edition): Data Storage and Management for iOS, OS X, and iCloud</a> 这本书的著者,此外还是 <a href="http://www.amazon.com/Core-Animation-Simplified-Techniques-Development/dp/0321617754/ref=sr_1_1?ie=UTF8&qid=1307226039&sr=8-1" rel="nofollow,noindex">Core Animation: Simplified Animation Techniques for Mac and iPhone Development</a> 的共同著者之一。</p> <p><a href="/misc/goto?guid=4958963566680980887" rel="nofollow,noindex">@mzarra </a></p> <p>本篇文章并不会为大家介绍一种新的设计模式。相反,我们会探索某些『亘古不变』的内容:也就是关于网络层方面的东西。</p> <p>在我的职业生涯当中,我曾不止一次地见到过拥有相同反面模式 (Anti-pattern,译者注:也就是所谓的『不完善的设计模式』)的项目及代码库。它总是伴随着新项目的出现而出现。</p> <p>我们开始编程:我们已经构建了设计、UI,以及用户体验。我们会基于我们的 UI 或者 UX 设计稿来构建视图控制器,我们乐此不疲。然而,视图控制器需要数据( <em>在这个世界上不存在那种不需要使用数据的 iOS 应用</em> ),通常这个数据是从网络获取的。我们在视图控制器中通过一个网络请求来抓取数据(这是一个很常见的模式了),然后从服务器得到返回的数据。这完全没有问题:我们的 UI 将会展示出来,我们为之感到非常高兴。 <strong>第一步已经迈出</strong> 。</p> <p>接下来跳转到 UI 上的下一个页面,现在我们就必须要构建另一个视图控制器。这个控制器需要不同的数据,甚至可能是从另一个服务器获取的数据。您需要再次做一边同样的事情,因为我们在第一个视图控制器中所做的运行得很成功。我们构建界面,或许会使用故事板来构建。然后我们连接网络,获取数据,最后在屏幕上将其展示出来。 <strong>我们再一次成功了</strong> 。</p> <p>然后我们会构建第三个页面。在第三、第四,甚至第五个视图控制器当中,我们可能会意识到我们需要来自第一或者第二个视图控制器当中的数据,而这些数据可能已经从内存中清除掉了(或者您可能会做一些 <em>奇怪的事情</em> ,比如说在导航控制器当中抓取数据)。我们将会遍历包含视图控制器的数组,以找出所需的视图控制器的索引值,并在旁边提供注释(『不要移动这个!』),从而从其中拿出相关的数据。 <strong>这个也是没有问题的</strong> 。</p> <p>然后,UX 设计师过来告诉你:『我们需要对 UI 当中的一些界面重新进行排序,不过会给用户带来更好的用户体验』。他们建议将第四个视图控制器移动到第二个位置上。 <strong>哦,这出问题了</strong> ,因为我们需要的是在序列当中第二个视图控制器中获取的数据。现在咋办?</p> <p>您或许可以选择复制代码,但是我们知道这是一个糟糕的主意。您可能会开始做一些『有意思』的工作。或许您可能会在屏幕之外去加载视图控制器,或者在初始化视图控制器但是却不把它放到堆栈当中( <em>将其提交后,我们就完成了获取数据的所有操作,因为您想要这段代码可以运行</em> )。接着您可能会想了『我需要做点什么,比如说缓存数据什么的』。说实话,您可以使用任何您希望使用的持久化引擎。</p> <p>我会选择 Core Data(我也很:heart:它)。Realm 也是不错的选择,JSON 文件也是可用得的,只要可以用来缓存数据就可以了。</p> <p>因此,您缓存了数据,改变了图层,然后每个视图控制器仍会留在缓存当中,以便能够获取数据,当任何一个视图控制器出现之后,都可以在缓存之外获取数据。 <strong>不过,这仍然还是一个坏主意</strong> 。</p> <p>这就是我所说的『反面模式』。这种类似的东西我见到过的已经数不胜数了,我自己也写过类似的东西。我们讨厌这样的模式。我在此公开呼吁大家:别在这么做了。 <em>我们想要写出好的代码,我们想要看到正确的结果,然而我们却写出这样糟糕的代码。这着实是一个严肃的问题</em> 。</p> <p>我们现在都普遍应用着 MVC 架构,但是我们需要在 MVC 的基础上多增加点东西。</p> <p>我想介绍另一种控制器,不过不是所谓的视图控制器。</p> <p><em>注意:如果您不喜欢『控制器』一词的话,那么可以使用『管理器 (manager)』、『处理器 (handler)』或者其他您觉得可以的词。关于是这是一个控制器对象,因为它既不是视图,也不是模型</em> 。</p> <p>这就是我们应该在 <strong>设计时</strong> 所应该做的事,而不是在第二天早晨发布版本的关键时候,说:『我需要对所有这些东西进行重构』。当我们拥有 UI/UX,我们直到应用是需要如何出现的,这也是我们需要查看的时候所需要了解的东西。</p> <p><em>『我需要从网络获取数据。我应该把这个从网络获取出数据的代码放到何处呢?』</em> 这就是 MVC-N 大显身手的时刻了。</p> <p>我们将拥有一个网络控制器:它会与互联网、您的本地网络,甚至与纯文本文件 (Flat file) 系统建立连接。接下来它会将数据放到您的缓存、或者持久化存储引擎当中。这里最精彩的部分在于:您的整个视图层都只与缓存进行沟通交流。它不会直接从网络中获取任何数据。 <strong>视图控制器的任务就是展示或者更新数据就可以了</strong> ,但是不会执行任何的网络操作。这个设计可以伴随您的一生,并且它拥有许多好处,以至于您就不会再去使用反面模式的代码了。</p> <pre> <code>class NetworkController: NSObject { let queue = NSOperationQueue() var mainContext: NSManagedObjectContext? func requestMyData() func requestMyData() -> NSFetchedResultsController func requestMyData(completion: (Void) -> Bool) }</code></pre> <p><code>这里没有太多的内容:这是一个对象。我让它继承了一个对象(即便是在 Swift 当中)因为我会在我的网络控制器当中使用 KVO。您或许不必跟着我这样做;如果您使用的是另一种不同的缓存系统的话,您或许就不需要 KVO。</code></p> <p><code>这 <strong>只是</strong> 一个对象而已,我们不必要根据视图控制器(或者其他类似的东西)来决定它是什么样的。它是一个底层对象。我们在其中设立了两个属性:一个变量和一个常量。我们持有一条队列和我们的缓存机制(Core Data, <em>因为我很喜欢 Core Data</em> )。 </code></p> <p><code>暴露给您的视图控制器的将是相关的便利方法和语法糖。我们可以用多种方式来构建这些内容,这取决于您有多信任那些制作 UI 的人。如果您信任他们的话,您可以使用一个函数,然后说『去获取我的数据吧』,您可以信任他们,给予他们直接访问缓存层的权利,将数据取出,然后在他们各自的视图控制器当中展示出来。然而,我并不是非常信任他们,因此我会给他们第二种版本的方法,也就是用返回 NSfetchedResultsController 来替代。 </code></p> <p><code>NSfetchedResultsController 对于 Core Data 来说是一个很好的用于连接缓存和视图控制器的胶水代码 (Glue Code)。它会将所有数据以所需的结构,按照所需的过滤模式,并依据所需的次序进行排列,最后传递给视图控制器。如果您拥有一个地址列表的话,它会按照字母顺序将这个列表进行重新排列,提供给您以进行填充。 </code></p> <p><code>NSfetchedResultsController 的另一方面就是,它会在数据发生变化的时候通知视图控制器。视图控制器无需与缓存进行交互,它所需要的,只是 NSfetchedResultsController 就可以了,从中可以拿出相应的数据,它会提供相关的委托回调来通知何时数据发生了改变。这极大地简化了视图控制器。 <strong>您的视图控制器只需要等待被通知刷新就可以了</strong> 。 </code></p> <p><code>如果您的应用在遵循某种常见的 UX 模式的话(例如下拉刷新),您的视图控制器或许会想要更多的信息,例如数据发生了更新。它可能需要知晓数据请求何时结束。如果您拥有一个下拉刷新的话,然后用户不停地下拉、下拉、下拉,这时候您就进入了一个未知的隧道当中——用户可能会发出多个请求。</code></p> <p><code>您或许会想要用某种方式提示他们说:『请求正在运行,暂未结束』。这就是一个所谓的承诺系统 (Promise System)。您可以将它们传递到一个闭包中,然后说『让我知道您结束的时刻。我可以更新刷新控件,然后不再用这个消息缠着您』。</code></p> <p><code>回到网络控制器这里来,这就是所有它应该拥有的外部接口。这些就是您想要公开的东西,甚至可能没有这个队列或者上下文,这取决于您有多信任制作 UI 的那帮人。</code></p> <p><code><em>那么我们用这个网络控制器做些什么呢?</em> 我们需要深入视图控制器内部。有以下几种方式: </code></p> <p><code>这只是我个人的建议。当您的应用启动之后,就会调用 applicationDidFinishLaunching 方法。您可以在这里构建您的根视图控制器,您也需要在这里构建网络控制器,这样您就可以将网络控制器传递到根视图控制器当中。接着,当下一个场景或者下一个视图控制器出现的时候,根就会将网络控制器传递给下一个,依此类推。这就是所谓的『依赖注入 (Dependency Injection)』。 </code></p> <p><code>这是从 Objective-C 那里得来的一个简单的概念,这样就不需要任何第三方框架了:我们只需要传递这个对象,然后设置为下一个对象的属性就可以了。</code></p> <p><code>第二种方法是我很讨厌的方式:您可以将网络控制器转变为单例的形式。这是一个完全安全的单例,因为单例的缺点之一就是不能有会导致单例失效的情况发生。您无法对单例进行重置,因为这会违背这个要求。不过,因为网络控制器本身完全是一个空的架构,它很容易重置其内部的要素,因此您可以放心大胆地使用单例。</code></p> <p><code>不过,我还是建议您不要使用单例。单例是万恶之源。如果您的系统和应用需要使用单例,并且也只能使用单例的话,那么我也不会(太)反对您。继续用就是了。</code></p> <p><code>第三种方式,同时也是最糟糕的一种方式,就是直接访问 AppDelegate。您的每个视图控制器直接访问 AppDelegate,然后调用 UIApplication.ApplicationDelegate 然后将其进行转换。我同样也强烈反对您使用这种方式,因为它会将元件的耦合度增加太多。 </code></p> <p><code>总体来看,请使用依赖注入这种方式。另外两个方法都可能在生产环境中等着您踩雷。</code></p> <h3><code>网络控制器 - NSOperation </code></h3> <pre> <code><code>class MyNetworkRequest: NSOperation { var context: NSManagedObjectContext? init(myParam1: String, myParam2: String) }</code></code></pre> <p><code><code>网络控制器是 <strong>基于网络操作 (Network Operation) 进行封装的</strong> 。网络操作执行后会生成一个对网络的请求操作;数据回传并处理之后,这样您接下来就可以将其保存到缓存当中,这样所有操作就结束了。这些都是操作的 <strong>任务最小离散单元 (small discrete unit of work)</strong> 。这是很优雅的做法,当您编写代码的时候,您可以在屏幕上看到任何您想要的东西。您可以轻易理解它,它只做一件事情:这很赞,所生成的代码也很好维护。这就是我们想要在网络控制器当中所构建的东西。 </code></code></p> <p><code><code>我喜欢使用一个继承自 NSOperation 的 MyNetworkRequests 。每个 NSOperation 都是专门只执行任务最小离散单元的,比如说获取 推ter 的时间轴信息。当您让操作执行任务最小离散单元的时候,您可以将这些离散单元串联在一起,然后对代码进行重用。 </code></code></p> <p><code><code>关于重用代码有以下两种思路:</code></code></p> <ol> <li> <p><code><code>『我会将其发布在网上,这样每个人都可以通过 CocoaPod 来进行重用。』:-1:</code></code></p> </li> <li> <p><code><code>『我会在应用的 15 处不同的地方使用相同的代码。我需要做的就是在一处地方进行改变,我的整个应用都会进行更新。』:+1:</code></code></p> </li> </ol> <p><code><code>这些方法看起来怎样?</code></code></p> <p><code><code>这些都是 NSOperation 的子类。在它们当中,有两个我们可以设置的属性。再说一次,我们使用的是依赖注入:我们将某项对缓存的访问操作注入到当前操作当中,然后再传递进一些参数(或许是 NSURLRequest 、 URL 或者某些和搜索参数一样简单的内容),然后让这个操作自行构建完整的 URL。 </code></code></p> <p><code><code><em>警告</em> :大段代码即将来袭!不要被吓跑了哦~:runner: </code></code></p> <pre> <code><code><code>class MyNetworkRequest: NSOperation { var context: NSManagedObjectContext? private var innerContext: NSManagedObjectContext? private var task: NSURLSessionTask? private let incomingData = NSMutableData() init(myParam1: String, myParam2: String) { super.init() //在这里设置参数 } var internalFinished: Bool = false override var finished: Bool { get { return internalFinished } set (newAnswer) { willChangeValueForKey("isFinished") internalFinished = newAnswer didChangeValueForKey("isFinished") } } }</code></code></code></pre> <p><code><code><code>开始的时候,我们继承 NSOperation 。我们有好几个属性设置为私有的,它们对于 NSOperation 来说是可内部访问的,因此我们把它们标记为私有,这样可以提醒我们,不要在外部碰这些玩意儿。 </code></code></code></p> <p><code><code><code>第一个属性是 innerContext (为了可以在多线程上使用 Core Data,我们可以持有多个上下文变量)。如果您打算使用不同的缓存机制的话,请遵循它们对于多线程访问的设计指导。 </code></code></code></p> <p><code><code><code>对于 NSURLSession 来说,我是又爱又恨(不过更倾向于喜欢),但是对于在 iOS 9 已经废除的 NSURLConnection 来说,已经是一个极大地提升了。我讨厌的部分是,如果您想要使用它的闭包实现 (block implementation) 的话,这将是一个棘手的问题。因为它会让您折回到 UIViewController 里面去,然后突然一下子您让当前的这个视图控制器离开了屏幕,结果发生了内存问题。不过,您同样可以在 NSOperation 设计当中使用它,我觉得这是最好的方式。 </code></code></code></p> <p><code><code><code>我们不能够再使用 NSoperation 当中的主要方法了。现在,无论您再怎么努力, NSOperation 都不会在您执行网络连接的时候暂停了。您可以使用私有 API( <em>不建议这么做</em> ),您也可以做一些同步的操作,或者操作锁定的操作( <em>不建议</em> )。在 iOS 6 或者 7 当中,它们会在不再执行并发的时候,改变 NSOperation 。您无法在已经调用过操作的线程上再让另一个操作运行起来了。无论您将 isConcurrent 属性设置为何值,它都会忽略您的操作。它始终保证能够在另一个线程,或者是另一个队列上运行。 </code></code></code></p> <p><code><code><code>我们可以使用 NSOperation 的并发设计来执行我们的网络请求。在 Swift 当中,由于我们不能够直接访问 finished 属性,我们必须要做一点黑科技(关于这个我已经提交了一个 radar,希望他们能够修复这个问题)。重载这个 <strong>变量</strong> 的目的在于,我们需要告诉队列我们已经结束了操作。为了实现这个效果,队列将会监听 “isFinished” 这个属性。在 Objective-C 当中, isFinished 翻译为变量 Finished 是完全没有问题的;在 Swift 中,它只能够监听 “isFinished”,它实际上并不能够监听实际的 Finish 属性,您无法对此进行改变。 </code></code></code></p> <p><code><code><code>这就是为什么我们的 setter 方法非常有趣,因为其中增加了 KVO 这一模块。不管我们有没有结束任务,我们都会让自己的内部状态变量保持挂起状态。无论何时我们对其进行了改变,我们都会让 isFinished 访问器触发 KVO。 </code></code></code></p> <pre> <code><code><code><code>func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveResponse response: NSURLResponse, completionHandler: (NSURLSessionResponseDisposition) -> Void) { if cancelled { finished = true sessionTask?.cancel() return } //检查回调代码,并根据结果做出相应的回应 completionHandler(.Allow) }</code></code></code></code></pre> <pre> <code><code><code><code><code>func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) { if cancelled { finished = true sessionTask?.cancel() return } incomingData.appendData(data) }</code></code></code></code></code></pre> <p><code><code><code><code><code>您可以将下面这两个方法放到一个抽象父类当中。通常情况下,您可以获取回调结果,然后离开网络控制器,说『去连接此服务器,然后请求该数据』。服务器会立即给您相应的回调结果。</code></code></code></code></code></p> <p><code><code><code><code><code>注释所在的地方是您放置逻辑和查看回调结果的地方:『状态返回码是什么?是不是正常的 200?』好的,万事大吉。『如果不是呢?』好吧,应用崩溃了。但是我们想要对这种情况进行处理。一旦我们知道我们成功的获取到了来自服务器的数据、状态,我们就向完成闭包 (completion block) 当中传递一个参数,这个闭包会分析我们传递进去的数据,然后告知我们 (.Allow) 。随后告知 URLConnection 继续执行,开始将获取的数据返回到我们的 NSURLSession 当中。 <em>如果您随意测试一下便会发现,这个方法的实现运行良好。</em> </code></code></code></code></code></p> <p><code><code><code><code><code>另一个方法可能会被多次调用,也可能不会被调用。调用的次数并不完全取决于数据的大小。有许多变量可以决定它,例如说蜂窝数据和 Wi-Fi 状态下调用次数会不同,或者是否发生丢包现象。我们要做的唯一一件事情就是将它添加到我们内部的变量当中,因为我们不知道我们会获取多少次数据。我们需要保持调用,保证数据的持续输入,然后将这些比特流持续添加到 NSData 数组当中,然后我们要持续等待,直到我们得到另一个回调为之,也就是我们是否被告知网络访问已经结束。 </code></code></code></code></code></p> <pre> <code><code><code><code><code><code>func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) { if cancelled { finished = true sessionTask?.cancel() return } if error != nil { self.error = error log("Failed to receive response: \(error)") finished = true return } //向 Core Data 中写入数据 finished = true }</code></code></code></code></code></code></pre> <p><code><code><code><code><code><code>关于上面这两个另外的部分是最后的部分了,但是这也是最有趣的部分。</code></code></code></code></code></code></p> <p><code><code><code><code><code><code>这个方法是我们能得到的最后一个部分,它将要表述『我成功地获取了数据』或者『我没有成功获取数据』。不管从服务器获取数据成功与否,它们都将会调用此方法。如果获取数据失败的话,我们会将错误对象进行赋值,随后我们会对这个错误进行处理。我们将会检查 cancelled: 以确定我们是否取消了网络操作,我们对发生的错误并不关心。 </code></code></code></code></code></code></p> <p><code><code><code><code><code><code>不过我们需要检查错误。您可以使用 if let ,但是这货给您带来的帮助并不大。我一般会将这个错误挂起。外面如果有人使用这个操作,并且还监听这个操作是否已经完成的话,那么他便可以查看这个错误,然后对其做出回应。从这里,我不想要告诉 UI 层哪里出问题了,因为这些人的任务只是将数据提取出来,然后根据数据做出相应的处理就够了。假设我们一切顺利(例如我们没有取消网络操作,并且我们也没有遇到任何错误), <strong>这时候我们便已经有了从网络上下载下来的数据了</strong> 。我们已经结束了网络的访问,这样我们便可以开始下一步操作。我们或许会在这里处理这个数据。 </code></code></code></code></code></code></p> <p><code><code><code><code><code><code>在同样的操作当中,我们将其保存到缓存当中;或者,我们可能会决定链接到另外的操作当中。这两个都是解决此问题的有效解决方案。</code></code></code></code></code></code></p> <p><code><code><code><code><code><code>但是 <strong>我会在这里对数据进行处理</strong> ,然后再存放到缓存当中。我不会通知我的 UI:我只是将数据放到缓存当中,只有缓存有责任去通知 UI。我们必须在底部设置 finished = true 。 </code></code></code></code></code></code></p> <p><code><code><code><code><code><code>因此,这就是我所谓的 MVC-N 了,我希望大家都可以使用这个设计实现。首先,就如我所讨论过的,任务最小离散单元让代码更容易理解、使用。当下一个开发者接手并需要修复网络控制器中的某个问题的时候,他们不必看完一大段长长的代码;他们只需要查看相的独立代码了。这是一场巨大的胜利。</code></code></code></code></code></code></p> <p><code><code><code><code><code><code>有一次,我在旧金山这里加入到了一个项目当中。他们的主程去度假了,因此他们让我加入进来去解决主程离开之后出现的那些 BUG。他们发现了一个 BUG,然后跟我说『如果您能够在两周内解决这个问题,我们就可以按时上线了』。</code></code></code></code></code></code></p> <p><code><code><code><code><code><code>这个 BUG 是:『在启动的时候,数据可能会出现,也可能不会出现』。这就是这个 BUG 的表现形式。</code></code></code></code></code></code></p> <p><code><code><code><code><code><code>我拿到了源代码,然后我启动了应用,但是我没发现这个问题。我等了会儿。一些绘制的很漂亮的 TableView 单元格出现了,然后它们消失了。接着另一个不同的单元格又出现了,然后又消失了。接着,数据终于趋于稳定正确了。</code></code></code></code></code></code></p> <p><code><code><code><code><code><code>我一遍又一遍地启动,尝试了不同的次序以获取不同的数据,但是这仍然导致相同的结果。我必须一遍又一遍地尝试,尝试了大概半个小时,还没有看任何代码,只是不停地重启而已。随后我就开始看代码了:</code></code></code></code></code></code></p> <p><code><code><code><code><code><code>我发现他们使用的是 Core Data,因此我可以立刻上手。我试图去在视图控制器当中寻找网络代码,但是实际上他们使用了一个网络控制器。他们在一个文件中对其进行了本地化;我觉得这应该要更简单、更好用。</code></code></code></code></code></code></p> <p><code><code><code><code><code><code>我打开了这个网络控制器(他们叫它『DataHandler』),里面有 12,000 行代码。</code></code></code></code></code></code></p> <p><code><code><code><code><code><code>这玩意儿有点蠢蠢的。开发者通过写了一个方法,告诉它『快去获取数据吧!』。当数据回传之后它们调用了一个闭包。在其中,它使用了另一个闭包用以将数据转换为 JSON 格式。在这里,它使用了另一个闭包将数据存放到 Core Data 当中。闭包中嵌入了闭包,闭包中又嵌入了闭包。不停地异步、异步、异步。</code></code></code></code></code></code></p> <p><code><code><code><code><code><code>如果这货只执行一次的话,那么它绝对不会有问题。他重复使用了这个模式,最终同时调用了将近 15 个网络调用;15 个异步的网络调用返回了 15 个异步数据处理调用,然后又返回 15 个异步缓存调用,然后又引起了 15 次以上的 UI 更新。这些全都是异!步!的!哪个数据首先出现在屏幕上完全取决于哪个调用首先抵达服务器,哪个调用首先返回,以及哪个调用首先同步到主线程上面。这绝对是一个烂摊子。</code></code></code></code></code></code></p> <p><code><code><code><code><code><code>这就是闭包调用的陷阱之一。单次运行,没有任何问题。您觉得『这是一个相当简单、精简、独立的代码单元了:我要重复使用它』。</code></code></code></code></code></code></p> <p><code><code><code><code><code><code>我们可以通过执行操作来修复这个问题。我们将这些操作放到一个队列当中。队列控制调用的频率如何,以及我们对这些操作如何触发进行更多的控制。通过这玩意儿,我们可以让单元测试变得轻松,因为现在我们有了任务最小离散单元了。我们可以初始化操作;我们可以手动触发它。我们可以调用 OperationStart ,然后它就会触发掉,之后我们就可以自己监听结束方法了。我们可以构建单元测试,然后在主线程运行,然后等待结束,最后根据数据做出回应。我们可以对每一个操作进行独立测试。 </code></code></code></code></code></code></p> <p><code><code><code><code><code><code>我们可以更进一步。如果我们向操作当中注入 URL,我们甚至可以控制操作发生的地方。我们可以使用在硬盘上的纯文本文件,然后现在我们就有了一致的输入,这样就可以测试输出了。</code></code></code></code></code></code></p> <p><code><code><code><code><code><code>甚至我们还可以更进一步,我们可以使用一个已知的 SQLite 文件作为缓存来进行测试。现在,我们有了已知的输入,已知的原始数据,我们就可以测试输出了。我们的单元测试是端对端的,这样就可以测试每个网络调用了。</code></code></code></code></code></code></p> <p><code><code><code><code><code><code>在闭包代码中,这就比较难做了;您最终不得不选择进行数据模拟。我们要做的就是说『您不再是与服务器进行数据交流,而是和这个文件进行数据交流』,随后您就会去使用这个 SQLite 文件,而后现在『我打算去测试您的结果,以确保您得到了正确的回调结果』。单元测试变得更容易、轻松了。</code></code></code></code></code></code></p> <p><code><code><code><code><code><code>您从前可能没有听说过 Steve Jobs 的这条格言,这是我最喜欢的一句话之一: <em>『当我的代码准确无误的时候,人们都视之为常。我并没有得到任何的赞誉,也没有得到任何的鼓励。而当我的代码出现问题的时候,所有的责备蜂拥而至』。</em> </code></code></code></code></code></code></p> <p><code><code><code><code><code><code>这就是我所在的处境。当这个网络控制器工作良好的时候,没有人在意这个问题。当我的网络操作没法工作的时候,突然间他们就说我的应用跟:hankey:一样。无论我们有多少 UI 工程师,无论我们在 UX 上面花了多少钱,这个应用就是个渣,因为我没法在我坐地铁的时候获取数据。</code></code></code></code></code></code></p> <p><code><code><code><code><code><code>这就是网络层的责任所在。存储层应该告知用户『我无法连接网络,这里是最近一次我得到的数据』。如果您无法理解这个概念的话,退出您手机上的 非死book,然后到一个电梯里面去,把电梯门关掉,然后再启动 非死book。啊哦,没法用了。我们可以通过对应用进行恰当的设计,添加缓存,从而修复这个问题。</code></code></code></code></code></code></p> <p><code><code><code><code><code><code>操作可以探知网速</code></code></code></code></code></code></p> <p><code><code><code><code><code><code>最重要的是,我们也可以关心 <strong>网络的当前状态</strong> 。当我们开始一个操作的时候,我们知道它会以调用 NSState 开始,以再次调用 NSState 结束,这样我们就可以将两者互减,就可以知道这个操作花费了多长时间。我们同样也知道返回了多少数据。 NSData 有一个名为 length 的属性,这告诉了我们从服务器获取回来的这条数据当中有多少位的数据。位长 / 时间 = 带宽。如果我将带宽告知给我的网络控制器,并对这些带宽聚合求平均值的话,我就可以根据我的网络状态添加一个 <strong>动态运行时的解决方案</strong> 了。我知道我的网络的速度是多少,并且精确到秒。 </code></code></code></code></code></code></p> <p><code><code><code><code><code><code>通过队列来控制性能</code></code></code></code></code></code></p> <p><code><code><code><code><code><code>我可以对此做出回应,因为 NSOperationQueue 有一个概念叫作『并发 (Concurrency)』。这样我就可以说『网络状态良好,那么我们就启动并发,尽可能多地一次性运行完全部操作』。或者,我也可以返回来说『网络很糟糕。我们要把速度放慢下来,一次只执行一件事情,然后在我所有的 NSOperations 中设置优先级,这样高优先级的操作会优先触发』。 <em>而不是在 推ter 上等待猫咪的图片刷出来,文字内容会优先加载,而喵咪的图片:smiley_cat: 会在后面加载出来</em> 。 </code></code></code></code></code></code></p> <p><code><code><code><code><code><code>网络控制器可以对网络状态做出回应</code></code></code></code></code></code></p> <p><code><code><code><code><code><code>现在我的应用反应更灵敏了,因为时间线加载请求比其他请求具有更高的优先级。</code></code></code></code></code></code></p> <p><code><code><code><code><code><code>我可以更进一步,『网络状态不好,我会取消所有的低优先级操作』。我可以暂停所有对图片的请求,这样我就可以离开时间线请求,直到网络状态改善,再去发表东西。</code></code></code></code></code></code></p> <p><code><code><code><code><code><code>取消操作的关键位置</code></code></code></code></code></code></p> <p><code><code><code><code><code><code>同样,对于取消操作而言,我们有一个很关键的位置。当应用退出的时候,我可以杀死所有的操作。我可以这么说『应用准备退出了,取消所有的头像加载请求,取消所有的时间线加载请求。如果您试图发表东西的话,我就会去向系统申请更多的时间,然后让那个操作结束』。即使我已经退出了应用,您的发表状态仍然还是在进行当中。</code></code></code></code></code></code></p> <p><code><code><code><code><code><code>我们不必要这么做。我们没必要侮辱我们的用户,然后迫使他们等待我们这些蹩脚的代码。</code></code></code></code></code></code></p> <p><code><code><code><code><code><code>后台处理的单决策点</code></code></code></code></code></code></p> <p><code><code><code><code><code><code>您知道当您试图退出应用的时候,您按下了 Home 按钮,但是这时候应用卡死了!用户就会想『啊,这个手机跟:hankey:一样。速度太慢了』。手机:这个锅我不背。这个锅应该归咎于写代码的人,他告诉应用『准备进入后台』,因此应用给系统回应『好吧,我要先来执行这个操作,再进入后台』。这样操作系统会停止一切操作,直到应用的这个方法返回。因此这会带来几秒中的延迟,然后才给应用传递 kill -9 。 </code></code></code></code></code></code></p> <p><code><code><code><code><code><code>这会导致手机的主线程被阻塞,直到所有的工作完成为止,因为后台处理的操作远比前往其他应用的操作更重要。我们没必要这么做。我们可以做得更好一些,即使别人不会注意到,不过我们的应用会运行得更好,操作系统也会很乐意看到这样的。</code></code></code></code></code></code></p> <ul> <li> <p><code><code><code><code><code><code>自行完成网络代码</code></code></code></code></code></code></p> <p><code><code><code><code><code><code>您注意到,我没有使用 AFNetworking ,我也没有使用 ASIHTTPRequest 。请自行完成。我保证您自行完成的代码远比任何通用的代码要快得多,这是一项常理了。因为您写的东西是非常具体到您的业务的,它将会比任何通用的东西快得多。 </code></code></code></code></code></code></p> </li> <li> <p><code><code><code><code><code><code>保证所有的网络代码都归类到一起。不要把网络代码放到视图控制器当中。</code></code></code></code></code></code></p> <p><code><code><code><code><code><code>不然的话您所做的就是将您的活动范围限制在一个角落当中,出了问题之后您就不得不选择重构。一旦您在视图控制器当中写了网络代码的话,在它后面放上一个 TODO 标记:『我需要对其进行重构』。避免那样做,您应该:执行网络代码,把它放到一个地方,这样您会更轻松。</code></code></code></code></code></code></p> </li> <li> <p><code><code><code><code><code><code>所有的 UI 都应该根据 Core Data 来表现</code></code></code></code></code></code></p> <p><code><code><code><code><code><code>您的所有 UI 都应该根据缓存来显示。您的 UI 不应该直接与网络直接交互。 <em>在一个理想状态下,UI 并不知道您有没有对网络进行访问。它所知道的就是缓存当中有它想要的数据</em> 。 </code></code></code></code></code></code></p> </li> <li> <p><code><code><code><code><code><code>分离数据采集和数据显示</code></code></code></code></code></code></p> <p><code><code><code><code><code><code>保证数据显示和数据收集彼此分离:这样您的应用就更容易维护,更容易修改,更容易添加新的功能,并且更容易重用。</code></code></code></code></code></code></p> </li> </ul> <p><code><code><code><code><code><code>我现在工作的项目当中,我们已经重写了 4 次代码了,因为我们不知道 UI 应该是何种样子。我们仍旧在开发当中。然而,无论我们怎么折腾,底层的网络层仍旧工作良好。我们已经重构了许多次 UI 层了,但是我们不必去重写网络层,因为它是完全抽象的,我们已经有三个月没去碰它了。</code></code></code></code></code></code></p> <p><code><code><code><code><code><code>这就是我希望大家所做的。 <strong>请停止将网络访问代码放到您的视图控制器当中。</strong> 将您的网络视作应用的一等公民。如果您的网络访问代码受影响了,那么您的整个应用也会受影响。每次您坐在 ail 应用面前,然后疯狂的下拉刷新,您或许会想:这个网络访问写得真糟糕。我们很容易让它变得更容易一些,一旦您开始这么做,您就不会使用别的方式了。 </code></code></code></code></code></code></p> <p><code><code><code><code><code><code>如果您有什么想问我的问题的话,请在 <a href="/misc/goto?guid=4958963566680980887" rel="nofollow,noindex">推ter</a> <a href="/misc/goto?guid=4959671324064854224" rel="nofollow,noindex">[email protected]</a> </code></code></code></code></code></code></p> <p><code><code><code><code><code><code>,或者给我发送 <a href="/misc/goto?guid=4959671324150610410" rel="nofollow,noindex">邮件</a> </code></code></code></code></code></code></p> <p><code><code><code><code><code><code>。我很高兴回答诸位的问题。</code></code></code></code></code></code></p> <p> </p> <p><code><code><code><code><code><code>See the discussion on Hacker News . </code></code></code></code></code></code></p> <p> </p> <p><code><code><code><code><code><code><a href="/misc/goto?guid=4958972548724433060" rel="nofollow,noindex">Get new videos & tutorials</a> — we won’t email you for any other reason, ever. </code></code></code></code></code></code></p> <p>来自: <a href="/misc/goto?guid=4959671324244842692" rel="nofollow">https://realm.io/cn/news/slug-marcus-zarra-exploring-mvcn-swift/</a></p> <p> </p>
本文由用户 AudCadman 自行上传分享,仅供网友学习交流。所有权归原作者,若您的权利被侵害,请联系管理员。
转载本站原创文章,请注明出处,并保留原始链接、图片水印。
本站是一个以用户分享为主的开源技术平台,欢迎各类分享!