有始有终,设计一个结构合理的下载模块
完成开发任务的同时,我们总希望自己能够交付高质量的代码。代码质量的测度有很多方法,可扩展性、可复用性是其中的两项指标。设计模式的理论能够非常有效地指导代码设计,但是光谈这些理论是非常抽象的,本文针对下载这个场景,结合设计模式的一些理论,谈一谈如何设计一个结构较为合理的下载模块。
Step.1 别急,先进行”需求评审“
在着手编码之前,先明确功能需求、技术需求,然后进行初步的思考。
从目标出发
从目标出发,能够帮助明确设计过程中的侧重点。对于下载这个场景,很直观可以想到,它涉及到的文件操作、持久化存储等步骤是会频繁出现在一个项目中的。所以我会希望为下载模块写的大量代码能够被良好复用。同时可以预见,下载这一场景是非常容易出现后续需求变更或者增加的,没准今天只下载视频,明天又需要添加对音频、对 zip 文件的支持;对于数据库存储框架,可能目前在使用 FMDB
,后续又要更换为 WCDB
。所以,也对这个模块的可扩展性、易修改性提出了要求。
结合一点点理论
设计模式中有几大原则,刚开始接触我们总感到难以把握。因为它们简短得像几字真言,而实际的场景却有千千万万种。那么,就从最易理解的**“单一职责原则”开始。简单来说,一个单独的模块应该只负责一个单独的任务,任务的粒度越细,它和其他模块的耦合性越低,它也越容易被复用。而遵循“依赖倒置原则”,则会有效提高代码的易修改性。比如对于数据库模块,在实际使用某一数据库框架进行存取操作的实现类之上,再抽象出一层接口类。在下载过程中只使用接口类中提供的方法,而接口类中方法的具体实现,则由下层的实现类完成。这样,当我们把数据库框架由 FMDB
替换为 WCDB
时,只需对实现类的代码进行修改,修改的目标则是使用新框架再次实现接口类中声明的方法,这也就是所谓的“针对接口编程”,而非”针对实现编程“。它带来的好处是显而易见的:在数据库框架的替换过程中,最上层的业务代码完全无需改动**,只需对数据库操作的实现类进行修改即可。
模块化的目的
有一件事是需要明确的,我们常谈的“模块化”,并非对所有模块都追求任意场景下的可复用。因为模块会分为业务模块和通用模块,通用模块力求做到任意场景下的可复用,而业务模块则专注于完成某一需求场景。虽然“下载”这个词在很多项目中会出现,但不同的项目中对它的定义是不同的。有的“下载”仅仅意指下载单个的文件,而有的下载则指的是某一场景下所有内容的本地缓存。
在这篇文章中,我预设的场景是一个下载任务中会包括各种具体的子任务,举个例子,一个下载任务可能由三个视频文件、两个音频文件、三张图片、两个网络请求的 JSON 格式结果组成。
因此,我会把本文所说的“下载”归入业务模块,它不追求做到任意场景下的可复用,但它能够很好地完成这个较复杂场景下的下载任务。而这个业务模块中所包含的文件下载、图片缓存、文件操作等具体步骤,其实是无关业务的,那么它们便可以归为通用模块。在其他进行图片缓存的场景下,可以使用这里的图片缓存模块,而其他的文件操作场景,也可以使用这里的文件操作模块。它们的具体分析会在下文展开。
Step.2 给出设计方案
结合文章第一部分的分析,着手进行方案的设计。
“下载”不是单单一件事
通常意义上的下载,是指将云端的资源获取到本地磁盘的过程。对于 iOS 应用,下载的目的多是进行某些内容的离线展示。一个完整的下载过程,应该由以下的步骤组成:
文件操作
对于所下载的文件,需要确定它在本地的存储路径;给定某个 key 值,需要获取对应文件的存储路径;对于某个指定的路径,会有检查文件存在性、完整性等操作;下载过程中不断进行文件写入,删除已下载内容时涉及文件删除、目录删除;除此之外,还有获取各个系统目录、获取磁盘空间数据等常规操作。若涉及安全性需求,还会有文件加密、解密操作。因此,将文件操作封装为一个单独模块是一个明智的选择。文件操作不仅仅会在下载这个场景中出现,因此,在这个模块的实现过程中应该尽量剥离业务相关的内容,力求成为一个通用的工具模块。
数据库操作
基于文章第一部分中给出的场景,这里的下载任务应该是结构化的数据。无论网络状况是否正常,已下载的内容都能够正常展示,所以下载记录应该被持久化存储。基于以上两点,数据库的使用是自然的选择。应该明确的是,数据库存储的是下载任务记录,或叫做日志,而非下载的文件。考虑到 iOS 中数据库框架的多样性和业务方对数据库性能的持续追求,很容易预见到数据库框架在未来的替换工作。因此对于这个模块,上文也进行了分析,那就是依照依赖倒置原则,分成抽象的接口类和具体的实现类。
较大体积文件的下载
在下载的需求中,视频、音频、zip 文件等体积较大的文件是很常见的。因此一个只针对较大体积文件的下载模块模块必不可少。它不涉及任何具体的业务细节,它的任务仅仅是根据给定的文件 url 和本地存储的路径,完成该文件的下载。做到这个模块的高内聚是比较容易的,因此强烈建议将这部分封装为一个通用模块,以满足任何场景下的文件下载需求。为减少通用模块之间的横向依赖,一个思路是本地路径由上层的业务模块调用文件操作模块获得,然后传递给本模块,而非本模块直接调用文件操作模块;对于文件写入操作,可直接使用系统的 NSFileManager
。同时也有另一种思路,大文件下载和文件操作之间的依赖是自然、可接受的,允许下载模块依赖文件操作模块。这些没有标准答案,可以自行取舍。
图片的下载
有时候下载任务中会包含图片下载,按照体积来看,将图片下载归入文件类型也不为过。但是图片的缓存在iOS的开发中是一个积淀已深的话题,我们拥有 YYWebImage
、SDWebImage
等优秀的图片缓存框架,有什么理由再去重复造一个性能未必更优的轮子呢?除此之外,刚刚提到的两个图片框架基本应用在了绝大多数的iOS网络应用中,所以很有可能出现的场景是:已经下载过的图片,在项目中的某处不相关的地方用上述图片框架进行加载。如果图片下载使用这些框架的缓存器来实现,那么在上述场景下,**框架会从本地缓存中寻找到目标图片,避免重复的云端下载,达到了有效且明显的优化效果。基于局部性原理,这种情景的命中率还是不可忽略的。**因此,建议将图片的下载拆分为一个内部实现使用上述框架的图片缓存器。
网络请求结果的缓存
有的下载场景中,需要对网络请求进行缓存。网络请求的结果多为 JSON 格式的数据,体积较小,属于轻量的下载内容。我的实现是网络请求缓存和图片缓存作为 cache 模块的一部分,整体封装一个 cache 模块。也可以将这两者分开模块化,视具体业务需求灵活决定。
特定场景下载的业务模块
以上列出的模块,基本都可以向可广泛复用的通用模块努力。上文提到,模块化中,也包括专注具体场景的业务模块。在本文的业务场景下,我封装了一个业务模块。它的职责是:持久化维护已下载和正在下载任务的list;根据按固定格式提交的下载任务,解析出结构化的任务结构;对于不同类型的子任务,使用上述对应的通用模块完成下载;同时负责协调各子任务之间的同步关系;在所有子任务完成下载后,检查整个结构的文件完整性;通过完整性校验后,进行数据库存储操作,存储该次下载日志;在整个活动周期内,模块还负责下载任务状态的更新。
模块整体结构
通过对整个下载过程的分析,我们拆分出了几个模块。依照单一职责原则,将每个模块的职责划分到了较为合适的粒度,都能够做到一定程度上的复用。对于其中扩展可能较高的模块,依照依赖倒置原则,抽象出了一层接口类,避免了未来底层修改时对上层业务代码的影响。在模块化的应用上,也做到了目的明确、合理拆分。
下图即是整体的示意图:
Step.3 完成具体实现吧!
其实写完第二部分,本文的写作目的已经差不多达到。大家从标题可以感受到,本文侧重点在于对”下载“这个场景运用一些理论的指导进行较为合理的代码结构设计。不过为做到有始有终——“从理论分析开始,用具体实现来结尾”,这部分对实现细节进行一些讨论,提供一些“干货”,这些方案面对不同场景会有不同的优劣表现,仅供参考。
文件操作模块
这部分我的实现是使用系统的 NSFileManager
进行文件存在性判断等基本操作。对于本地存储的目标路径,生成规则为文件 URL 做 md5 操作,再添加具体的文件类型后缀。在安全性较高的场景中,所下载的文件都来自自有的服务器,那么文件正确性校验可以由后端提供部分支持,如对于每个文件都返回特定的校验值,在本地下载完成后,使用由已下载文件生成的校验值和后端提供的进行比对。
数据库模块
对于数据库中需要存储什么字段,我的意见是这样的:对于某个具体的文件,存储初始 url、文件在本地存储的路径、文件大小、更新时间等基本信息。对于结构化的整条下载记录,则将还原初始下载任务的所需字段都进行存储。具体解释下,初始下载任务的提交时多是使用业务方的数据类型,比如一篇微博展示时的 model ,一篇文章展示时的 model。而下载任务提交到下载模块后,我们会将初始的数据类型转化为下载模块的规定的数据格式。若涉及到断点续传等场景,便会存在 app 重启后,由从数据库中取得的下载模块所用数据格式向初始业务方数据格式的逆转化,这时就需要初始任务所有必要的状态信息,从而进行现场恢复,继续进行下载。
上文说到,下载管理业务模块需要维护下载中、已下载任务的 list,用什么来区分状态呢?我的实现是为下载记录添加标识是否完成的字段,这样当 app 重启后,从数据库中取得所有的下载记录,若某条记录被标识为未完成,那么它便是需要还原为初始下载任务的记录,被归入下载中 list。
大体积文件下载模块
关于这部分的讨论已经有很多,本文不再赘述。值得一提的是,这个通用组件依然会面临底层实现更换或者版本升级的问题,所以依照依赖倒置抽象出接口层的思路在这里依然适用。
缓存模块
关于图片的缓存在上文已经详细讨论。对于 JSON 格式的网络请求结果,iOS 中一般使用 NSDictionary
存储,它支持 NSCoding
协议,因此 YYCache
、EGOCache
等缓存框架都是可以使用的。这部分的接口设计比较直白,为指定 key 对应的值进行缓存,根据给定 key 返回对应的缓存值,以及移除给定 key 对应的内容。抽象接口层的思路,照例适用。
下载管理业务模块
在项目的很多地方可能都需要获知当前下载模块的状态,所以这里使用单例实现是一个比较好的选择。在整个下载过程的最初,它根据提交的每一个初始任务数据,解析出具体的子任务类型,调用对应的子模块完成子任务的下载。同一下载任务下的各子任务之间应该是异步的,所以 dispatch group
是一个直观的选择。顺序提交的所有初始任务之间,则是同步的关系,这里可以使用类似队列的结构来管理。下面给出一个示意图:
对于下载中、已下载这两种状态的区分,这里提供一个改进思路:在某个初始任务真正开始下载之前,就向数据库中插入一条新的下载记录,设置状态字段为未完成,当所有子任务均完成且通过完整性校验后,更新状态字段为完成。
最后,为大家提供一个业务模块的样例伪代码,用以展示整个下载流程。
//下载管理业务模块的接口列表(大意展示)//业务方的model
@class OriginModel;@interface DownloadManager : NSObject
//获取下载管理对象(单例)
+ (instancetype)sharedInstance;
//获取下载中的任务
- (NSArray<OriginModel *> *)getDownloadingItems;
//获取已下载的任务
- (NSArray<OriginModel*> *)getDownloadedItems;
//根据id获取已下载的item
- (OriginModel *)getDownloadedItemById:(id<NSCopying>)itemId;
//是否下载过指定id的item
- (BOOL)didDownloadedItem:(id<NSCopying>)itemId;
//批量下载
- (void)downloadItems:(NSArray<OriginModel*> *)items;
//暂停下载
- (void)pauseDownloadForItem:(id<NSCopying>)itemId;
//恢复下载
- (void)resumeDownloadForItem:(id<NSCopying>)itemId;
//取消下载
- (void)cancelDownloadForItem:(id<NSCopying>)itemId;
@end
复制代码
//下载管理业务模块的主要实现@implementation DownloadManager- (void)downloadItems:(NSArray<OriginModel *> *)items {// 解析任务结构,将所有任务push进任务队列MissionStruct *oneStruct = [self analyzeMission];for (MissionItem *item in oneStruct) {[self.missionList pushItem:item];}...
// 若非空,从任务队列中取出任务元素if (![self.missionList isEmpty]) {MissionItem *oneMission = [self.missionList pop];[self handleMission:oneMission];}
}- (void)handleMission:(MissionItem *)mission {// 调用数据库模块,插入一条新纪录[DatabaseManager insertMission:mission];dispatch_group_t downloadGroup;// 下载视频for (videoMission in mission.videos) {dispatch_group_enter(downloadGroup);// 调用文件管理模块,获取该url对应的文件路径targetPath = [FileManager pathForURL:videoMission.url];// 调用大文件下载模块,下载该视频[FileDownloadManager downloadFile:videoMission.urltargetPath:targetPathsuccess:^(){dispatch_group_leave(downloadGroup);}];}// 下载音频for (audioMission in mission.audios) {dispatch_group_enter(downloadGroup);// 调用文件管理模块,获取该url对应的文件路径targetPath = [FileManager pathForURL:audioMission.url];// 调用大文件下载模块,下载该音频[FileDownloadManager downloadFile:audioMission.urltargetPath:targetPathsuccess:^(){dispatch_group_leave(downloadGroup);}];}// 缓存图片for (imageMission in mission.images) {dispatch_group_enter(downloadGroup);// 调用图片缓存模块,缓存该图片[ImageCacheManager cacheImage:imageMission.urlsuccess:^(){dispatch_group_leave(downloadGroup);}];}// 缓存网络请求for (contentMission in mission.contents) {dispatch_group_enter(downloadGroup);// 调用网络请求缓存模块,缓存该网络请求[RequestCacheManager cacheRequest:contentMission.urlsuccess:^(){dispatch_group_leave(downloadGroup);}];}...// 所有子任务均完成dispatch_group_notify(downloadGroup, dispatch_get_global_queue(0, 0), ^{// 通过完整性校验if ([self verifyAllSubMission:mission]) {// 调用数据库模块,更新该下载纪录[DatabaseManager updateMission:mission];} else {// 未通过完整性校验,移除数据库对应记录[DatabaseManager removeMission:mission];}});
}@end
复制代码
知识小集是一个团队公众号,主要定位在移动开发领域,分享移动开发技术,包括 iOS、Android、小程序、移动前端、React Native、weex 等。每周都会有 原创 文章分享,我们的文章都会在公众号首发。欢迎关注查看更多内容。
有始有终,设计一个结构合理的下载模块相关推荐
- 现要为某一个销售部门编写一个程序管理约100种商品。要求设计一个结构体类型来描述商品,每种商品包括商品编号(如A001)、商品名称、商品销售量和商品销售额等信息,并编写以下函数···········
原题:现要为某一个销售部门编写一个程序管理约100种商品.要求设计一个结构体类型来描述商品,每种商品包括商品编号(如A001).商品名称.商品销售量和商品销售额等信息,并编写以下函数: 1.编写一个函 ...
- 如何设计一个结构合理的java项目
1.前言 最近写一个Java处理工具,是一个springboot的非web项目,正好借这个机会总结一下自己的经验,当开发一个Java应用时,应该全局考虑哪些方面,包括如何划分功能包,如果建立对象关联, ...
- 设计一个简单的UI框架,实现不同模块之间相互转换,使用单例实现。
实现不同模块或窗口的互相切换,其实用一些代码都可以实现,但是使用UI框架不仅方便后续修改添加删除,同时在做出扩展效果时,也可以快速实现.由于我还是学生,这些搭建UI的材料都是以前玩的一些小游戏里面,比 ...
- failed to open log file_C++中glog源码剖析以及如何设计一个高效 log模块
每个开发者编程中都会记录log信息,多数人都会使用log第三方库,log库使用起来很方便,但我们也需要了解log系统的原理,这里以glog为例进行分析. 开始 这里不会介绍glog中是如何控制INFO ...
- 一个七年的Java程序员从业总结:比起秃头,我更怕数据库底层设计原理结构
前言 说到数据库这个词,我只能用爱恨交加这个词来形容它. 两年前在自己还单纯懵懂的时候进了数据库的课堂,听完数据库的课,觉得这是一门再简单不过的课程,任何一门编程语言都比SQL要晦涩难懂,任何一门理论 ...
- 考研数据结构之队列(3.3)——练习题之设计一个循环队列,用front和rear分别作为队头和队尾指针,另外用一个标志tag表示队列是空还是不空来设计队列的结构和相关基本运算算法(C表示)
题目 设计一个循环队列,用front和rear分别作为队头和队尾指针,另外用一个标志tag表示队列是空还是不空,约定当tag为0时队空,当tag为1时队不空,这样就可以用front==rear作为队满 ...
- 根据伪代码画出流程图和盒图以及根据流程图判断是否为结构化流程图,并且为其设计一个等价结构化程序。
一.首先附上作业图: 二.解决实例 1.画出程序流程图和盒图. 流程图 ...
- Python:PyQt5设计一个文本编辑器窗体程序(附UI窗体和图片素材下载)
hello,大家好,我是wangzirui32,今天我们来学习如何用PyQt5设计一个文本编辑器窗体,开始学习吧! 文章目录 1. UI窗体设计 2. 编写代码 2.1 pyuic生成代码 2.2 修 ...
- 【openai】请帮我设计一个通用的ERP管理系统,涉及到的表结构用mysql语言表达出来,全部写出来
背景 这周末把openAi集成到自己的web系统里面了 尝试提问了几个技术和日常问题,感觉回答的还不错 问题1:[请帮我设计一个通用的ERP管理系统,涉及到的表结构用mysql语言表达出来,全部写出来 ...
最新文章
- HttpServletRequestWrapper的使用
- django批量修改table_Django 数据库表多对多的创建和增删改查
- navicat 或者workbench 无法连接127.0.0.1(61)的解决方法
- gerber文件怎么导贴片坐标_利用Gerber文件生成贴片坐标及元件位置图的方法技巧...
- dax 筛选 包含某个字_DAX分享9:DAX中用变量来计算动态filter context中数值
- 仅用 2 年过渡到自研 ARM 芯片,苹果的底气从何而来?
- 让你python代码更快的3个小技巧
- 在填写表单中输入全角数字的解决方案
- Spring-MVC案例:Spitter的笔记
- hdu 1026【Ignatius and the Princess I】
- html词云图生成,图悦在线词云图制作工具
- AT89C51中断模板(宏定义)
- 高一计算机函数公式,求高一数学函数所有公式
- vue实现中英文网站配置
- Java开发工程师面试三分钟自我介绍
- 002.西门子M440变频器端子控制正反转
- T-SQL程序练习03
- 【老罗笔记】一万小时天才理论
- oracle crs 4563,启动CRS及instance!
- 获取固有节假日的时间戳数组 (美国节假日)
热门文章
- pjax 历史管理 jQuery.History.js
- Console.WriteLine()与MessageBox.Show()的区别
- Java中的移位操作以及基本数据类型转换成字节数组【收集】
- PO BO VO DTO POJO DAO概念及其作用(附转换图)
- python version 3.4 required_Python version 3.3 required, which was not found in the registry
- nodejs-模块系统
- POJ3692 最大点权独立集元素个数
- 【C 语言】数据类型本质 ( sizeof 函数 | 数据类型大小 )
- 【Android 逆向】代码调试器开发 ( 代码调试器功能简介 | 设置断点 | 读写内存 | 读写寄存器 | 恢复运行 | Attach 进程 )
- 【错误记录】反射内部类报错 ( Android 使用 Hook 时反射内部类报错 )