談?wù)?MVX 中的 Model

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ù)先定義好的模型中匪蟀。

JSON-to-Mode

我們可以將這種模型層中提供的對象理解為『即開即用』的 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)不夠的。

Dynamic-Stati

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ù)的獲取以及處理的流程是這樣的:

MVCS-Architecture
  1. 大多數(shù)情況下服務(wù)的發(fā)起都是在 Controller 中進行的岗照;
  2. 然后會在 HTTP 請求的回調(diào)中交給模型層處理 JSON 數(shù)據(jù);
  3. 返回開箱即用的對象交還給 Controller 控制器;
  4. 最后由 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ù) requestURLmethodparameters 都應(yīng)該以配置的形式聲明在每一個 Request 類中舟奠。

Abstract-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)"
    }
}

我們在這里寫了兩個簡單的 RequestAllUsersRequestFindUserRequest坛猪,它們兩個一個負(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)系。

Manager-And-Request

這兩種網(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-And-Dictioanry

單獨的 Model 層并不能返回什么關(guān)鍵的作用,它只有與網(wǎng)絡(luò)服務(wù)層 Service 結(jié)合在一起的時候才能發(fā)揮更重要的能力疑苫。

Service-And-API

而網(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é):

  1. 使用數(shù)據(jù)庫存儲并管理 Web 應(yīng)用的數(shù)據(jù)膀捷;
  2. 包含 Web 應(yīng)用所有的業(yè)務(wù)邏輯迈嘹;

除了上述兩大職責(zé)之外,Model 層還會存儲應(yīng)用的狀態(tài)全庸,同時秀仲,由于它對用戶界面一無所知,所以它不依賴于任何視圖的狀態(tài)壶笼,這也使得 Model 層的代碼可以復(fù)用神僵。

Model 層的兩大職責(zé)決定了它在整個 MVC 框架的位置:

Server-MV

因為 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)兩部分功能:

  1. 使用合理的方式對數(shù)據(jù)庫進行遷移和更新炮障;
  2. 具有數(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í)行支救。

Relation-Between-Database-And-Mode

總而言之抢野,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ù)禽车。

web-black-box

在服務(wù)端拿到數(shù)據(jù)后對其進行處理寇漫、加工以及存儲刊殉,最后仍然以數(shù)據(jù)的形式返回給用戶。

而客戶端重展示州胳,其輸入就是用戶的行為觸發(fā)的事件记焊,而輸出是用戶界面:

client-black-box

也就是說,用戶的行為在客戶端應(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)放置在不明顯的角落铅鲤。

404

作為軟件工程師或者設(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)用中的位置奠货。

Model-in-Client

那么這樣就產(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)雅的辦法:

  1. 如果按照 API 組織 Service 層嘶朱,那么網(wǎng)絡(luò)請求越多,整個項目的 Service 層的類的數(shù)量就會越龐大光酣;
  2. 如果按照資源組織 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ù)。

JSON-Mode

上層與 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ù)雜性司训。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末构捡,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子壳猜,更是在濱河造成了極大的恐慌勾徽,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件统扳,死亡現(xiàn)場離奇詭異喘帚,居然都是意外死亡畅姊,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門吹由,熙熙樓的掌柜王于貴愁眉苦臉地迎上來若未,“玉大人,你說我怎么就攤上這事倾鲫〈趾希” “怎么了?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵乌昔,是天一觀的道長隙疚。 經(jīng)常有香客問我,道長磕道,這世上最難降的妖魔是什么供屉? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮溺蕉,結(jié)果婚禮上伶丐,老公的妹妹穿的比我還像新娘。我一直安慰自己疯特,他們只是感情好撵割,可當(dāng)我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著辙芍,像睡著了一般啡彬。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上故硅,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天庶灿,我揣著相機與錄音,去河邊找鬼吃衅。 笑死往踢,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的徘层。 我是一名探鬼主播峻呕,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼趣效!你這毒婦竟也來了瘦癌?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤跷敬,失蹤者是張志新(化名)和其女友劉穎讯私,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡斤寇,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年桶癣,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片娘锁。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡牙寞,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出莫秆,到底是詐尸還是另有隱情间雀,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布馏锡,位于F島的核電站雷蹂,受9級特大地震影響伟端,放射性物質(zhì)發(fā)生泄漏杯道。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一责蝠、第九天 我趴在偏房一處隱蔽的房頂上張望党巾。 院中可真熱鬧,春花似錦霜医、人聲如沸齿拂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽署海。三九已至,卻和暖如春医男,著一層夾襖步出監(jiān)牢的瞬間砸狞,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工镀梭, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留刀森,地道東北人。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓报账,卻偏偏與公主長得像研底,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子透罢,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容