极光日报
首发于极光日报
隔离 View 和 Model  (Swift)

隔离 View 和 Model (Swift)

简评:我们经常需要在编写 方便的代码 和 易于维护的代码 之间取得平衡,当然如果能兼顾两者是最好的。

在平衡便利性和可维护性时,往往会遇到 View 和 Model 需要建立联系的情况,如果 View 和 Model 建立太强的连接会导致代码难以重构和难以重用。

专用 View

先看一个例子,在构建应用时,我们需要创建用于显示特定类型数据的专用视图,假设我们需要在表格中显示用户列表。一个常见的方法是创建一个专门用于渲染用户数据的 UserTableViewCell,如下:

class UserTableViewCell: UITableViewCell {
    override func layoutSubviews() {
        super.layoutSubviews()

        let imageView = self.imageView!
        imageView.layer.masksToBounds = true
        imageView.layer.cornerRadius = imageView.bounds.height / 2
    }
}

我们还需要使用 User model 来填充 Cell 里面的内容,一般会添加一个 configure 方法,用于填充内容,代码如下:

extension UserTableViewCell {
    func configure(with user: User) {
        textLabel?.text = "\(user.firstName) \(user.lastName)"
        imageView?.image = user.profileImage
    }
}

上面的代码在功能上没有任何的问题,但是技术上讲,我们实际上已经将 Model 层泄露给我们的 View 层。我们的 UserTableViewCell class 不仅专门用于呈现 User 的信息(不能是其他的数据模型)而且还需要知道 User 对象的具体内容,起初,这可能不是问题,但是如果我们继续沿着这条路走,我们的 View 很容易演变成包含 app 逻辑的代码:

extension UserTableViewCell {
    func configure(with user: User) {
        textLabel?.text = "\(user.firstName) \(user.lastName)"
        imageView?.image = user.profileImage

        // Since this is where we do our model->view binding,
        // it may seem like the natural place for setting up
        // UI events and responding to them.
        if !user.isFriend {
            let addFriendButton = AddFriendButton()

            addFriendButton.closure = {
                FriendManager.shared.addUserAsFriend(user)
            }

            accessoryView = addFriendButton
        } else {
            accessoryView = nil
        }
    }
}

编写如上所示的代码可能看起来非常方便,但通常会让程序难以测试和维护。

通用 View

解决上述问题的一个办法是让 View 和 Model 之间进行严格的分离(代码层面和概念层面进行分离)。

我们再回去看看我们 UserTableViewCell 。他不再与 User 耦合,我们可以该名为 RoundedImageTableViewCell 并删除 configure 这个与 User 耦合的方法。

class RoundedImageTableViewCell: UITableViewCell {
    override func layoutSubviews() {
        super.layoutSubviews()

        let imageView = self.imageView!
        imageView.layer.masksToBounds = true
        imageView.layer.cornerRadius = imageView.bounds.height / 2
    }
}

进行上述更改的好处是,我们现在可以轻松的将 RoundedImageTableViewCell 和 其他 model 配合使用。

但是,在将我们的模型代码与我们的视图代码进行分离时,损失了便利性。之前我们可以使用 configure(with:) 方法来渲染 User,现在我们需要找一种新方法来实现这一点。

我们可以做的是创建一个专用对象来配置 View 的显示。我们来构建一个 UserTableViewCellConfigurator 类,代码如下(不同的架构实现有所不同):

class UserTableViewCellConfigurator {
    private let friendManager: FriendManager

    init(friendManager: FriendManager) {
        self.friendManager = friendManager
    }

    func configure(_ cell: UITableViewCell, forDisplaying user: User) {
        cell.textLabel?.text = "\(user.firstName) \(user.lastName)"
        cell.imageView?.image = user.profileImage

        if !user.isFriend {
            // We create a local reference to the friend manager so that
            // the button doesn't have to capture the configurator.
            let friendManager = self.friendManager

            let addFriendButton = AddFriendButton()

            addFriendButton.closure = {
                friendManager.addUserAsFriend(user)
            }

            cell.accessoryView = addFriendButton
        } else {
            cell.accessoryView = nil
        }
    }
}

这样我们能够重复使用通用 UI 代码,以及方便的在 View 中呈现 model 数据。并且我们的代码通过依赖注入可以很方便的进行测试,而不是使用单例(这部分内容可以参考这篇文章)。

无论在哪个地方想使用 User 渲染 Cell,我们都可以简单地使用我们的 configurator。

class UserListViewController: UITableViewController {
    override func tableView(_ tableView: UITableView,
                            cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        let user = users[indexPath.row]

        configurator.configure(cell, forDisplaying: user)

        return cell
    }
}

View 工厂

configurator 非常适合可复用的 View,例如 TableViewCell ,因为他们重用的时候需要不断 re-configurator(重新配置)来显示新的 model,但对于更多的 “static” View,通常只需要配置一次就够了,因为它们渲染的模型在他的生命周期内不会改变。

这种情况下,使用工程模式可能是一个不错的选择。通过这种方式,我们可以将视图创建和配置捆绑在一起,同时仍然保持 UI 代码简单(与任何 model 分离)。

假设我们想要重建一种简单的方式呈现应用消息。我们可能会有一个视图控制器来显示一条消息。以及某种形式的通知视图。当用户收到一条新消息时弹出一个 View,为了避免重复代码我们创建一个 MessageViewFactory 让我们轻松为给定的 message 创建视图。

class MessageViewFactory {
    func makeView(for message: Message) -> UIView {
        let view = TextView()

        view.titleLabel.text = message.title
        view.textLabel.text = message.text
        view.imageView.image = message.icon

        return view
    }
}

如上所示我们不仅使用通用的 TextView 来显示我们的消息(而不是专用类 MessageView),我们对外隐藏了具体的视图(我们的方法只返回 UIView)。就像在 Swift 中的代码封装 看到的一样,API 中删除具体类型可以在后期更便于修改。

原文:Preventing views from being model aware in Swift
推荐阅读: Swift 封装篇

极光日报,极光开发者旗下媒体。

每天导读三篇英文技术文章。

文章被以下专栏收录

    简介:每日导读(或翻译)三篇优质英文文章,内容 80% 涉及硅谷/编程/科技/,期待共同成长。