注册

Swift高级分享 - 在Swift中构建模型数据

在代码库中建立可靠的结构通常是必不可少的,以便更容易使用。然而,实现一个既足够严格以防止错误和问题的结构 - 以及对现有功能足够灵活的结构以及我们想要的任何未来变化 - 都可能非常棘手。

对于模型代码而言尤其如此,模型代码通常由许多不同的功能使用,每个功能都有自己的一组要求。本周,让我们来看看构建核心模型的数据的几种不同技术,以及如何改进该结构对我们的其余代码库产生重大积极影响。

形成层次结构

在项目开始时,模型通常可以保持非常简单。由于我们尚未实现许多功能,因此我们的模型很可能不需要包含太多数据。然而,随着我们的代码库的增长,我们的模型经常发生变化 - 并且很容易达到一个简单的模型最终成为各种相关数据的“全能”的程度。

例如,假设我们正在构建一个电子邮件客户端,它使用Message模型来跟踪每条消息。最初,该模型可能只包含给定消息的主题行和正文,但此后逐渐增长为包含各种其他数据:

struct Message {
var subject: String
var body: String
let date: Date
var tags: [Tag]
var replySent: Bool
let senderName: String
let senderImage: UIImage?
let senderAddress: String
}

虽然为了呈现消息需要所有上述数据,但是直接将其保留在Message类型本身中会使事情变得有点混乱 - 并且很可能使消息更难以使用,尤其是当我们创建新实例时 - 撰写新邮件时或编写单元测试时。

缓解上述问题的一种方法是将数据分解为多个专用类型 - 然后我们可以使用它们来形成模型层次结构。例如,我们可能会将有关消息发送者的所有数据提取到Person结构中,并将所有元数据(例如消息的标记和日期)提取到Metadata类型中,如下所示:

struct Person {
var name: String
var image: UIImage?
var address: String
}

extension Message {
struct Metadata {
let date: Date
var tags: [Tag]
var replySent: Bool
}
}

现在,有了上述内容,我们可以为我们的Message类型提供一个更清晰的结构 - 因为每个数据不直接作为消息本身的一部分现在包含在更具上下文的专用类型中:

struct Message {
var subject: String
var body: String
var metadata: Metadata
let sender: Person
}

上述方法的另一个好处是,我们现在可以更容易地在不同的上下文中重用部分数据。例如,我们可以使用我们的新Person类型来实现联系人列表等功能,或者允许用户定义组 - 因为该数据不再直接绑定到该Message类型。

减少重复

除了用于更好地组织我们的代码之外,可靠的结构还可以帮助减少项目中的重复。假设我们的电子邮件应用程序使用事件驱动的方法来处理不同的用户操作 - 使用如下所示的Event枚举:

enum Event {
case add(Message)
case update(Message)
case delete(Message)
case move(Message, to: Folder)
}

使用枚举来定义各种代码需要处理的有限事件列表,这是在应用程序中建立更清晰数据流的好方法 - 但是我们当前的实现要求每个案例都包含Message事件所针对的事件 - 领先在Event类型本身内复制,以及在我们想要从事件的消息中提取信息时。

由于每个事件的操作都是对消息执行的,所以让我们将两者分开,并创建一个更简单的枚举类型,它将包含我们的所有操作:

enum Action {
case add
case update
case delete
case move(to: Folder)
}

然后,让我们再次形成一个层次结构 - 这一次通过重构我们的Event类型成为一个包含a Action和Message它将被应用于的包装器- 如下所示:

struct Event {
let message: Message
let action: Action
}

上述方法为我们提供了两全其美 - 处理事件现在只需要切换事件Action,现在可以使用message属性直接从事件的消息中提取数据。

递归结构

到目前为止,我们已经形成了层次结构,其中每个孩子和父母都是完全独立的类型 - 但这并不总是最优雅,或最方便的解决方案。假设我们正在开发一个显示各种内容的应用程序,例如文本和图像,并且我们再次使用枚举来定义每个内容 - 如下所示:

enum Content {
case text(String)
case image(UIImage)
case video(Video)
}

现在让我们说我们希望让用户能够形成一组内容 - 例如,通过创建收藏列表,或使用文件夹来组织内容。最初的想法可能是寻找一个专用Group类型,它包含组的名称和属于它的内容:

struct Group {
var name: String
var content: [Content]
}

然而,尽管上述内容看起来优雅且结构合理,但在这种情况下它有一些缺点。通过引入一种新的专用类型,我们将需要单独处理各个内容组 - 使得构建列表之类的内容变得更加困难 - 而且我们也无法轻松支持嵌套组。

因为在这种情况下,一个组只不过是构造内容的另一种方式,所以让它改为Content枚举本身的第一类成员,只需为它添加一个新的例子 - 就像这样:

enum Content {
case text(String)
case image(UIImage)
case video(Video)
case group(name: String, content: [Content])
}

我们上面基本上做的是创建Content一个递归数据结构。这种方法的优点在于我们现在可以重用我们用于处理内容的大部分相同代码来处理组,并且我们可以自动支持任意数量的嵌套组。

例如,以下是我们如何处理显示内容列表的表视图的单元格选择:

extension ListViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let content = contentList[indexPath.row]

switch content {
case .text(let string):
navigator.showText(string)
case .image(let image):
navigator.showImage(image)
case .video(let video):
navigator.openPlayer(for: video)
case .group(let name, let content):
navigator.openList(withTitle: name, content: content)
}
}
}

上面我们使用导航器模式导航到新目的地。您可以在“Swift中的导航”中找到更多相关信息。

由于Content现在是递归的,因此navigator.openList在处理组时调用现在只需创建一个ListViewController包含该组内容列表的新实例,使用户能够轻松地创建和导航任何内容层次结构,而我们只需要很少的努力。

专业模特

虽然能够重用代码通常是件好事,但有时最好创建一个更专业的新版本的模型,而不是尝试在非常不同的上下文中重用它。

回到之前的电子邮件应用程序示例,假设我们希望用户能够保存部分撰写的邮件草稿。而不是让该功能处理完整的Message实例,这需要不能用于草稿的数据 - 例如发件人的姓名或收到邮件的日期 - 让我们创建一个更简单的Draft类型,我们将嵌套在Message其他上下文中:

extension Message {
struct Draft {
var subject: String?
var body: String?
var recipients: [Person]
}
}

这样,我们可以自由地将某些属性作为选项,并减少加载和保存草稿时我们需要处理的数据量 - 而不会影响我们处理正确消息的任何代码。

结论

虽然哪种模型结构最适合每种情况,但在很大程度上取决于所需的数据类型以及数据的使用方式 - 在能够重用代码和不创建模型之间取得平衡太复杂,往往是关键。

形成清晰的层次结构 - 无论是使用专用类型还是通过创建递归数据结构 - 同时仍然偶尔为特定用例创建模型的专用版本,可以在我们的模型代码中形成更清晰的结构 - 并且像往常一样,常量重构和小改进通常是达到目的的方式。

链接:https://www.jianshu.com/p/06e7d171dd99

0 个评论

要回复文章请先登录注册