为什么模型对象不应该实现Swift的Decodable或Encodable协议

  • Post author:
  • Post category:其他


到目前为止,您可能在想:“他在说什么?

Decodable



Encodable

协议非常有用!”

我也同意你的看法。在

Decodable



Encodable

协议确实很有用。例如,Swift提供了一种本地方法来解析JSON元素或从

User Defaults

存储和检索对象,这是很棒的。没有什么问题。

但是,我认为我们在模型对象中使用这些协议会犯错。我将尝试解释原因。




领域模型和数据模型


领域模型(Domain Model)

是一种面向对象的模型,该模型合并了行为和数据。它代表了我们试图建模的业务规则。


数据模型(Data Model)

是持久性存储中的数据结构。它没有任何行为。

持久性存储的一些例子是

User Defaults



Core Data

,文件,数据库,甚至是外部API。这些存储中的每个存储中的数据模型都可能不同。

领域模型和数据模型都包含数据,但是领域模型也包含业务规则。

领域模型中的对象应该不知道使用哪个持久性存储或数据模型。

这是因为领域模型和数据模型有不同的原因而改变。仅当执行业务规则或获得更多关于解决问题的见解时,领域模型才应更改。

另一方面,数据模型可能由于不同的原因而改变。例如,持久性存储需要从本地存储更改为远程API。领域模型不应受此基础结构更改的影响。




Decodable和Encodable


Decodable

协议用于对来自某些外部表示的对象进行

转化

。例如,它用于将JSON对象解析为结构体或类。


Decodable:一种可以从外部表示形式进行解码的类型。

另一方面,该

Encodable

协议用于将对象存储到某个外部表示中。例如,它可用于获取对象的JSON表示形式。


Encodable:可以将自身编码为外部表示形式的类型。

但是,为什么我们不可以使用

Decodable



Encodable

在我们的领域模型对象?

让我们用一个例子来回答这个问题。假设我们具有以下用户的JSON表示形式:

{ 
   “ first_name”:“ dick”,
   “ last_name”:“ richardson”,
   “ mail”:“ drichardson@enclave.com”,
   “ day_of_birth”:7026198103 
}

我们使用一个名为

User



Decodable

结构体解析JSON,并在我们的领域模型中表示一个

User

struct User: Decodable {

  let firstName: String
  let lastName: String
  let email: String
  let dayOfBirth: Int
}

但是,如果JSON发生变化会怎样?假设现在

first



last

名称位于一个

name

字段中:

{
   “ name”:{first”:“ dick”,
      “ last”:“ richardson”
   },
   “ email”:“ drichardson@enclave.com”,
   “ day_of_birth”:7026198103 
}

由于此较小的更改,以前的

User

结构现在无法解析JSON数据。我们被迫更改领域模型以解析新的数据模型:

struct User: Decodable {
  
  let name: Name
  let email: String
  let dayOfBirth: Int
  
  struct Name: Decodable {
    
    let first: String
    let last: String
  }
}

好。现在,该

User

结构体解析新的JSON格式,但是我们必须更改

firstName

and

lastName

的所有用法,分别将它们替换为

name.first



name.last

由于数据的更改,我们刚刚更改了领域模型。

这就是我不在我的领域模型对象中使用

Decodable



Encodable

的原因。




将领域模型与数据模型分开

我们需要做的是将领域模型与数据模型分离。

我们可以通过使用两个不同的类或结构体来实现。一个解析JSON,另一个代表领域模型对象。

struct User {

  let firstName: String
  let lastName: String
  let email: String
  let dateOfBirth: Date
}
struct UserDTO: Decodable {

  let name: NameDTO
  let email: String
  let dateOfBirth: Int
  
  struct NameDTO: Decodable {
    
    let first: String
    let last: String
  }
}
struct UserDTOMapper {
  
  static func map(_ dto: UserDTO) -> User {
    
    return User(firstName: dto.name.first, 
                lastName: dto.name.last, 
                dateOfBirth: Date(timeIntervalSince1970: dto.dateOfBirth))
  }
}

请注意,

User



Decodable

协议不再实现,因为它不用于解析JSON数据。

User

现在表示领域模型,并与数据模型分离。

我们创建了一个名为

UserDTO

(数据传输对象)的

Decodable

结构体,用于解析JSON数据。此结构体包含创建

User

所需的数据 。

最后,一个

UserDTOMapper



UserDTO

数据创建一个新的

User




优点

由于这种方法,领域模型不再与数据耦合,并且不需要每次数据模型更改时都进行更改。

当然,领域模型并非不受所有数据更改的影响。有时,模型仍然会改变。

在这种情况下,问问自己:“是迫使领域模型发生变化的数据还是其他?” 可能是业务规则发生了变化,领域模型也发生了变化,从而导致数据模型发生了变化。

将领域模型与数据分离的另一个优势是,领域模型变得更具表现力。我们可以用更

复杂的

类型,而不是只是普通的

String



Int

或其他

Decodable

类型。

在前面的示例中,现在的出生日期 在

User

结构体中以

Date

表示,与在

UserDTO

结构体中将出生日期表示为

Int

不同 。那些更复杂的类型可以在映射过程中创建。




Repositories仓库

既然我们知道了将域模型与数据分离的价值,我想介绍一个可以帮助我们实现目标的概念:

repository仓库

可以将repository仓库视为元素的集合,可以在其中存储或检索它们。它提供了获取和存储这些元素的方法。

它是领域模型和数据模型之间的边界。这是一个

隐藏

使用的真正持久性存储及其所有实现细节的好地方,例如JSON解析和映射到领域模型对象。



示例

让我们来看一个例子:

// 1
protocol UserRepository {
  func getUser(completion: @escaping ((User?) -> Void))
}

// 2
class APIUserRepository: UserRepository {
    
  func getUser(completion: @escaping ((User?) -> Void)) {

    let session = URLSession.shared
    let request = createRequest()

    // 3
    let dataTask = session.dataTask(with: request) { data, response, error in
                                                    
      if let data = data {
        completion(createUser(from: data))
      }
      else {
        completion(nil)
      }
    }

    dataTask.resume()
  }

  private func createRequest() -> URLRequest {
    // Create the request
  }
  
  // 4
  private func createUser(from data: Data) -> User? {

      let decoder = JSONDecoder()
      decoder.keyDecodingStrategy = .convertFromSnakeCase

      guard let userDTO = try? decoder.decode(UserDTO.self, from: data) else {
        return nil
      }

      return UserDTOMapper.map(userDTO)
  }
}

这里发生了什么?

  1. 仓库协议。使用协议是一个好主意,因为通过这种方式,可以使用

    依赖注入

    轻松地更改实际仓库的实现。名为

    UserRepository

    。该名称不应告诉我们有关所使用的持久性存储的任何信息。
  2. 仓库实现。与协议不同,类的名称应为我们提供有关所选持久性存储的线索。在这种情况下,

    APIUserRepository

    使用外部API取回

    Users

  3. 仓库用于

    URLSession

    执行请求并获取

    User

    。我不想在这里进一步探讨细节,因为我不想错过这个例子的重点。如果您想了解有关网络使用的

    URLSession

    更多信息 ,可以

    在这里

    看到一个很好的教程。

  4. UserDTO

    结构体用于解析从API获得的JSON数据。如果将数据成功解析到DTO中,则

    UserDTOMapper

    从中创建一个

    User

    。如果解析失败,

    nil

    则返回。

就这样。很简单,对吧?

使用仓库时,非常容易更改使用的持久性存储。让我们这样做并将用户存储在本地。顾名思义,此新实现使用

User Defaults

来取回用户:

class UserDefaultsUserRepository: UserRepository {

  private let userDefaults = UserDefaults.standard
  private let userKey = "userKey"
  
  func getUser(completion: @escaping ((User?) -> Void)) {
    
    if let data = userDefaults.value(forKey: userKey) as? Data {
      completion(createUser(from: data))
    }
    else {
      completion(nil)
    }
  }

  private func createUser(from data: Data) -> User? {

    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase

    guard let userDTO = try? decoder.decode(UserDTO.self, from: data) else {
      return nil
    }

    return UserDTOMapper.map(userDTO)
  }
}

请注意,此实现中使用的DTO与

APIUserRepository

中使用的相同 。当然,这不是强制性的。

每个实现都可以使用适合仓库需求的不同DTO。但是为了使示例简单,我使用了相同的。




结论

关于仓库的一件好事是,您可以将所有实现细节都隐藏在协议背后。

占用仓库的对象不必关心真正使用了哪种机制。它只关心仓库返回领域模型对象( 在此示例中为

User

)。

而且,由于我们已经将领域模型与数据模型解耦,因此仓库实现可以更改,并且对系统的影响最小,因为返回的领域模型将保持不变。



版权声明:本文为u010001360原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。