简单App的开发通常不需要考虑数据层的问题,或者说考虑得可以比较粗糙。比如一个展示产品信息的页面,通常我们认为只需要在网络正常的情况使用,可以认为是一个非常接近于B-S模式的C-S模式。
而随着App功能的逐渐迭代进化,以及产品方对于产品的思考逐渐深入,数据维护需求迟早会提上议事日程。与其一开始草草地写一个App交差,还不如一开始就考虑清楚如何设计好一个扩展能力强,又不至于功能过剩的架构。
A. 网络数据
网络数据是App最重要的数据来源。现在App开发者似乎已经可以不动脑筋地一上来就拖进AFNetworking之类的功能库,因为这类功能库俨然成为业界的新标准。其实使用这种库我是百分百得赞成,因为前人已经造好了轮子,除非你真有更多闲余时间,或者你老板觉得研究功能模块也会给你发工资,那你可以尝试全部采用自写模块。
首先思考使用AFNetworking给你带来了什么益处。首先你不需要再关注HTTP的更多细节,包括数据缓存,https认证,摘要认证,业务上你不用考虑请求的调度问题,额外的你可能还能获得数据序列化的便利性。这些可能是你当下没有想到,但未来App中一定会需要用到的功能。
然后我们来思考我们需要注意什么。
- 你的App和服务器的通讯数据。
典型的用JSON。如果你的App用了XML等目前非主流的传输协议,而且短期内没有精力改,那也没关系。但是劝你一定要遵守RESTful的API的设计规范。
很多程序员以为数据传输这种事不就是从服务器拉一个东西来,怎么传都行,并没什么太大区别。假如你的App只准备维护一年,那是没太大区别。这里举个简单例子来说明乱定义API的危害:
例子1: 我们App要获得一个首页数据,最初程序员随便取了个接口名叫
http://api.myapp.com/getData
,然后第二个页面出现了,是个人信息页,后台程序员说,那还取这个名字吧,传递的参数换下就行。更妙的是,参数还是用POST方式传输的
http://api.myapp.com/getData
POST data: apiName=myProfile
如果产品哪一天说你的接口数据太大,要做接口缓存,而好死不死的你的参数在POST里面,那很遗憾,HTTP的缓存就不要想了。你需要自己实现一套类似HTTP的ETag或者Modified-Since逻辑,还不能保证比AFNetworking写的好,这不是给自己找事吗?
例子2: 很多开发人员写JSON极其随意,常见如:
{"data": {
"imgurl": "http://api.myapp.com/link",
"title": "My List",
}}
请问这个data是什么,对于服务器和客户端来说,对应任何一个实际的数据结构吗?哪怕想把几个不相干的数据结构打包成一个新节点,也请取一个确定的名字,data这种名字到最后就是客户端根本不能预知到内部的数据类型,也许还得加一个type来区别,那写个data只是为了给节点凑个名字吗?
更常见的是一物多名,比如一个用户信息,一会叫userProfile。一会叫userInfo。这种设计最后的结果必然导致开发人员不看文档根本不能了解接口的含义,而遗憾的是没有文档又是很多开发组的日常状态。
B 数据模型
客户端网络数据通常会被转换成数据结构,供各业务层使用。设计数据模型很多人觉得是个简单体力活,其实也不尽然。我觉得最理想的数据模型,应当由客户端和服务器开发人员协商后确定,切勿由一端主导。
还是以用户信息为例:产品初期我们可能设想的用户信息也就没几样:
@interface UserModel : NSObject
@property (nonatomic, strong) NSString *userId;
@property (nonatomic, strong) NSString *userName;
@property (nonatomic, strong) NSString *desc;
@property (nonatomic, strong) NSString *iconUrl;
@end
先还是想想userId的数据类型吧。不要服务器定了个长整型结果客户端是个字符,后患无穷!
随着业务的复杂化,UserModel东西越来越多了!也许该包含子对象了。
@interface UserModel : NSObject
@property (nonatomic, strong) NSString *userId;
@property (nonatomic, strong) NSString *userName;
@property (nonatomic, strong) NSString *desc;
@property (nonatomic, strong) NSString *iconUrl;
@property (nonatomic, strong) UserLevel *userLevel;
@property (nonatomic, strong) UserProfile *userProfile;
@property (nonatomic, strong) Relation *userRelation;
@end
什么时候可以增加子对象了?其实并不难确定:
- 服务端数据表发现用一张表存储用户信息已经不合适,需要分表时;或者数据来源并不相同时
- 子对象有独立使用的应用场景时
第一点其实很好判断。Relation这个结构假如包含的是这个用户和接口中“我”的关系信息,对客户端来说绝不可能是和其他这个用户固有属性是在一个表内,或者说产生数据逻辑差异很大。这样还是建议客户端将这块数据独立命名。当然,在网络传输时也应当独立开来。
tips:给每一个节点增加一种“未获得”状态。
这个技巧的好处是,哪怕来自服务器的JSON数据是不完整的,你也能轻易的知道究竟是数据缺失,还是数据本身是个空。对于普通的页面也许并无明显价值,可如果信息是来自多个接口呢?比如从接口1获得了UserModel的一部分数据,从接口2又补充了一部分,最终形成了完整的UserModel,对于有缓存需求的App这是一种非常理想的节约流量的模式。
数据模型的建立,比较理想的是采用JSON->Model的工具,如Mantle,JsonModel等,这类工具其实并不难写,但还是建议用一些较成熟的工具。
B. 表现层模型
这个非常好理解,MVVM开发模型中View Model这一层。我还是强烈建议表现层还是要尽量接近数据层模型,最好是它的超集。切记不要另外定义一个类似UserModel2,最糟糕的一种是里面还都是Dictionary,看似一个万能结构,后人维护的时候会有多恐怖!
C. 客户端持久化
除了上面说到的网络层缓存,还有一种App常用的是和业务相关的持久化。比如,启动的时候想快速展示之前关闭的页面;把音乐,书籍,图片等保存到客户端等。这些业务相关的持久化数据需要非常谨慎的设计。
我们通常对App的持久化问几个问题,来推断其类型:
- 保存的数据量多大?k级别还是m级别
- 是否需要本地索引
- 数据类型,二进制还是文本?
- 读写的频率多大
第一个问题涉及到存储的方式,显然m级别的数据不适合直接在数据库存储。第二个问题是是否使用数据库存储的关键,而且提出这个问题不能基于当下,要为未来预留空间。第三个问题在已经确定使用数据库存储时,需要考虑是直接存入数据库,还是采用文件+数据库索引的模式。第四个问题,如果高频率的读写信息,信息量又不小,数据库不不堪重负,必须事先考虑增加内存缓冲层。
看过不少App,什么数据都往NSUserDefaults里面塞。也只能说我们走大运,苹果为我们做的好,起码NSUserDefaults的效率真是理想,如果同样的粗野作风去操作sqlite,一定会让你想死。
再谈谈数据模型与持久化模型的统一。用过CoreData的童鞋会明白,要存储数据,需要建立NSManagedObject模型。已经有的数据Model并不能直接转换成NSManagedObject的子类,要存储一种类型要建立两次模型,活活累死人的节奏。
Realm提供了RLMObject对象,同时支持JSON的直接转换,似乎比JSONModel或者MTLModel更富有天然的优势。但RLMObject仍然有很多不完善的功能。而且Realm的不开源也让人不太放心。
最理想的方式是,表现层可以不关注数据的来源(网络来的?客户端数据库?),可以直接使用的Model结构。数据逻辑有Model层自己维护,包括并不限于支持自动更新,差量更新,懒加载技术等等。