建立一个类型安全的 Swift 模型

UserProfile

每个用户都有 namefirstNamelastName 有些用户还有 emailphoneNumber 。模型可能会是这样:

struct UserProfile {
    let name: String
    let lastName: String
    let phoneNumber: String?
    let email: String?
}

可能我们每天都会看到这样的模型,也习惯了这样的模型,但仔细思考一下。email 有没有把信息完整的表示出来呢?还有phoneNumber ? 他俩仅仅只是字符串吗?

如果他有在父类中没有的其他行为,则引入一个新的类型。 --- Martin Fowler

make a type if it will have some special behavior in its operations that the base type doesn’t have. by Martin Fowler

在之后的代码里面,我们可能就会用到这两个属性。比如说打电话,或者是给用户发送邮件。但是从模型里面,我们只能推导出来这两个东西是 String? 当然,我们可以在每次使用的时候都做一次校验,或者在初始化方法里面校验,但是这都不是类型层面上的东。如果有其他人要用这段代码的时候,他们知道这两个东西需要做一次校验吗?加一段注释:// 这个属性需要做一次校验 是最有效的方法吗?又如果项目中还有一个类Contact 也需要用到 phoneNumber 呢?校验的代码又复制过去?

typealias Email = String
typealias PhoneNumber = String

struct UserProfile {
    let name: String
    let lastName: String
    let phoneNumber: PhoneNumber?
    let email: Email?
}

我们至少能用到 typealias 来让他们知道, 这些东西跟 String 还是有一点区别的。

不知道你有没有发现 TimeIntervalCLLocationDegrees 这个两个类型,实际上都是 Double 的类型别名。为什么要这么做呢?答案是: 为了上下文。当我们看到 CLLocationDegrees 的时候,我们就能知道这个值的范围在 [-180,180] 之间。如果这个值是不可能是 1000 的。同样,手机号也不可能是一个类似 "HelloWorld" 的字符串。

当我们使用 typealias 来声明自定义类型的时候,我们可以在今后的版本中轻易的给他添加更多的上下文,而不用去修改很多的代码。比如说 CGFloat ,在早期的 Swift 版本中,这个属性只是 Double 的一个类型别名,而现在他已经是一个完整的数据类型了。

现在我们来试试怎样把刚刚的类型别名改成一个完整的数据类型:

struct Email {
    let address: String
    
    /** 或者还可以将邮箱地址分隔开成 domain 还有 localPart*/
    
    //  let domain: String
    //  let localPart: String
}

struct PhoneNumber {
    let digist: String
    
    /** 跟邮箱一样,手机号码可以分成 国家代码,区域代码,号码三个部分 */
    
    //  let countryCode: String?
    //  let areaCode: String
    //  let destination: String
}

struct UserProfile {
    let name: String
    let lastName: String
    let phoneNumber: PhoneNumber?
    let email: Email?
}

现在的代码就更能表示出 UserFrofile.email 具体是什么东西了,这样的代码甚至能够避免以后会出现的问题。

比如下面的问题。比如说我们并没有重写 UserPrifile ,现在我们需要用户的全名。

struct UserProfile {
    let name: String
    let lastName: String
    let phoneNumber: String
    let email: String?
}

extension UserProfile {
    var fullName: String {
        return name + " " + cellNumber
    }
}

处于一些原因,我们错误的把 cellNumber 拼在了后面(正确的应该是 lastName )。编译器不会发现这其实是个错误,因为对编译器来说他们都是 String。这个问题有可能在运行时才会被发现出来,甚至是测试阶段。但是如果使用 PhoneNumber 这个类型的话,这个问题在编译期就能够被发现了。

User

随着 App 的发展,我们肯能会用到 OTP 作为 App 的登录方法。这时候可能还会引入一个新的类型 User

struct User {
    let id: String
    let isRegistered: Bool
    let phoneNumber: String?
    let profile: UserProfile?
}

struct UserProfile {
    let name: String
    let lastName: String
    let email: String?
}

如果完全按照上文的方法,代码会变成这样子:

typealias UserID = String

struct PhoneNumber {
    let digits: String
}

struct Email {
    let address: String
}

struct User {
    let id: UserID
    let phoneNumber: PhoneNumber?
    let isRegistered: Bool
    let profile: UserProfile?
}

struct UserProfile {
    let name: String
    let lastName: String
    let email: Email?
}

如果我们接着写下去,代码会变得很啰嗦。PhoneNumber 还有 Email 还有一些意义, 而 UserID 还有 UserProfileUser 之外没有任何意义。所以再把代码整理一下:

struct User {
    typealias ID = String
    
    let id: ID
    let phoneNumber: PhoneNumber?
    let isRegistered: Bool
    let profile: Profile?
    
    struct Profile {
        let name: String
        let lastName: String
        let email: Email?
    }
}

如果要使用 UserProfile 的时候,我们可以使用 User.Profile 来替代。同样 UserID 也用 User.ID 替代。这样就更有意义了。

但是这样仍然还有问题。 对 profile 还有 isRegisterted 来说仍然还有可能会有问题。要知道,如果用户已经注册,就一定会有用户资料。反之,如果未注册,就肯定没有用户资料。但是从这个类的声明讲,这并没有体现出来这个逻辑。我们使用枚举和模式匹配来做这件事情:

struct User {
    typealias ID = String
    
    let id: ID
    let phoneNumber: PhoneNumber?
    let status: Status
    
    enum Status {
        case notRegistered
        case registered(profile: Profile)
    }
    struct Profile {
        let name: String
        let lastName: String
        let email: Email?
    }
}

现在我们就能确定只有当用户已注册的时候才会有 Profile 了。

是否使用枚举,完全取决于 App 的上下文。

如果我们有一个新属性 isEmailVerified 。用来表示 email 是否在服务端做过验证。这就不需要使用枚举,因为是否验证跟这个值是否存在没有关系。

JSON & Codable

首先我们需要知道 JSON 并不是类型安全的。他只支持基本数据类型,数组还有字典。

JSON 包含了很多的上下文,因为在 JSON 文件中, email 和 phoneNumber 都是字符串。我们应该知道如何去处理它。

下面是一个 User 的 JSON 示例

// 未注册用户
"user": {
    "id": "a_unique_id",
    "phone_number": "+989354358291",
    "is_registered": false
}
// 注册用户
"user": {
    "id": "a_unique_id",
    "phone_number": "+989354358291",
    "is_registered": true,
    "profile": {
        "name": "Farzad",
        "last_name": "Sharbafian",
        "email": "[email protected]",
        "is_email_verified": true
    }
}

首先我们来处理 PhoneNumberEmail。 Codable 有一个方法 singleValueContainer 用来表示 JOSN 的这部分数据就是它本身,我们并不需要为了它而做更多的事情。用这个方法可以讲 JSON 文件中的字符串直接转换成 Model。

struct PhoneNumber: Codable {
    let digits: String
    
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
            let rawDigits = try container.decode(String.self)
        digits = rawDigits
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(digits)
    }
}

struct Email: Codable {
    let address: String
    
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let rawAddress = try container.decode(String.self)
        address = rawAddress
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(address)
    }
}

我们不需要为 Profile 实现 Codable 的方法,因为它所包含的所有类型都是 Codable 的,编译器会自动帮我们生成这部分代码。我们需要做的只是修改 CodingKey 因为 JSON 中的 key 跟我们需要的 key 不一样。

struct User {
    struct Profile: Codable {
        let name: String
        let lastName: String
        let email: Email?
        let isEmailVerified: Bool
        
        private enum CodingKeys: String, CodingKey {
            case name
            case lastName = "last_name"
            case email
            case isEmailVerified = "is_email_verified"
        }
    }
}

但是对于 User,我们还有一些很复杂的事情要做,因为在 JONS 中没有 status 这个东西。在这部分,代码就有点丑了。

struct User: Codable {
    typealias ID = String
    
    let id: ID
    let phoneNumber: PhoneNumber
    let status: Status
    
    enum Status {
        case notRegistered
        case registered(profile: Profile)
    }
    
    private enum CodingKeys: String, CodingKey {
        case id
        case phoneNumber = "phone_number"
        case isRegistered = "is_registered"
        case profile
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(ID.self, forKey: .id)
        phoneNumber = try container.decode(PhoneNumber.self, forKey: .phoneNumber)
        let isRegistered = try container.decode(Bool.self, forKey: .isRegistered)
        status = isRegistered ? try .registered(profile: container.decode(Profile.self, forKey: .profile)) : .notRegistered
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(id, forKey: .id)
        try container.encode(phoneNumber, forKey: .phoneNumber)
        switch status {
        case .notRegistered:
            try container.encode(false, forKey: .isRegistered)
        case .registered(let profile):
            try container.encode(true, forKey: .isRegistered)
            try container.encode(profile, forKey: .profile)
        }
    }
    
    
    struct Profile: Codable {
        
        let name: String
        let lastName: String
        let email: Email?
        let isEmailVerified: Bool
        
        private enum CodingKeys: String, CodingKey {
            case name
            case lastName = "last_name"
            case email
            case isEmailVerified = "is_email_verified"
        }
    }
}

现在,所有的模型都是类型安全的了,不需要知道什么额外的信息,所有的信息代码都能告诉我们了。

现在仍然能够像之前一样有 isRegisteredprofile 两个属性。这两个信息我们并不需要存起来,只需要看看 User 的实现,我们很容易就能够实现:

// MARK:- User Helper methods
extension User {
    var isRegistered: Bool {
        return status.isRegistered
    }
    
    var profile: Profile? {
        return status.profile
    }
}

// MARK:- User.Status helper methods
extension User.Status {
    fileprivate var isRegistered: Bool {
        switch self {
        case .registered: return true
        case .notRegistered: return false
        }
    }
    
    fileprivate var profile: User.Profile? {
        switch self {
        case .registered(let profile): return profile
        case .notRegistered: return nil
        }
    }
}

用户信息只是建立模型的一个非常基本的例子,这样来做,我们就可以省去将来可能会犯下的错误。

你可能感兴趣的:(建立一个类型安全的 Swift 模型)