操作抽象设计-实践

作者: Delpan | 来源:发表于2017-12-18 16:04 被阅读889次

    前言

    最近在做程序优化和代码总结的工作,在优化和总结的过程中发现,程序中存在着许多重复性的交互代码,特别是在业务逻辑层,虽然业务模块本身具有独立性,各业务模块之间也有比较明确的分界,但各业务模块内部不免还是存在着一些重复性的交互代码。

    例如,模块A有一个收藏功能,模块B有一个评论功能,完成收藏事件与评论事件都需要确认用户是否登录,这时模块A需要与登录模块交互,模块B也需要与登录模块交互,为了确保登录后可以继续完成相应的事件,可能还需要保存一些临时的数据与状态,这些模块都需要与登录模块交互,且都存在这些交互过程,这就使得各业务模块都存在着一定重复性的交互代码,我们需要一种方式来解决这些重复性交互的问题。


    目录

    -操作抽象设计

    -改善Alert用户体验

    --方式一

    --方式二

    --方式三

    -优化启动流程

    --main函数调用前

    --main函数调用后

    -操作抽象实现

    --操作抽象

    --NSOperation的内部实现

    --操作管理抽象

    Demo


    操作抽象设计

    程序中的各模块之间,或多或少都会存在着一些重复性的交互代码,逻辑对象存在重复的交互过程,如前言中的例子。我们该怎么解决这类问题?以什么方式,什么思路来解决?要回答这些问题,首先我们需要知道哪些交互过程是重复的,是怎么重复的,所以我们先来看看例子中的交互过程是怎样的。

    由于不同程序的构架不同,不同团队的处理方式也不同,所以我们忽略这个交互过程是否合理。观察两个交互过程我们会发现,其中有一部分交互是完全相同的。

    相同的交互过程存在于两个不同的独立模块中,面对这种情况,我们可以选择扩展现有的抽象,把相同的交互过程封装到扩展的抽象中,例如扩展用户抽象(User),把相同的交互过程封装到用户抽象中,那么整个交互过程就改变了。

    经过这样的处理,问题好像得到了解决,把相同的交互过程封装到用户抽象后,模块A或模块B完成相应事件的交互过程就被简化了,所需要交互的对象也减少了。但程序的架构或处理方式会随着程序的迭代而作出一些调整,交互过程也可能需要作出相应的调整,而直接依赖具体模块或具体组件会使得程序难以维护和扩展。例如随着程序的迭代,需要把原本封装在用户抽象中的相同交互过程,调整为封装在登录控制器抽象中,那么接下来的工作就是把原本依赖于用户抽象的模块都改为依赖登录控制器抽象。

    这样的依赖关系,别说扩展了,连维护都是个大问题,所以在日常开发当中,我们都会尽可能地避免这种情况的发生。如何避免直接的依赖关系,这是解耦的问题,通常我们可以选择在具体模块或组件之上加一层抽象来避免这种直接的依赖关系,我们还可以选择加入中间件,让各模块或组件通过中间件来交互。用中间件来交互的例子有很多,例如在mach内核中,各任务通过端口传递mach报文的方式来交互。

    通过与中间件交互,而不是直接与具体模块交互,虽然可以解决直接依赖具体模块的耦合问题,但这仅仅只是解决了模块之间的耦合问题,而并没有减少模块所需要处理的交互。

    通过前后对比就会发现,模块A的交互过程并没有发生变化,仍然是先开始登录操作,收藏操作在登录操作完成后才开始,而模块A为了能够正确地开始收藏操作,还可能需要保存一些收藏操作所需的临时数据和状态。解决耦合问题并不是我们的目标,我们所期望的是既可以解决耦合问题,又可以减少交互,还要尽可能地避免重复的交互过程,所以单纯地加入中间件并不能满足我们的需求。我们需要设计一种新的方式,所以先抛开过往的一切,抛开那些惯性的思维,回到最初的起点,重新来分析问题。

    我们先把视角切换成面向过程,模块A需要完成收藏事件,所以先对收藏事件做算法分解,收藏事件的整个处理过程由登录和收藏两个子过程组成。

    算法分解完成后,现在我们把视角切换成面向对象。以过往的处理方式,我们会把登录和收藏这两个子过程抽象成对象的行为,再由模块A与相关的对象来完成事件,但正如前面所看到的,模块A需要顺序地和相关对象进行交互,也就是说模块A需要处理整个交互过程,而我们的目标是减少交互和避免重复的交互过程,所以这种方式不符合我们的需求。这里需要明确提出的一点是,这种处理方式本身并没有什么问题,我们所讨论的并不是这种处理方式的对与错,所以你完全可以基于这种方式来开发。

    既然过往的抽象方式不能达到目的,所以我们需要尝试其它的抽象方式。不管是减少交互,还是避免重复的交互过程,实际上都是在改变模块A(客户)的交互,既然要改变模块A的交互,首先要知道模块A目前处理事件的关注点是什么。在过往的处理方式中,模块A的关注点在于完成事件需要哪些模块参与,怎么和这些模块交互,也就是模块A需要处理整个交互过程。既然交互和关注点有关,那么我们是否能从改变关注点入手来改变交互过程呢?该怎么改变关注点呢?

    怎么改变关注点?不妨把程序问题转换成生活问题,把模块A的处理方式转换成实际情况来思考。假设有一个技术团队,模块A是这个团队的负责人,其它各模块是团队中的开发人员,收藏事件是一次迭代开发,现在模块A接收到完成这次迭代开发的命令,所以开始组织开发人员参与开发,在开发过程中,模块A需要记录登录任务的开发状态,等待相应的人员完成开发,当登录任务完成后,模块A需要告诉收藏任务的相应人员开始开发,然后记录收藏任务的开发状态,当收藏任务开发完成时,模块A把这次迭代开发标记为已完成。

    假如你是模块A,你有什么感觉?如果我是模块A,我感觉腰腿酸痛、精神不振,好像身体被掏空了(把肾虚说得这么理直气壮😂)!完成一次迭代开发,我需要把相应任务分配给相应的开发人员,需要管理每一个开发任务,需要记录每一个任务的状态,还要通知和组织相应的开发人员完成开发。额!好吧,这些好像确实是负责人应该做的事,但我想偷懒,我希望有一个系统可以帮助我管理开发过程,我需要做的只是创建任务,设置任务之间的依赖关系,再把任务输入到系统即可,我不再需要去关注这个任务怎么完成,什么时候完成,什么时候下一个开始任务。

    既然有了方向,我们把问题转换回程序问题,如果程序中存在这么一个系统,这个系统管理着一系列的操作,那么对客户而言,需要做的只是创建操作,设置操作之间的依赖关系,把操作输入系统即可,客户不再需要去关注这个操作怎么完成,什么时候完成,什么时候开始下一个操作。那么这时模块A的关注点就被改变了,模块A不再关注完成事件需要哪些模块参与,怎么和这些模块交互,而是关注完成事件需要哪些操作,这些操作之间有什么依赖关系,但这些操作背后的一切对模块A来说都是透明的,也就是操作被自动化管理了。

    有需求才有产出,需求提供了方向。我们的需求是建立一个系统(组件或模块),这个系统用于管理一系列的操作,管理操作之间的关系,观察操作的状态,启动操作,也就是我们要建立一个自动化操作的管理系统。

    有了需求之后,我们要怎么着手搭建这个系统呢?应该怎么开始呢?最基本的,先分析需求,从需求中提取关键抽象,怎么提取关键抽象?从需求的关键词入手,当前需求中的关键词是操作和操作管理,所以当前的关键抽象就是操作和操作管理,接下来需要做的是确定这两个抽象的基础外部视图,什么是外部视图?也就是这个抽象的职责,属性和行为。

    我们的需求是要建立一个复杂的自动化操作的管理系统,但为什么得到的基础外部视图的内容却如此简单?这里需要说明一下,在前期分析和设计阶段,我们关注的是抽象与抽象之间的关系,所以只需要基础外部视图就已经够了,即使到了后期的阶段,也只需要一个相对完整的外部视图就,像抽象的内部视图(结构,具体实现等)是在实现阶段确定的,现阶段不需要也不应该去关注内部视图。

    回到我们的最初目标,减少模块A的交互,避免重复的交互过程。有了上面两个抽象的基础外部视图后,现在把登录和收藏这两个子过程抽象成两个操作对象,模块A设置这两个操作对象的依赖关系,传到操作管理对象即可,模块A不再需要关注有哪些模块参与,怎么和这些模块交互了。

    那模块B处理评论事件又变成怎样了?

    当交互过程抽象成操作对象后,模块A和模块B在交互中的if else也被封装到操作对象中了。例如,模块A在处理收藏事件时需要判断用户是否已判断,用户已登录则直接开始处理收藏,若用户未登录则先进行用户登录的处理,但现在这个判断的过程及处理对模块A已经是透明的了,OperationLogin对象会根据用户状态来处理这一个过理,当用户已登录,OperationLogin对象会直接标记为已完成,这时下一个操作对象就会开始完成其相应的事件,模块A和模块B都不需要再去处理这些判断。

    改善Alert用户体验

    App通常会以Alert的形式来提示你是否允许某些操作,例如App需要访问你的相册时,会弹出一个Alert提示你是否允许访问。但很多时候在我们还没操作或看清楚本次Alert时,下一个Alert又弹出了,甚至会突然弹出几个Alert,就像这样。

    虽然很多时候确实需要弹出多个Alert来提示用户,但这样的用户体验会很糟糕。这种用户体验更多地是跟产品设计相关的,但我们还是应该在程序中尽可能地去改善,至少让Alert弹的方式变得优雅一些。

    接连弹出Alert,等待用户处理完一个Alert再弹出下一个Alert。我们要怎么实现呢?

    方式一:

    一种实现方式是提供一个AlertManager抽象,然后通过hook UIAlertView的show方法,把当前需要显示的AlertView添加到AlertManager,再由AlertManager做串行显示的管理。AlertManager的职责是管理一个个AlertView,通过一个串行队列来管理这些AlertView的显示。

    这种实现方式的优点在于,不需要改动程序原有的代码即可完成Alert事件的管理,但缺点也很明显,那就是无法应对API的更新,在iOS8之后,UIAlertView已经被UIAlertController所取代了。如果程序当中出现了一个模块用UIAlertView,另一个模块用UIAlertController的情况,那只能又去hook UIViewController的presentViewController方法了,而且AlertManager还需要监听AlertView的状态,当AlertView被用户处理后,把队列中的下一个AlertView显示出来。

    方式二:

    提供一个AlertManager抽象,向AlertManager传递一个Alert事件所需的信息,由AlertManager创建与信息相应的AlertTask,通过一个串行队列来管理这些AlertTask,在AlertTask出队列时,创建AlertView并把信息显示出来。

    这种实现方式的优点是,客户只需要关注Alert的信息,不需要关注Alert的信息是怎么显示的,同时也解决了API更新的问题,缺点是AlertManager的管理机制只限于Alert事件,其它类型的事件也想实现一个接一个的方式,只能再提供一个不同类型但管理机制相同的抽象了,这样程序中会存在很多机制相同的抽象,而且会增加程序的复杂度,增加学习成本。跟方式一的AlertManager一样,需要监听AlertView的状态。

    方式三:

    把Alert事件抽象成Alert操作对象,设置Alert操作对象之间的依赖关系,由于先前我们已经完成了一个自动化操作的管理系统,所以只需要把Alert操作对象输入管理系统即可。

    这种实现方式的优点是,客户只需要传递Alert的信息给Alert操作对象,不需要关注Alert的信息是怎么显示的,同时也解决了API更新的问题,还可以通过设置Alert操作对象之间的依赖关系来控制Alert出现的顺序,也避免了程序中存在相同机制的情况,缺点是会增加程序的复杂度,增加学习成本。

    这里还有一个问题,Alert可能来自不同的模块,所以我们需要扩展原有的实现,使得管理系统可以控制某一类操作对象,让这些操作对象可以一个接一个地执行,具体的实现请看Demo和下面实现部份的讲述。

    优化启动流程

    main函数调用前

    iOS程序启动通常被分为main函数调用前后的两个过程,main函数调用前的过程大致是这样的。

    从创建进程到设置线程入口属于kernel内核范畴的工作,而加载依赖共享库到恢复App入口属于动态链接器dyld范畴的工作。main函数调用前的优化重点在于减少操作,例如减少程序中的类和分类,减少资源文件的加载,合并load方法,合并动态库等,减少这些操作从而减少前半部份所需要的时间。网上已经有大量关于前半部份优化的文章,所以这里不再提及(说得好像自己会一样😂)。

    main函数调用后

    main函数调用后的优化分为效率和流程两个方面,这一节主要讨论怎么优化启动流程。关于效率方面的优化,网上也有一些文章讲解,我感觉好像没什么可以讲的,毕竟每个项目的情况都不同,只可能提供一个大方向,各自根据实际情况来优化了,但会在讨论流程优化的时候穿插一些效率优化需要注意的点。

    main函数调用后的主要工作是加载程序的内容,所需要处理的任务可能有很多,而且每个程序的情况都有所不同,所以这里只分析一个简单的例子。

    程序启动时可能需要处理二三十个任务甚至更多,如果main函数调用后在App的主线程一次性处理所有的启动任务,那启动所需要的时间可能会很长,用户需要等待很长的时间才能看到App的内容。在讨论怎么优化之前,首先要知道当前的关键问题是什么,所以先来了解一下App启动时,主线程的执行过程是怎样的。

    站在用户的角度来看,使用App时最直接的交互就是可视化元素,所以App启动时,用户越快能看到App的可视化元素体验就越好。App的可视化元素要显示在屏幕上,则依赖渲染进程来渲染可视化元素,而渲染进程只有在接收到渲染事务后,才会把事务中的可视化元素渲染到屏幕上,所以App的可视化元素能否快速地显示在屏幕上,这取决于App能否及时地把渲染事务发送到渲染进程。

    由上图我们可以得知,App能否及时地把渲染事务发送到渲染进程取决于处理启动任务所需要的时间,即启动任务的处理时间越长,用户看到App的可视化元素所需要的时间就越长。知道问题所在之后,我们就可以来讨论优化的方案了。

    既然我们已经知道问题在于App的主线程压力过大,所以最简单的处理就是把不需要在主线程处理的任务放到辅助线程中处理,但这并不能保证主线程的压力会大大减少,因为仍然有一部份任务只能在主线程中处理的,像UI任务,所以我们还需要别的优化方案来解决这些只能在主线程中处理的启动任务。为了更容易讨论,现在我们假设例子中所有的启动任务都只能在主线程中处理。

    对于这些只能在主线程中处理的启动任务,通常的解决方法是把这些启动任务分优化级处理,在main函数调用后只处理最高优化级的任务,也就是程序启动所必需的任务,其它优先级的任务延迟处理。

    在高优先级的任务中,第一个被处理的任务一定是启动信息收集系统,原因也很简单,因为在处理后续的任务时可能会出错,信息收集系统可以第一时间把这些错误信息收集起来。加载启动页基本上就是最后一个任务了,因为启动页的内容通常是缓存在本地或者直接通过网络加载出来的。需要注意的是,我们应该尽可能在加载启动页前只处理跟启动页相关的任务,把其它启动任务放到加载启动页之后执行,让用户尽可能快地看到启动页,只要启动页成功加载到屏幕上,程序就会有1秒以上的时间去处理其它的任务。还需要注意的是,如果启动页中存在动画效果,应该尽可能地选择Core Animation作为动画引擎,而不是POP等动画引擎,想了解两者的差异可以看我之前写的《iOS动画的基础知识》

    加载内容页的任务并没有被划分为高优先级,主要是因为App的内容页通常都有大量的视图,创建这些视图需要消耗大量的时间,而且还需要同步地处理这些视图的布局,显示等,而这些都是递归操作,所以加载内容页的时间算起来就非常可观了。把启动任务划分为不同的优先级来处理后,App主线程的执行过程又是怎样?

    这种优化方式跟页面性能优化的原理类似,把一个大事务分割成若干个小事务,再把这些小事务分配到不同的Loop中处理。把启动任务划分为不同的优先级仅仅只是第一步,还需要一个类似于操作系统中调度器的管理系统来管理和调度这些不同优先级的启动任务。

    实际上即使没有这个管理系统也可以很容易地实现,启动任务按优先级分配到不同的Loop中处理。

    只需要简单地划分一下启动任务,把需要延迟处理的启动任务dispatch回MainQueue即可。

    把需要延迟处理的启动任务取出,同时向主线程发送消息,当主线程处理完高优先级的启动任务后,去检测消息列队中是否有消息要处理,检测到待处理消息后,开始处理延迟的启动任务。所以就目前的需求而言,启动任务的管理系统可以以很简单的方式便可实现。

    在前面的分析中,所有的启动任务都是在主线程中处理的,但无论何时,我们都希望App可以快速地响应用户的操作,所以一般情况下都会尽可能地减少主线程的工作量,使主线程可以快速地处理交互的消息。实际上我们往往会把那些不需要在主线程中处理的启动任务分配到辅助线程中处理,但在多线程环境下完成App的启动,就远比前面分析的过程复杂了。

    为了支持多线程环境,管理系统的内部实现至少会面临这么几个问题。管理系统的调度算法是怎样的,启动任务的处理顺序是否由优先级决定?如果不是由优先级决定,那么怎么保证任务可以正常地处理?例如,更新用户信息的任务比配置网络环境的任务先处理,由于当前还没有配置好网络环境,更新用户信息的任务就无法正确处理了。如果是由优先级决定,那么整个处理过程的效率就比较低了,因为低优先级的启动任务需要等高优先级的启动任务处理完毕业才开始处理。

    如果整个处理过程是串行的,那么这样的多线程处理就没有什么意义了。按优先级处理启动任务似乎是现在比较普遍的处理方式,一些开发书像《高性能iOS应用开发》也是这么介绍的。不知道应用这种方式的团队是怎么解决这些问题的,调度算法是否经常要改。

    我们尝试换一种方式来优化main函数调用后的流程。观察例子中的启动任务就会发现,这些启动任务之间存在着一定的关联,那么如果把这些启动任务抽象成相应的操作对象,再设置这些操作对象之间的依赖关系会怎样?

    设置了操作对象之间的依赖关系之后可以看到,整个启动流程的架构跟我们所期望的,或者在草稿本上画的相差无几,启动任务一个接一个地处理,不需要再去划分这些启动任务的优先级了。

    前后两种优化方式的某些关注点是一样的,都关注启动时有哪些任务,都把原本属于AppDelegate的交互抽象出来。但不同的是,现在我们不再需要去关注启动任务的优先级,或者说优先级的概念已经被淡化了,也没有了划分优先级处理后所需要考虑的问题,不再需要关注多线程环境下怎么保证启动任务的处理顺序了,因为当操作对象所依赖的其它对象处理完毕后,它会自动开始处理自己的事件,例如LaunchCache操作对象和Networkconfig操作对象依赖于Collection操作对象,当Collection操作对象处理完事件后,LaunchCache操作对象和Networkconfig操作对象就会自动开始处理任务。我们需要关注的只是当前有多少个操作,这些操作之间有什么依赖关系,哪些操作可以在多线程环境下处理即可,操作自动化运行。

    从执行效率来看,在确定启动流程的架构之后,可以尽可能地增加处理启动任务的并发数,所以CPU使用率的提升还是比较可观的。

    从开发效率来看,不需要去管理启动任务的优先级,不需要去考虑和优化调度启动任务的算法,只需要设置启动任务之间的关系即可,减少的工作量也比较可观。

    从后期维护来看,当启动任务增加,减少或需要重新调整启动任务之间的关系时,我们只需要去调整启动任务之间的依赖关系即可,不需要去考虑和优化调度算法,提高了稳定性。

    从测试来看,减少了测试项,简化了测试项,因为不需要去测试调度算法是否高效了,只需要把注意力放在操作对象的实现上即可。

    操作抽象实现

    操作抽象

    我们要怎么实现前面所提到的操作抽象呢?实际上Cocoa中已经存在这种带有状态的操作抽象了,NSOperation。看到这里你可能想说,早说NSOperation不就行了,扯这么多干嘛。前面的分析与设计是不针对任何具体实现的,纯粹用抽象概念来讨论的,现在到了实现阶段,才去考虑前面整个的设计要怎么实现,用什么来实现的。分析设计和实现这两个过程是不能反过来的,也就是不能因为想用NSOperation再去设计使用场景。例如,你要吃面,所以你需要一双筷子,而不是你有一双筷子,所以你要吃面。

    在考虑具体实现时,我们会找现有的框架或过往的实现中是否存在相同的对象模型,如果有现成的对象模型,那么会优先选择用现成的对象模型而不是做一个新的对象模型,这并不是为了偷懒,理由很简单,因为现成的对象模型是经过实践和测试的,是相对稳定的,没有一个对象模型是一创造出来就可以稳定应对所有场景的,都要经过长期地应用,维护和扩展才会变得相对稳定的,所以优先选择现在的对象模型,如果没有现成的对象模型才会去创造一个。

    NSOperation的内部实现

    看到NSOperation和NSOperationQueue,可能很多人的第一反应是多线程,NSOperation与NSOperationQueue结合实现了多线程技术,但多线程技术仅仅只是它们的一部份,而且在我看来,多线程技术并不是NSOperation和NSOperationQueue的核心。在iOS4以后,NSOperation和NSOperationQueue的多线程是用GCD来实现的,也就是NSOperation与NSOperationQueue作为GCD的面向对象的抽象物而存在。但如果你只是想以面向对象的方式来使用GCD,你自己实现Operation和OperationQueue的抽象,也不过四五百行代码,而且效率要比Cocoa自带的NSOperation和NSOperationQueue高,首先来了解一下NSOperation的内部实现。

    顾名思义,NSOperationInternal是NSOperation的内部抽象,向NSOperation对象发送的消息最终都会转发给内部的NSOperationInternal对象,NSOperationInternal对象完成具体的工作,也就是说我们平常所用的NSOperation实际上只是一个代理,此代理(Proxy)非彼代理(Delegate)。NSOperationInternal的内部结构大致是怎样的?

    NSOperationInternal内部结构中的queue是NSOperation被添加的queue,operation是外部的NSOperation对象,dependencies是NSOperation的关联对象,down_dependencies也就是NSOperation的反关联对象了,state是NSOperationInternal对象目前处理事件的状态,这跟NSOperation不同,NSOperation是用几个BOOL值(isFinished,isExecuting,isReady)来表示事件当前状态的。

    NSOperation被添加到NSOperationQueue的过程是怎样的?

    当NSOperation被添加到相应的NSOperationQueue时,NSOperation的NSOperationInternal就会把queue设置为当前的NSOperationQueue。

    设置NSOperation对象之间的依赖关系的过程又是怎样的?

    当设置NSOperation1与NSOperation2的依赖关系后,NSOperation2被添加到了NSOperationInternal1的dependencies中,NSOperationInternal1被添加到了NSOperationInternal2的down_dependencies中,然后NSOperationInternal1设置NSOperation1的isReady状态为NO。

    NSOperationInternal除了完成NSOperation的具体工作外,它还有一个职责就是监听NSOperation的状态(isFinished,isExecuting,isReady)。当NSOperation的状态改变时,NSOperationInternal会作出不同的响应。当NSOperation2处理完事件后,NSOperation1是如何得知且开始处理自己的事件?

    由于NSOperation被添加到NSOperationQueue时,NSOperation的NSOperationInternal会把queue设置为当前的NSOperationQueue,所以NSOperation的isReady状态从NO变成YES,即表示NSOperation可以处理事件,NSOperationInternal监听到NSOperation状态变化时,NSOperationInternal会向queue发送启动相应的NSOperation的消息。这就是NSOperation可以自动化的原因,要注意的是,NSOperation要跟NSOperationQueue结合才能实现自动化,如果只是设置了NSOperation之间的依赖关系,但没有把NSOperation添加到NSOperationQueue中,NSOperation是不会自动化的。

    NSOperationInternal通过监听NSOperation的状态来完成相应的操作,所以当我们自定义NSOperation的子类时需要手动管理NSOperation的isFinished,isExecuting状态。当我们自定义NSOperation的子类时,要么实现start方法,要么实现main方法,如果两个方法同时实现,那么只有start方法会被调用。

    为了实现NSOperation的自动化,Cocoa为NSOperation增加了NSOperationInternal,让NSOperationInternal在背后处理一切事务。如果NSOperation只用于多线程技术,那么它的状态机制就没有太多意义了。所以当你只是想以面向对象的方式来使用GCD,自己实现的效率会更高。

    操作管理抽象

    我们可以用NSOperation来实现操作抽象(Operation),那么操作管理抽象(OperationManager)又怎么来实现呢?实现操作管理抽象需要考虑哪些问题?

    实现操作管理抽象至少要考虑以下问题,是否为每一类操作抽象都创建一个相应的操作管理抽象?如果不为每一类操作抽象配置一个相应的操作管理抽象,那用什么来让每一类操作抽象保持独立性?NSOperation需要跟NSOperationQueue结合使用才能实现自动化,是否由操作管理抽象来创建和管理NSOperationQueue?不同类型的操作抽象是否可以共用同一个NSOperationQueue?是否存在全局的NSOperationQueue?

    操作抽象有什么独立性?类似Alert操作抽象需要一个接一个地处理,如果共用同一个操作管理抽象,那么这个操作管理抽象的实现就会存在许多的判断语句来区分不同类型的操作抽象,解决判断语句多的问题,可以选择为不同类型的操作抽象创建一个相应的配置抽象,通过配置抽象来提供独立性,但这样会使得操作管理抽象的实现越来越复杂,需要的抽象也越来越多,这对于以后的维护和扩展都不利,而且学习成本也大大地增加了。

    如果NSOperationQueue由客户创建,每一个要用操作抽象的客户都需要自己维护一个或多个NSOperationQueue,那么客户的职责就增加了,而我们的初衷是减少客户的交互。如果不同类型的操作抽象不能共用同一个NSOperationQueue,那么就需要为每一类操作抽象都创建并维护一个NSOperationQueue。

    面对这些问题,我们先做出一些假设,尝试基于这些假设来实现操作管理抽象。每一类操作抽象都有一个相应的操作管理抽象,由操作管理抽象来创建和管理NSOperationQueue,不同类型的操作抽象可以共用同一个NSOperationQueue,存在全局的NSOperationQueue。

    考虑一下操作管理抽象的基本实现,基本实现就是创建NSOperationQueue,根据实际需要对每一类操作抽象做额外的处理,把操作抽象添加到NSOperationQueue中。现在对基本实现做一般化的抽象,创建一个高层抽象,在高层抽象定义一个基本实现的模板方法,由具体的操作管理抽象实现模板方法中相应的方法。

    现在还有两个需求要解决,不同类型的操作抽象可以共用同一个NSOperationQueue,存在全局的NSOperationQueue。可以为高层抽象(OperationManager)的模板方法中的子方法提供默认实际来解决这两个需求,也就是在高层抽象中提供全局公共的NSOperationQueue。这样,具体的操作管理抽象可以根据自己的需要选择实现或不实现创建NSOperationQueue的方法,还可以对相应的操作抽象做一些额外的处理。

    基于前面的假设确定了基本设计,现在来确定实现基本设计的语言,不同的语言会有不同的特性,在最终实现时,可以根据语言特性来做一些特殊的处理,或者选择更容易实现设计的语言来实现。现在我们需要为每一类操作抽象都创建一个相应的操作管理抽象,这种一对一的关系是否很像Objective-C中的类对象和实例对象?在Objective-C的世界中,所有的东西都是对象,包括类,Objective-C中的类对象主要有两个职责,一是提供创建实例用的原型模板,二是用于消息机制。用类对象来充当设计中的操作管理抽象,这样在编写代码的时候,不再需要引入新的类型,在运行时也可以减少所需要的内存,减少程序中的抽象。

    首先对NSOperation类对象做职责扩展,为NSOperation添加一个分类,分类中定义一个模板方法。

    NSOperation的子类可以实现模板方法中的两个子方法,第一个子方法是一个hook,让子类有权决定是否继续执行模板方法,如果子类想完全接手操作对象的管理,只要实现方法并返回YES即可。提供公共的NSOperationQueue,同时也留出扩展的空间让子类创建和管理自己的NSOperationQueue。

    我们还需要有一个对象来控制线程安全,把操作对象安全地分配到操作管理对象中。对NSOperationQueue类对象做职责扩展,让NSOperationQueue的类对象来完成这些操作,选择NSOperationQueue类对象的主要原因是,减少程序中的抽象,而且平时的习惯也是创建操作对象,再把操作对象添加到NSOperationQueue中。

    客户只需要关注完成某一事件需要什么操作,这些操作怎么组合,不需要关注这些操作的实现,也不需要关注这些操作是否并发,这些对客户都是透明的。具体的实现请看Demo

    相关文章

      网友评论

      本文标题:操作抽象设计-实践

      本文链接:https://www.haomeiwen.com/subject/fjfopxtx.html