谈谈 MVX 中的 Model


常见的 Model 层

在大多数 iOS 的项目中,Model 层只是一个单纯的数据结构,你可以看到的绝大部分模型都是这样的:

Swift

struct User {enum Gender: String {case male = "male"case female = "female"}let name: Stringlet email: Stringlet age: Intlet gender: Gender
}

模型起到了定义一堆『坑』的作用,只是一个简单的模板,并没有参与到实际的业务逻辑,只是在模型层进行了一层抽象,将服务端发回的 JSON 或者说 Dictionary 对象中的字段一一取出并装填到预先定义好的模型中。

我们可以将这种模型层中提供的对象理解为『即开即用』的 Dictionary 实例;在使用时,可以直接从模型中取出属性,省去了从 Dictionary 中抽出属性以及验证是否合法的过程。

Swift

let user = User...nameLabel.text = user.name
emailLabel.text = user.email
ageLabel.text = "\(user.age)"
genderLabel.text = user.gender.rawValue

JSON -> Model

使用 Swift 将 Dictionary 转换成模型,在笔者看来其实是一件比较麻烦的事情,主要原因是 Swift 作为一个号称类型安全的语言,有着使用体验非常差的 Optional 特性,从 Dictionary 中取出的值都是不一定存在的,所以如果需要纯手写这个过程其实还是比较麻烦的。

Swift

extension User {init(json: [String: Any]) {let name = json["name"] as! Stringlet email = json["email"] as! Stringlet age = json["age"] as! Intlet gender = Gender(rawValue: json["gender"] as! String)!self.init(name: name, email: email, age: age, gender: gender)}
}

这里为 User 模型创建了一个 extension 并写了一个简单的模型转换的初始化方法,当我们从 JSON 对象中取值时,得到的都是 Optional 对象;而在大多数情况下,我们都没有办法直接对 Optional 对象进行操作,这就非常麻烦了。

麻烦的 Optional

在 Swift 中遇到无法立即使用的 Optional 对象时,我们可以会使用 ! 默认将字典中取出的值当作非 Optional 处理,但是如果服务端发回的数据为空,这里就会直接崩溃;当然,也可使用更加安全的 if let 对 Optional 对象进行解包(unwrap)。

Swift

extension User {init?(json: [String: Any]) {if let name = json["name"] as? String,let email = json["email"] as? String,let age = json["age"] as? Int,let genderString = json["gender"] as? String,let gender = Gender(rawValue: genderString) {self.init(name: name, email: email, age: age, gender: gender)}return nil}
}

上面的代码看起来非常的丑陋,而正是因为上面的情况在 Swift 中非常常见,所以社区在 Swift 2.0 中引入了 guard 关键字来优化代码的结构。

Swift

extension User {init?(json: [String: Any]) {guard let name = json["name"] as? String,let email = json["email"] as? String,let age = json["age"] as? Int,let genderString = json["gender"] as? String,let gender = Gender(rawValue: genderString) else {return nil}self.init(name: name, email: email, age: age, gender: gender)}
}

不过,上面的代码在笔者看来,并没有什么本质的区别,不过使用 guard 对错误的情况进行提前返回确实是一个非常好的编程习惯。

不关心空值的 OC

为什么 Objective-C 中没有这种问题呢?主要原因是在 OC 中所有的对象其实都是 Optional 的,我们也并不在乎对象是否为空,因为在 OC 中向 nil 对象发送消息并不会造成崩溃,Objective-C 运行时仍然会返回 nil 对象。

这虽然在一些情况下会造成一些问题,比如,当 nil 导致程序发生崩溃时,比较难找到程序中 nil 出现的原始位置,但是却保证了程序的灵活性,笔者更倾向于 Objective-C 中的做法,不过这也就见仁见智了。

OC 作为动态语言,这种设计思路其实还是非常优秀的,它避免了大量由于对象不存在导致无法完成方法调用造成的崩溃;同时,作为开发者,我们往往都不需要考虑 nil 的存在,所以使用 OC 时写出的模型转换的代码都相对好看很多。

Objective-C

// User.h
typedef NS_ENUM(NSUInteger, Gender) {Male = 0,Female = 1,
};@interface User: NSObject@property (nonatomic, strong) NSString *email;
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) NSUInteger age;
@property (nonatomic, assign) Gender gender;@end// User.m
@implementation User- (instancetype)initWithJSON:(NSDictionary *)json {if (self = [super init]) {self.email = json[@"email"];self.name = json[@"name"];self.age = [json[@"age"] integerValue];self.gender = [json[@"gender"] integerValue];}return self;
}@end

当然,在 OC 中也有很多优秀的 JSON 转模型的框架,如果我们使用 YYModel 这种开源框架,其实只需要写一个 User 类的定义就可以获得 -yy_modelWithJSON: 等方法来初始化 User 对象:

Objective-C

User *user = [User yy_modelWithJSON:json];

而这也是通过 Objective-C 强大的运行时特性做到的。

除了 YYModel,我们也可以使用 Mantle 等框架在 OC 中解决 JSON 到模型的转换的问题。

元编程能力

从上面的代码,我们可以看出:Objective-C 和 Swift 对于相同功能的处理,却有较大差别的实现。这种情况的出现主要原因是语言的设计思路导致的;Swift 一直鼓吹自己有着较强的安全性,能够写出更加稳定可靠的应用程序,而安全性来自于 Swift 语言的设计哲学;由此看来静态类型、安全和动态类型、元编程能力(?)看起来是比较难以共存的。

其实很多静态编程语言,比如 C、C++ 和 Rust 都通过宏实现了比较强大的元编程能力,虽然 Swift 也通过模板在元编程支持上做了一些微小的努力,不过到目前来看( 3.0 )还是远远不够的。

OC 中对于 nil 的处理能够减少我们在编码时的工作量,不过也对工程师的代码质量提出了考验。我们需要思考 nil 的出现会不会带来崩溃,是否会导致行为的异常、增加应用崩溃的风险以及不确定性,而这也是 Swift 引入 Optional 这一概念来避免上述问题的初衷。

相比而言,笔者还是更喜欢强大的元编程能力,这样可以减少大量的重复工作并且提供更多的可能性,与提升工作效率相比,牺牲一些安全性还是可以接受的。

网络服务 Service 层

现有的大多数应用都会将网路服务组织成单独的一层,所以有时候你会看到所谓的 MVCS 架构模式,它其实只是在 MVC 的基础上加上了一个服务层(Service),而在 iOS 中常见的 MVC 架构模式也都可以理解为 MVCS 的形式,当引入了 Service 层之后,整个数据的获取以及处理的流程是这样的:

![](现有的大多数应用都会将网路服务组织成单独的一层,所以有时候你会看到所谓的 MVCS 架构模式,它其实只是在 MVC 的基础上加上了一个服务层(Service),而在 iOS 中常见的 MVC 架构模式也都可以理解为 MVCS 的形式,当引入了 Service 层之后,整个数据的获取以及处理的流程是这样的:)

  1. 大多数情况下服务的发起都是在 Controller 中进行的;
  2. 然后会在 HTTP 请求的回调中交给模型层处理 JSON 数据;
  3. 返回开箱即用的对象交还给 Controller 控制器;
  4. 最后由 View 层展示服务端返回的数据;

不过按理来说服务层并不属于模型层,为什么要在这里进行介绍呢?这是因为Service 层其实与 Model 层之间的联系非常紧密;网络请求返回的结果决定了 Model 层该如何设计以及该有哪些功能模块,而 Service 层的设计是与后端的 API 接口的设计强关联的,这也是我们谈模型层的设计无法绕过的坑。

iOS 中的 Service 层大体上有两种常见的组织方式,其中一种是命令式的,另一种是声明式的。

命令式

命令式的 Service 层一般都会为每一个或者一组 API 写一个专门用于 HTTP 请求的 Manager 类,在这个类中,我们会在每一个静态方法中使用 AFNetworking 或者 Alamofire 等网络框架发出 HTTP 请求。

Objective-C

import Foundation
import Alamofirefinal class UserManager {static let baseURL = "http://localhost:3000"static let usersBaseURL = "\(baseURL)/users"static func allUsers(completion: @escaping ([User]) -> ()) {let url = "\(usersBaseURL)"Alamofire.request(url).responseJSON { response inif let jsons = response.result.value as? [[String: Any]] {let users = User.users(jsons: jsons)completion(users)}}}static func user(id: Int, completion: @escaping (User) -> ()) {let url = "\(usersBaseURL)/\(id)"Alamofire.request(url).responseJSON { response inif let json = response.result.value as? [String: Any],let user = User(json: json) {completion(user)}}}
}

在这个方法中,我们完成了网络请求、数据转换 JSON、JSON 转换到模型以及最终使用 completion 回调的过程,调用 Service 服务的 Controller 可以直接从回调中使用构建好的 Model 对象。

Objective-C

UserManager.user(id: 1) { user inself.nameLabel.text = user.nameself.emailLabel.text = user.emailself.ageLabel.text = "\(user.age)"self.genderLabel.text = user.gender.rawValue
}

声明式

使用声明式的网络服务层与命令式的方法并没有本质的不同,它们最终都调用了底层的一些网络库的 API,这种网络服务层中的请求都是以配置的形式实现的,需要对原有的命令式的请求进行一层封装,也就是说所有的参数 requestURL、method 和 parameters 都应该以配置的形式声明在每一个 Request 类中。

如果是在 Objective-C 中,一般会定义一个抽象的基类,并让所有的 Request 都继承它;但是在 Swift 中,我们可以使用协议以及协议扩展的方式实现这一功能。

Swift

protocol AbstractRequest {var requestURL: String { get }var method: HTTPMethod { get }var parameters: Parameters? { get }
}extension AbstractRequest {func start(completion: @escaping (Any) -> Void) {Alamofire.request(requestURL, method: self.method).responseJSON { response inif let json = response.result.value {completion(json)}}}
}

在 AbstractRequest 协议中,我们定义了发出一个请求所需要的全部参数,并在协议扩展中实现了 start(completion:) 方法,这样实现该协议的类都可以直接调用 start(completion:) 发出网络请求。

Swift

final class AllUsersRequest: AbstractRequest {let requestURL = "http://localhost:3000/users"let method = HTTPMethod.getlet parameters: Parameters? = nil
}final class FindUserRequest: AbstractRequest {let requestURL: Stringlet method = HTTPMethod.getlet parameters: Parameters? = nilinit(id: Int) {self.requestURL = "http://localhost:3000/users/\(id)"}
}

我们在这里写了两个简单的 Request 类 AllUsersRequest 和 FindUserRequest,它们两个一个负责获取所有的 User 对象,一个负责从服务端获取指定的 User;在使用上面的声明式 Service 层时也与命令式有一些不同:

Swift

FindUserRequest(id: 1).start { json inif let json = json as? [String: Any],let user = User(json: json) {print(user)}
}

因为在 Swift 中,我们没法将 JSON 在 Service 层转换成模型对象,所以我们不得不在 FindUserRequest 的回调中进行类型以及 JSON 转模型等过程;又因为 HTTP 请求可能依赖其他的参数,所以在使用这种形式请求资源时,我们需要在初始化方法传入参数。

命令式 vs 声明式

现有的 iOS 开发中的网络服务层一般都是使用这两种组织方式,我们一般会按照资源或者功能来划分命令式中的 Manager 类,而声明式的 Request 类与实际请求是一对一的关系。

这两种网络层的组织方法在笔者看来没有高下之分,无论是 Manager 还是 Request 的方式,尤其是后者由于一个类只对应一个 API 请求,在整个 iOS 项目变得异常复杂时,就会导致网络层类的数量剧增。

这个问题并不是不可以接受的,在大多数项目中的网络请求就是这么做的,虽然在查找实际的请求类时有一些麻烦,不过只要遵循一定的命名规范还是可以解决的。

小结

现有的 MVC 下的 Model 层,其实只起到了对数据结构定义的作用,它将服务端返回的 JSON 数据,以更方便使用的方式包装了一下,这样呈现给上层的就是一些即拆即用的『字典』。

单独的 Model 层并不能返回什么关键的作用,它只有与网络服务层 Service 结合在一起的时候才能发挥更重要的能力。

而网络服务 Service 层是对 HTTP 请求的封装,其实现形式有两种,一种是命令式的,另一种是声明式的,这两种实现的方法并没有绝对的优劣,遵循合适的形式设计或者重构现有的架构,随着应用的开发与迭代,为上层提供相同的接口,保持一致性才是设计 Service 层最重要的事情。

谈谈 MVX 中的 Model相关推荐

  1. java中的model_Java程序员必看之springmvc中的Model对象在重定向中的数据

    原标题:Java程序员必看之springmvc中的Model对象在重定向中的数据 在springmvc框架中,一个handler方法中的参数可以内置接收Model类型的对象,主要用于存储数据之用,主要 ...

  2. html js脚本限制 正则,简单谈谈JS中的正则表达式

    1.正则表达式包括两部分 ①定义正则表达式的规则: ②正则表达式的模式(i/g/m): 2.声明正则表达式 ① 字面量声明: var reg = /表达式规则/表达式模式: eg:var reg = ...

  3. 【18】ASP.NET Core MVC 中的 Model介绍

    ASP.NET Core MVC 中的 Model 在本视频中,我们将通过一个示例讨论 ASP.NET Core MVC 中的 Model. 我们希望最终从 Student 数据库表中查询特定的学生详 ...

  4. Linux存储保护,谈谈Linux中的存储保护

    谈谈Linux中的存储保护 以下讨论的内容是以i386平台为基础的 Linux将4G的地址划分为用户空间和内核空间两部分.在Linux内核的低版本中(2.0.X),通常0-3G为用户空间,3G-4G为 ...

  5. ORM中的Model与DDD中的DomainModel

    0.引言 在现有的系统开发中,大部分的系统应该都会用到ORM,无论用的是EF还是NHibernate.作为对象和持久化数据的桥梁,ORM确实非常方便,以至于在DDD的时候,我们很自然的将 ORM中的M ...

  6. 谈谈JAVA中的安全发布

    谈谈JAVA中的安全发布 昨天看到一篇文章阐述技术类资料的"等级",看完之后很有共鸣.再加上最近在工作中越发觉得线程安全性的重要性和难以捉摸,又掏出了<Java并发编程实战& ...

  7. Spark精华问答 | 谈谈spark中的宽窄依赖

    总的来说,Spark采用更先进的架构,使得灵活性.易用性.性能等方面都比Hadoop更有优势,有取代Hadoop的趋势,但其稳定性有待进一步提高.我总结,具体表现在如下几个方面. 1 Q:Spark ...

  8. 谈谈C#中的三个关键词new , virtual , override

    谈谈C#中的三个关键词new , virtual , override C#支持单继承,说到继承就不得不说new,virtual和override这三个关键词,灵活正确的使用这三个关键词,可以使程序结 ...

  9. 谈谈java中成员变量与成员方法继承的问题

    谈谈java中成员变量与成员方法继承的问题 关于成员变量和成员方法的的继承问题,我也可以做一个小测试,来看看结果. 首先我们先创建一个父类: 其次再创建一个子类,子类中要比父类中少一个成员方法: 这样 ...

  10. 也谈谈Javascript中的几个怪异特性(上)

    2019独角兽企业重金招聘Python工程师标准>>> Andy Croxall在Ten Oddities And Secrets About JavaScript罗列了他认为的10 ...

最新文章

  1. html求和按钮,使用模板标记在html模板中求和
  2. linux csr蓝牙驱动,csr4.0蓝牙适配器驱动下载
  3. 戴维南定律和诺顿定律
  4. 吴恩达 coursera ML 第十七课总结+作业答案
  5. dvwa_xss_储存型
  6. centos安装cmake
  7. 六个主要的社会网络分析软件的比较
  8. android T 前台Service
  9. 「沙龙回顾」从技术演进角度看猫眼电影
  10. 局域网中的通信子网和资源子网
  11. Lua中的os.time和os.date以及时区计算
  12. 我们一起追逐过的大肥猫——tomcat部署
  13. 关于阿里云aca和acp哪个好?阿里云认证证书有含金量吗?
  14. 谷粒商城 - 微服务分布式电商项目
  15. linux sort 排序 指定间隔符
  16. 两个地址之间的距离测量(使用高德API)
  17. oracle检测数据表变化,Oracle 检查表的数据变动
  18. 我学会了用计算机作文,我学会了用计算机
  19. 3COM 交换机端口的监听配置 ZT
  20. SBT20100VDC-ASEMI贴片肖特基二极管SBT20100VDC

热门文章

  1. java 验证码 仿12306_Java仿12306图片验证码
  2. linux开启wifi热点命令,deepin Linux 开启wifi热点
  3. caption重要性 image_Multimodal —— 看图说话(Image Caption)任务的论文笔记(一)评价指标和NIC模型...
  4. 用VSCode写简历表
  5. als算法参数_推荐算法之ALS
  6. jquery 内容分页_为内容创建现代jQuery分页
  7. WIN10外接显示器有妙招
  8. 图片API 通用接口
  9. SDS之BlockChain Storage系列:3、为什说区块链存储是下一个热点 (上)
  10. 在服务器上如何打开mdf文件,在没sql server数据库状况下怎么打开.mdf文件