Follow GitHub: Draveness
常見的 Model 層
在大多數(shù) iOS 的項目中芒率,Model 層只是一個單純的數(shù)據(jù)結(jié)構(gòu),你可以看到的絕大部分模型都是這樣的:
struct User {
enum Gender: String {
case male = "male"
case female = "female"
}
let name: String
let email: String
let age: Int
let gender: Gender
}
模型起到了定義一堆『坑』的作用篙顺,只是一個簡單的模板偶芍,并沒有參與到實際的業(yè)務(wù)邏輯,只是在模型層進行了一層抽象德玫,將服務(wù)端發(fā)回的 JSON 或者說 Dictionary
對象中的字段一一取出并裝填到預(yù)先定義好的模型中匪蟀。
我們可以將這種模型層中提供的對象理解為『即開即用』的 Dictionary
實例;在使用時宰僧,可以直接從模型中取出屬性材彪,省去了從 Dictionary
中抽出屬性以及驗證是否合法的過程。
let user = User...
nameLabel.text = user.name
emailLabel.text = user.email
ageLabel.text = "\(user.age)"
genderLabel.text = user.gender.rawValue
JSON -> Model
使用 Swift 將 Dictionary
轉(zhuǎn)換成模型琴儿,在筆者看來其實是一件比較麻煩的事情查刻,主要原因是 Swift 作為一個號稱類型安全的語言,有著使用體驗非常差的 Optional 特性凤类,從 Dictionary
中取出的值都是不一定存在的穗泵,所以如果需要純手寫這個過程其實還是比較麻煩的。
extension User {
init(json: [String: Any]) {
let name = json["name"] as! String
let email = json["email"] as! String
let age = json["age"] as! Int
let gender = Gender(rawValue: json["gender"] as! String)!
self.init(name: name, email: email, age: age, gender: gender)
}
}
這里為 User
模型創(chuàng)建了一個 extension 并寫了一個簡單的模型轉(zhuǎn)換的初始化方法谜疤,當(dāng)我們從 JSON 對象中取值時佃延,得到的都是 Optional 對象;而在大多數(shù)情況下夷磕,我們都沒有辦法直接對 Optional 對象進行操作履肃,這就非常麻煩了。
麻煩的 Optional
在 Swift 中遇到無法立即使用的 Optional 對象時坐桩,我們可以會使用 !
默認(rèn)將字典中取出的值當(dāng)作非 Optional 處理尺棋,但是如果服務(wù)端發(fā)回的數(shù)據(jù)為空,這里就會直接崩潰绵跷;當(dāng)然膘螟,也可使用更加安全的 if let
對 Optional 對象進行解包(unwrap)成福。
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 中非常常見荆残,所以社區(qū)在 Swift 2.0 中引入了 guard
關(guān)鍵字來優(yōu)化代碼的結(jié)構(gòu)奴艾。
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)
}
}
不過,上面的代碼在筆者看來内斯,并沒有什么本質(zhì)的區(qū)別蕴潦,不過使用 guard
對錯誤的情況進行提前返回確實是一個非常好的編程習(xí)慣。
不關(guān)心空值的 OC
為什么 Objective-C 中沒有這種問題呢俘闯?主要原因是在 OC 中所有的對象其實都是 Optional 的潭苞,我們也并不在乎對象是否為空,因為在 OC 中向 nil 對象發(fā)送消息并不會造成崩潰真朗,Objective-C 運行時仍然會返回 nil 對象萄传。
這雖然在一些情況下會造成一些問題,比如蜜猾,當(dāng)
nil
導(dǎo)致程序發(fā)生崩潰時,比較難找到程序中nil
出現(xiàn)的原始位置振诬,但是卻保證了程序的靈活性蹭睡,筆者更傾向于 Objective-C 中的做法,不過這也就見仁見智了赶么。
OC 作為動態(tài)語言肩豁,這種設(shè)計思路其實還是非常優(yōu)秀的,它避免了大量由于對象不存在導(dǎo)致無法完成方法調(diào)用造成的崩潰辫呻;同時清钥,作為開發(fā)者,我們往往都不需要考慮 nil
的存在放闺,所以使用 OC 時寫出的模型轉(zhuǎn)換的代碼都相對好看很多祟昭。
// 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
當(dāng)然怖侦,在 OC 中也有很多優(yōu)秀的 JSON 轉(zhuǎn)模型的框架篡悟,如果我們使用 YYModel 這種開源框架匾寝,其實只需要寫一個 User
類的定義就可以獲得 -yy_modelWithJSON:
等方法來初始化 User
對象:
User *user = [User yy_modelWithJSON:json];
而這也是通過 Objective-C 強大的運行時特性做到的急凰。
除了 YYModel抡锈,我們也可以使用 Mantle 等框架在 OC 中解決 JSON 到模型的轉(zhuǎn)換的問題锭碳。
元編程能力
從上面的代碼,我們可以看出:Objective-C 和 Swift 對于相同功能的處理勿璃,卻有較大差別的實現(xiàn)擒抛。這種情況的出現(xiàn)主要原因是語言的設(shè)計思路導(dǎo)致的;Swift 一直鼓吹自己有著較強的安全性补疑,能夠?qū)懗龈臃€(wěn)定可靠的應(yīng)用程序歧沪,而安全性來自于 Swift 語言的設(shè)計哲學(xué);由此看來靜態(tài)類型莲组、安全和動態(tài)類型诊胞、元編程能力(?)看起來是比較難以共存的锹杈。
其實很多靜態(tài)編程語言撵孤,比如 C、C++ 和 Rust 都通過宏實現(xiàn)了比較強大的元編程能力竭望,雖然 Swift 也通過模板在元編程支持上做了一些微小的努力邪码,不過到目前來看( 3.0 )還是遠(yuǎn)遠(yuǎn)不夠的。
OC 中對于 nil
的處理能夠減少我們在編碼時的工作量咬清,不過也對工程師的代碼質(zhì)量提出了考驗闭专。我們需要思考 nil
的出現(xiàn)會不會帶來崩潰,是否會導(dǎo)致行為的異常旧烧、增加應(yīng)用崩潰的風(fēng)險以及不確定性影钉,而這也是 Swift 引入 Optional 這一概念來避免上述問題的初衷。
相比而言掘剪,筆者還是更喜歡強大的元編程能力平委,這樣可以減少大量的重復(fù)工作并且提供更多的可能性,與提升工作效率相比夺谁,犧牲一些安全性還是可以接受的肆汹。
網(wǎng)絡(luò)服務(wù) Service 層
現(xiàn)有的大多數(shù)應(yīng)用都會將網(wǎng)路服務(wù)組織成單獨的一層,所以有時候你會看到所謂的 MVCS 架構(gòu)模式予权,它其實只是在 MVC 的基礎(chǔ)上加上了一個服務(wù)層(Service)昂勉,而在 iOS 中常見的 MVC 架構(gòu)模式也都可以理解為 MVCS 的形式,當(dāng)引入了 Service 層之后扫腺,整個數(shù)據(jù)的獲取以及處理的流程是這樣的:
- 大多數(shù)情況下服務(wù)的發(fā)起都是在 Controller 中進行的岗照;
- 然后會在 HTTP 請求的回調(diào)中交給模型層處理 JSON 數(shù)據(jù);
- 返回開箱即用的對象交還給 Controller 控制器;
- 最后由 View 層展示服務(wù)端返回的數(shù)據(jù)攒至;
不過按理來說服務(wù)層并不屬于模型層厚者,為什么要在這里進行介紹呢?這是因為 Service 層其實與 Model 層之間的聯(lián)系非常緊密迫吐;網(wǎng)絡(luò)請求返回的結(jié)果決定了 Model 層該如何設(shè)計以及該有哪些功能模塊库菲,而 Service 層的設(shè)計是與后端的 API 接口的設(shè)計強關(guān)聯(lián)的,這也是我們談模型層的設(shè)計無法繞過的坑志膀。
iOS 中的 Service 層大體上有兩種常見的組織方式熙宇,其中一種是命令式的,另一種是聲明式的溉浙。
命令式
命令式的 Service 層一般都會為每一個或者一組 API 寫一個專門用于 HTTP 請求的 Manager 類烫止,在這個類中,我們會在每一個靜態(tài)方法中使用 AFNetworking 或者 Alamofire 等網(wǎng)絡(luò)框架發(fā)出 HTTP 請求戳稽。
import Foundation
import Alamofire
final 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 in
if 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 in
if let json = response.result.value as? [String: Any],
let user = User(json: json) {
completion(user)
}
}
}
}
在這個方法中馆蠕,我們完成了網(wǎng)絡(luò)請求、數(shù)據(jù)轉(zhuǎn)換 JSON惊奇、JSON 轉(zhuǎn)換到模型以及最終使用 completion
回調(diào)的過程互躬,調(diào)用 Service 服務(wù)的 Controller 可以直接從回調(diào)中使用構(gòu)建好的 Model 對象。
UserManager.user(id: 1) { user in
self.nameLabel.text = user.name
self.emailLabel.text = user.email
self.ageLabel.text = "\(user.age)"
self.genderLabel.text = user.gender.rawValue
}
聲明式
使用聲明式的網(wǎng)絡(luò)服務(wù)層與命令式的方法并沒有本質(zhì)的不同颂郎,它們最終都調(diào)用了底層的一些網(wǎng)絡(luò)庫的 API吼渡,這種網(wǎng)絡(luò)服務(wù)層中的請求都是以配置的形式實現(xiàn)的,需要對原有的命令式的請求進行一層封裝祖秒,也就是說所有的參數(shù) requestURL
、method
和 parameters
都應(yīng)該以配置的形式聲明在每一個 Request
類中舟奠。
如果是在 Objective-C 中竭缝,一般會定義一個抽象的基類,并讓所有的 Request 都繼承它沼瘫;但是在 Swift 中抬纸,我們可以使用協(xié)議以及協(xié)議擴展的方式實現(xiàn)這一功能。
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 in
if let json = response.result.value {
completion(json)
}
}
}
}
在 AbstractRequest
協(xié)議中耿戚,我們定義了發(fā)出一個請求所需要的全部參數(shù)湿故,并在協(xié)議擴展中實現(xiàn)了 start(completion:)
方法,這樣實現(xiàn)該協(xié)議的類都可以直接調(diào)用 start(completion:)
發(fā)出網(wǎng)絡(luò)請求膜蛔。
final class AllUsersRequest: AbstractRequest {
let requestURL = "http://localhost:3000/users"
let method = HTTPMethod.get
let parameters: Parameters? = nil
}
final class FindUserRequest: AbstractRequest {
let requestURL: String
let method = HTTPMethod.get
let parameters: Parameters? = nil
init(id: Int) {
self.requestURL = "http://localhost:3000/users/\(id)"
}
}
我們在這里寫了兩個簡單的 Request
類 AllUsersRequest
和 FindUserRequest
坛猪,它們兩個一個負(fù)責(zé)獲取所有的 User
對象,一個負(fù)責(zé)從服務(wù)端獲取指定的 User
皂股;在使用上面的聲明式 Service 層時也與命令式有一些不同:
FindUserRequest(id: 1).start { json in
if let json = json as? [String: Any],
let user = User(json: json) {
print(user)
}
}
因為在 Swift 中墅茉,我們沒法將 JSON 在 Service 層轉(zhuǎn)換成模型對象,所以我們不得不在 FindUserRequest
的回調(diào)中進行類型以及 JSON 轉(zhuǎn)模型等過程;又因為 HTTP 請求可能依賴其他的參數(shù)就斤,所以在使用這種形式請求資源時悍募,我們需要在初始化方法傳入?yún)?shù)。
命令式 vs 聲明式
現(xiàn)有的 iOS 開發(fā)中的網(wǎng)絡(luò)服務(wù)層一般都是使用這兩種組織方式洋机,我們一般會按照資源或者功能來劃分命令式中的 Manager
類坠宴,而聲明式的 Request
類與實際請求是一對一的關(guān)系。
這兩種網(wǎng)絡(luò)層的組織方法在筆者看來沒有高下之分绷旗,無論是 Manager
還是 Request
的方式喜鼓,尤其是后者由于一個類只對應(yīng)一個 API 請求,在整個 iOS 項目變得異常復(fù)雜時刁标,就會導(dǎo)致網(wǎng)絡(luò)層類的數(shù)量劇增颠通。
這個問題并不是不可以接受的,在大多數(shù)項目中的網(wǎng)絡(luò)請求就是這么做的膀懈,雖然在查找實際的請求類時有一些麻煩顿锰,不過只要遵循一定的命名規(guī)范還是可以解決的。
小結(jié)
現(xiàn)有的 MVC 下的 Model 層启搂,其實只起到了對數(shù)據(jù)結(jié)構(gòu)定義的作用硼控,它將服務(wù)端返回的 JSON 數(shù)據(jù),以更方便使用的方式包裝了一下胳赌,這樣呈現(xiàn)給上層的就是一些即拆即用的『字典』牢撼。
單獨的 Model 層并不能返回什么關(guān)鍵的作用,它只有與網(wǎng)絡(luò)服務(wù)層 Service 結(jié)合在一起的時候才能發(fā)揮更重要的能力疑苫。
而網(wǎng)絡(luò)服務(wù) Service 層是對 HTTP 請求的封裝熏版,其實現(xiàn)形式有兩種,一種是命令式的捍掺,另一種是聲明式的撼短,這兩種實現(xiàn)的方法并沒有絕對的優(yōu)劣,遵循合適的形式設(shè)計或者重構(gòu)現(xiàn)有的架構(gòu)挺勿,隨著應(yīng)用的開發(fā)與迭代曲横,為上層提供相同的接口,保持一致性才是設(shè)計 Service 層最重要的事情不瓶。
服務(wù)端的 Model 層
雖然文章是對客戶端中 Model 層進行分析和介紹禾嫉,但是在客戶端大規(guī)模使用 MVC 架構(gòu)模式之前,服務(wù)端對于 MVC 的使用早已有多年的歷史蚊丐,而移動端以及 Web 前端對于架構(gòu)的設(shè)計是近年來才逐漸被重視熙参。
因為客戶端的應(yīng)用變得越來越復(fù)雜,動輒上百萬行代碼的巨型應(yīng)用不斷出現(xiàn)麦备,以前流水線式的開發(fā)已經(jīng)沒有辦法解決現(xiàn)在的開發(fā)尊惰、維護工作讲竿,所以合理的架構(gòu)設(shè)計成為客戶端應(yīng)用必須要重視的事情。
這一節(jié)會以 Ruby on Rails 中 Model 層的設(shè)計為例弄屡,分析在經(jīng)典的 MVC 框架中的 Model 層是如何與其他模塊進行交互的题禀,同時它又擔(dān)任了什么樣的職責(zé)。
Model 層的職責(zé)
Rails 中的 Model 層主要承擔(dān)著以下兩大職責(zé):
- 使用數(shù)據(jù)庫存儲并管理 Web 應(yīng)用的數(shù)據(jù)膀捷;
- 包含 Web 應(yīng)用所有的業(yè)務(wù)邏輯迈嘹;
除了上述兩大職責(zé)之外,Model 層還會存儲應(yīng)用的狀態(tài)全庸,同時秀仲,由于它對用戶界面一無所知,所以它不依賴于任何視圖的狀態(tài)壶笼,這也使得 Model 層的代碼可以復(fù)用神僵。
Model 層的兩大職責(zé)決定了它在整個 MVC 框架的位置:
因為 Model 是對數(shù)據(jù)庫中表的映射,所以當(dāng) Controller 向 Model 層請求數(shù)據(jù)時覆劈,它會從數(shù)據(jù)庫中獲取相應(yīng)的數(shù)據(jù)保礼,然后對數(shù)據(jù)進行加工最后返回給 Controller 層。
數(shù)據(jù)庫
Model 層作為數(shù)據(jù)庫中表的映射责语,它就需要實現(xiàn)兩部分功能:
- 使用合理的方式對數(shù)據(jù)庫進行遷移和更新炮障;
- 具有數(shù)據(jù)庫的絕大部分功能,包括最基礎(chǔ)的增刪改查坤候;
在這里我們以 Rails 的 ActiveRecord 為例胁赢,簡單介紹這兩大功能是如何工作的。
ActiveRecord 為數(shù)據(jù)庫的遷移和更新提供了一種名為 Migration 的機制白筹,它可以被理解為一種 DSL智末,對數(shù)據(jù)庫中的表的字段、類型以及約束進行描述:
class CreateProducts < ActiveRecord::Migration[5.0]
def change
create_table :products do |t|
t.string :name
t.text :description
end
end
end
上面的 Ruby 代碼創(chuàng)建了一個名為 Products
表徒河,其中包含三個字段 name
系馆、description
以及一個默認(rèn)的主鍵 id
,然而在上述文件生成時虚青,數(shù)據(jù)庫中對應(yīng)的表還不存在它呀,當(dāng)我們在命令行中執(zhí)行 rake db:migrate
時螺男,才會執(zhí)行下面的 SQL 語句生成一張表:
CREATE TABLE products (
id int(11) DEFAULT NULL auto_increment PRIMARY KEY
name VARCHAR(255),
description text,
);
同樣地棒厘,如果我們想要更新數(shù)據(jù)庫中的表的字段,也需要創(chuàng)建一個 Migration 文件下隧,ActiveRecord 會為我們直接生成一個 SQL 語句并在數(shù)據(jù)庫中執(zhí)行奢人。
ActiveRecord 對數(shù)據(jù)庫的增刪改查功能都做了相應(yīng)的實現(xiàn),在使用它進行數(shù)據(jù)庫查詢時淆院,會生成一條 SQL 語句何乎,在數(shù)據(jù)庫中執(zhí)行,并將執(zhí)行的結(jié)果初始化成一個 Model 的實例并返回:
user = User.find(10)
# => SELECT * FROM users WHERE (users.id = 10) LIMIT 1
這就是 ActiveRecord 作為 Model 層的 ORM 框架解決兩個關(guān)鍵問題的方式,其最終結(jié)果都是生成一條 SQL 語句并扔到數(shù)據(jù)庫中執(zhí)行支救。
總而言之抢野,Model 層為調(diào)用方屏蔽了所有與數(shù)據(jù)庫相關(guān)的底層細(xì)節(jié),使開發(fā)者不需要考慮如何手寫 SQL 語句各墨,只需要關(guān)心原生的代碼指孤,能夠極大的降低出錯的概率甥雕;但是鄙漏,由于 SQL 語句都由 Model 層負(fù)責(zé)處理生成,它并不會根據(jù)業(yè)務(wù)幫助我們優(yōu)化 SQL 查詢語句蟀给,所以在遇到數(shù)據(jù)量較大時黎做,其性能難免遇到各種問題叉跛,我們?nèi)匀恍枰謩觾?yōu)化查詢的 SQL 語句。
Controller
Model 與數(shù)據(jù)庫之間的關(guān)系其實大多數(shù)都與數(shù)據(jù)的存儲查詢有關(guān)蒸殿,而與 Controller 的關(guān)系就不是這樣了筷厘,在 Rails 這個 MVC 框架中,提倡將業(yè)務(wù)邏輯放到 Model 層進行處理伟桅,也就是所謂的:
Fat Models, skinny controllers.
這種說法形成的原因是敞掘,在絕大部分的 MVC 框架中,Controller 的作用都是將請求代理給 Model 去完成楣铁,它本身并不包含任何的業(yè)務(wù)邏輯玖雁,任何實際的查詢、更新和刪除操作都不應(yīng)該在 Controller 層直接進行盖腕,而是要講這些操作交給 Model 去完成赫冬。
class UsersController
def show
@user = User.find params[:id]
end
end
這也就是為什么在后端應(yīng)用中設(shè)計合理的 Controller 實際上并沒有多少行代碼,因為大多數(shù)業(yè)務(wù)邏輯相關(guān)的代碼都會放到 Model 層溃列。
Controller 的作用更像是膠水劲厌,將 Model 層中獲取的模型傳入 View 層中,渲染 HTML 或者返回 JSON 數(shù)據(jù)听隐。
小結(jié)
雖然服務(wù)端對于應(yīng)用架構(gòu)的設(shè)計已經(jīng)有了很長時間的沉淀补鼻,但是由于客戶端和服務(wù)端的職責(zé)截然不同,我們可以從服務(wù)端借鑒一些設(shè)計雅任,但是并不應(yīng)該照搬后端應(yīng)用架構(gòu)設(shè)計的思路风范。
服務(wù)端重數(shù)據(jù),如果把整個 Web 應(yīng)用看做一個黑箱沪么,那么它的輸入就是用戶發(fā)送的數(shù)據(jù)硼婿,發(fā)送的形式無論是遵循 HTTP 協(xié)議也好還是其它協(xié)議也好,它們都是數(shù)據(jù)禽车。
在服務(wù)端拿到數(shù)據(jù)后對其進行處理寇漫、加工以及存儲刊殉,最后仍然以數(shù)據(jù)的形式返回給用戶。
而客戶端重展示州胳,其輸入就是用戶的行為觸發(fā)的事件记焊,而輸出是用戶界面:
也就是說,用戶的行為在客戶端應(yīng)用中得到響應(yīng)栓撞,并更新了用戶界面 GUI亚亲。總而言之:
客戶端重展示腐缤,服務(wù)端重數(shù)據(jù)捌归。
這也是在設(shè)計客戶端 Model 層時需要考慮的重要因素。
理想中的 Model 層
在上面的兩個小節(jié)中岭粤,分別介紹了 iOS 中現(xiàn)有的 Model 層以及服務(wù)端的 Model 層是如何使用的惜索,并且介紹了它們的職責(zé),在這一章節(jié)中剃浇,我們準(zhǔn)備介紹筆者對于 Model 層的看法以及設(shè)計巾兆。
明確職責(zé)
在具體討論 Model 層設(shè)計之前,肯定要明確它的職責(zé)虎囚,它應(yīng)該做什么角塑、不應(yīng)該做什么以及需要為外界提供什么樣的接口和功能。
客戶端重展示淘讥,無論是 Web圃伶、iOS 還是 Android,普通用戶應(yīng)該無法直接接觸到服務(wù)端蒲列,如果一個軟件系統(tǒng)的使用非常復(fù)雜窒朋,并且讓普通用戶直接接觸到服務(wù)端的各種報錯、提示蝗岖,比如 404 等等侥猩,那么這個軟件的設(shè)計可能就是不合理的。
這里加粗了普通和直接兩個詞抵赢,如果對這句話有疑問欺劳,請多讀幾遍 :)
專業(yè)的錯誤信息在軟件工程師介入排錯時非常有幫助,這種信息應(yīng)當(dāng)放置在不明顯的角落铅鲤。
作為軟件工程師或者設(shè)計師划提,應(yīng)該為用戶提供更加合理的界面以及展示效果,比如彩匕,使用您所瀏覽的網(wǎng)頁不存在來描述或者代替只有從事軟件開發(fā)行業(yè)的人才了解的 404 或者 500 等錯誤是更為合適的方式腔剂。
上面的例子主要是為了說明客戶端的最重要的職責(zé)媒区,將數(shù)據(jù)合理地展示給用戶驼仪,從這里我們可以領(lǐng)會到掸犬,Model 層雖然重要,但是卻不是客戶端最為復(fù)雜的地方绪爸,它只是起到了一個將服務(wù)端數(shù)據(jù)『映射』到客戶端的作用湾碎,這個映射的過程就是獲取數(shù)據(jù)的過程,也決定了 Model 層在 iOS 應(yīng)用中的位置奠货。
那么這樣就產(chǎn)生了幾個非常重要的問題和子問題:
- 數(shù)據(jù)如何獲冉槿臁?
- 在何時獲取數(shù)據(jù)递惋?
- 如何存儲服務(wù)端的數(shù)據(jù)柔滔?
- 數(shù)據(jù)如何展示?
- 應(yīng)該為上層提供什么樣的接口萍虽?
Model 層 += Service 層睛廊?
首先,我們來解決數(shù)據(jù)獲取的問題杉编,在 iOS 客戶端常見的 Model 層中超全,數(shù)據(jù)的獲取都不是由 Model 層負(fù)責(zé)的,而是由一個單獨的 Service 層進行處理邓馒,然而經(jīng)常這么組織網(wǎng)絡(luò)請求并不是一個非常優(yōu)雅的辦法:
- 如果按照 API 組織 Service 層嘶朱,那么網(wǎng)絡(luò)請求越多,整個項目的 Service 層的類的數(shù)量就會越龐大光酣;
- 如果按照資源組織 Service 層疏遏,那么為什么不把 Service 層中的代碼直接扔到 Model 層呢?
既然 HTTP 請求都以獲取相應(yīng)的資源為目標(biāo)救军,那么以 Model 層為中心來組織 Service 層并沒有任何語義和理解上的問題改览。
如果服務(wù)端的 API 嚴(yán)格地按照 RESTful 的形式進行設(shè)計,那么就可以在客戶端的 Model 層建立起一一對應(yīng)的關(guān)系缤言,拿最基本的幾個 API 請求為例:
extension RESTful {
static func index(completion: @escaping ([Self]) -> ())
static func show(id: Int, completion: @escaping (Self?) -> ())
static func create(params: [String: Any], completion: @escaping (Self?) -> ())
static func update(id: Int, params: [String: Any], completion: @escaping (Self?) -> ())
static func delete(id: Int, completion: @escaping () -> ())
}
我們在 Swift 中通過 Protocol Extension 的方式為所有遵循 RESTful
協(xié)議的模型添加基本的 CRUD 方法宝当,那么 RESTful
協(xié)議本身又應(yīng)該包含什么呢?
protocol RESTful {
init?(json: [String: Any])
static var url: String { get }
}
RESTful 協(xié)議本身也十分簡單胆萧,一是 JSON 轉(zhuǎn)換方法庆揩,也就是如何將服務(wù)器返回的 JSON 數(shù)據(jù)轉(zhuǎn)換成對應(yīng)的模型,另一個是資源的 url
對于這里的
url
跌穗,我們可以遵循約定優(yōu)于配置的原則订晌,通過反射獲取一個默認(rèn)的資源鏈接,從而簡化原有的RESTful
協(xié)議蚌吸,但是這里為了簡化代碼并沒有使用這種方法锈拨。
extension User: RESTful {
static var url: String {
return "http://localhost:3000/users"
}
init?(json: [String: Any]) {
guard let id = json["id"] as? Int,
let name = json["name"] as? String,
let email = json["email"] as? String,
let age = json["age"] as? Int,
let genderValue = json["gender"] as? Int,
let gender = Gender(rawInt: genderValue) else {
return nil
}
self.init(id: id, name: name, email: email, age: age, gender: gender)
}
}
在 User
模型遵循上述協(xié)議之后,我們就可以簡單的通過它的靜態(tài)方法來對服務(wù)器上的資源進行一系列的操作羹唠。
User.index { users in
// users
}
User.create(params: ["name": "Stark", "email": "example@email.com", "gender": 0, "age": 100]) { user in
// user
}
當(dāng)然 RESTful 的 API 接口仍然需要服務(wù)端提供支持奕枢,不過以 Model 取代 Service 作為 HTTP 請求的發(fā)出者確實是可行的娄昆。
問題
雖然上述的方法簡化了 Service 層,但是在真正使用時確實會遇到較多的限制缝彬,比如萌焰,用戶需要對另一用戶進行關(guān)注或者取消關(guān)注操作,這樣的 API 如果要遵循 RESTful 就需要使用以下的方式進行設(shè)計:
POST /api/users/1/follows
DELETE /api/users/1/follows
這種情況就會導(dǎo)致在當(dāng)前的客戶端的 Model 層沒法建立合適的抽象谷浅,因為 follows
并不是一個真實存在的模型扒俯,它只代表兩個用戶之間的關(guān)系,所以在當(dāng)前所設(shè)計的模型層中沒有辦法實現(xiàn)上述的功能一疯,還需要引入 Service 層撼玄,來對服務(wù)端中的每一個 Controller 的 action 進行抽象,在這里就不展開討論了墩邀。
對 Model 層網(wǎng)絡(luò)服務(wù)的設(shè)計互纯,與服務(wù)端的設(shè)計有著非常大的關(guān)聯(lián),如果能夠?qū)蛻舳撕头?wù)端之間的 API 進行嚴(yán)格規(guī)范磕蒲,那么對于設(shè)計出簡潔留潦、優(yōu)雅的網(wǎng)絡(luò)層還是有巨大幫助的。
緩存與持久存儲
客戶端的持久存儲其實與服務(wù)端的存儲天差地別辣往,客戶端中保存的各種數(shù)據(jù)更準(zhǔn)確的說其實是緩存兔院,既然是緩存,那么它在客戶端應(yīng)用中的地位并不是極其重要站削、非他不可的坊萝;正相反,很多客戶端應(yīng)用沒有緩存也運行的非常好许起,它并不是一個必要的功能十偶,只是能夠提升用戶體驗而已。
雖然客戶端的存儲只是緩存园细,但是在目前的大型應(yīng)用中惦积,也確實需要這種緩存,有以下幾個原因:
- 能夠快速為用戶提供可供瀏覽的內(nèi)容猛频;
- 在網(wǎng)絡(luò)情況較差或者無網(wǎng)絡(luò)時狮崩,也能夠為用戶提供兜底數(shù)據(jù);
以上的好處其實都是從用戶體驗的角度說的鹿寻,不過緩存確實能夠提高應(yīng)用的質(zhì)量睦柴。
在 iOS 中,持久存儲雖然不是一個必要的功能毡熏,但是蘋果依然為我們提供了不是那么好用的 Core Data 框架坦敌,但這并不是這篇文章需要介紹和討論的內(nèi)容。
目前的絕大多數(shù) Model 框架,其實提供的都只是硬編碼的數(shù)據(jù)庫操作能力狱窘,或者提供的 API 不夠優(yōu)雅杜顺,原因是雖然 Swift 語法比 Objective-C 更加簡潔,但是缺少元編程能力是它的硬傷训柴。
熟悉 ActiveRecord 的開發(fā)者應(yīng)該都熟悉下面的使用方式:
User.find_by_name "draven"
在 Swift 中通過現(xiàn)有的特性很難提供這種 API,所以很多情況下只能退而求其次妇拯,繼承 NSObject
并且使用 dynamic
關(guān)鍵字記住 Objective-C 的特性實現(xiàn)一些功能:
class User: Object {
dynamic var name = ""
dynamic var age = 0
}
這確實是一種解決辦法幻馁,但是并不是特別的優(yōu)雅,如果我們在編譯器間獲得模型信息越锈,然后使用這些信息生成代碼就可以解決這些問題了仗嗦,這種方法同時也能夠在 Xcode 編譯器中添加代碼提示。
上層接口
Model 層為上層提供提供的接口其實就是自身的一系列屬性甘凭,只是將服務(wù)器返回的 JSON 經(jīng)過處理和類型轉(zhuǎn)換稀拐,變成了即拆即用的數(shù)據(jù)。
上層與 Model 層交互有兩種方式丹弱,一是通過 Model 層調(diào)用 HTTP 請求德撬,異步獲取模型數(shù)據(jù),另一種就是通過 Model 暴露出來的屬性進行存取躲胳,而底層數(shù)據(jù)庫會在 Model 屬性更改時發(fā)出網(wǎng)絡(luò)請求并且修改對應(yīng)的字段蜓洪。
總結(jié)
雖然客戶端的 Model 層與服務(wù)端的 Model 層有著相同的名字,但是客戶端的 Model 層由于處理的是緩存坯苹,對本地的數(shù)據(jù)庫中的表進行遷移隆檀、更改并不是一個必要的功能,在本地表字段進行大規(guī)模修改時粹湃,只需要刪除全部表中的內(nèi)容恐仑,并重新創(chuàng)建即可,只要不影響服務(wù)端的數(shù)據(jù)就不是太大的問題为鳄。
iOS 中的 Model 層不應(yīng)該是一個單純的數(shù)據(jù)結(jié)構(gòu)裳仆,它應(yīng)該起到發(fā)出 HTTP 請求、進行字段驗證以及持久存儲的職責(zé)孤钦,同時為上層提供網(wǎng)絡(luò)請求的方法以及字段作為接口鉴逞,為視圖的展示提供數(shù)據(jù)源的作用。我們應(yīng)該將更多的與 Model 層有關(guān)的業(yè)務(wù)邏輯移到 Model 中以控制 Controller 的復(fù)雜性司训。