UserProfile
每个用户都有 name
、 firstName
、 lastName
有些用户还有 email
、phoneNumber
。模型可能会是这样:
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 还是有一点区别的。
不知道你有没有发现 TimeInterval
、CLLocationDegrees
这个两个类型,实际上都是 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
还有 UserProfile
在 User
之外没有任何意义。所以再把代码整理一下:
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
}
}
首先我们来处理 PhoneNumber
和 Email
。 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"
}
}
}
现在,所有的模型都是类型安全的了,不需要知道什么额外的信息,所有的信息代码都能告诉我们了。
现在仍然能够像之前一样有 isRegistered
和 profile
两个属性。这两个信息我们并不需要存起来,只需要看看 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
}
}
}
用户信息只是建立模型的一个非常基本的例子,这样来做,我们就可以省去将来可能会犯下的错误。