展示模型

让我们看一下MVC中的一个常见挑战,以及一个已有30年历史的解决方案。 我们将其与MVVM进行对比,然后进行权衡。

我们将以“ 邮件”收件箱为例。

前言:什么是MVC?

如果您听到以下内容,请阻止我:

MVC太糟糕了! 我们有一个庞大的视图控制器,具有网络,集合视图布局和图像缓存。 我们离开了MVC,一切都已修复!

这不是针对MVC的罢工,而是我们的失败教训。 当控制器包含网络,存储或布局代码时,它不是MVC。 是“景观和泥浆球”。

该视图与演示有关,包括图形,布局和动画。 该模型涉及业务规则,包括与您的后端交谈。 控制器主要是位于视图和模型之间的胶水。 它设置场景,然后解释任一侧的事件。

在尝试新的设计模式之前,我们先弄清楚基础知识。

我们的MVC设计

  • 模型:我们从MessageStore开始。 它具有fetchAllConversations()方法,该方法返回一个Conversation对象数组。 每个线程包含一个Message对象数组。
  • 视图:具有MessageCell表视图单元格的表视图。
  • 控制器:表格视图的数据源。

在幕后,当收到新消息时, MessageStore会收到推送通知,并检查REST端点是否有新消息。 或不。 它是模型背后抽象的实现细节。

我们要求收件箱中的每个对话都必须显示最新消息的预览。 当有新消息到达对话时,它会更新。 让我们逐步完成流程。

MessageStore新消息时, MessageStore通知我们的控制器。 我们可以使用委托或NSNotificationCenter ,但这实际上取决于您。

 类InboxViewController:UIViewController,UITableViewDataSource { 
var对话:[对话] = []
var messageStore:MessageStore!
var tableView:UITableView!
  func sessionsDidUpdate(_ notification:Notification){ 
对话= messageStore.fetchAllConversations()
tableView.reloadData()
}
 覆盖viewDidLoad(){ 
super.viewDidLoad()
//设置订户等
}
}

现在让我们解决常见问题。

问题1:复杂的显示规则

让我们添加一些有关数据显示方式的规则:

  • 在该消息预览中,如果最新消息是照片,则显示“ 附件:1张图像”。 否则,显示消息文本。
  • 如果时间戳记是今天,则显示时间,例如“ 10:00 am”。 如果是今天之前,请显示日期,例如“ 昨天”

“嗯。 我们正在描述显示,为什么不将其扔到 MessageCell 呢?”

几个月后,您的公司要求一个iPad应用程序具有完全不同的单元设计。 为了共享这些显示规则,您是否将MessageCell分为PhoneMessageCellPadMessageCell

然后,您的公司要求提供本机Mac应用程序。 (嘿,这是幻想。)MacOS不使用UITableViewCell ,因此您不能只创建MacCell子类。

您可以在控制器中使用此逻辑,但是我们回到违反MVC的原则,此外,Mac上没有UIViewController

问题2:显示状态

然后是搜索框。 我们需要跟踪搜索字符串,但是将UITextFieldtext属性视为事实的来源是一个坏主意。 例如,出于性能原因,我们可能会回收视图,并且每个iOS开发人员都知道单元回收错误的痛苦。

您可以在视图控制器中跟踪此状态,但是它引入了较早的紧密耦合。

问题3:测试

假设您要测试邮件搜索,以确保它不区分大小写。 通过UIViewController内部的业务逻辑,您需要在测试工具中包含大量的依赖项。

除了放慢测试套件的速度之外,当MessageCell发生另一个不相关的故障时,搜索测试可能会中断。 级联的测试失败使更难追踪精确破坏的内容。

介绍演示模型

早在1988年,Smalltalk开发人员就意识到应用程序中实际上有两种类型的模型:处理业务逻辑的域模型和处理其在屏幕上表示方式的应用程序模型 。 后者有点模棱两可,所以我更喜欢Martin Fowler 2004年的品牌重塑,一种Presentation Model

例如, Message是一个高级概念,属于我们的域模型。 时间戳格式规则和跟踪搜索文本属于我们的演示模型。

为了清楚起见,我将添加一个Presentation后缀。

  class ConversationPresentation { 
私人var对话:对话
初始化(_对话:会话){
自我对话=对话
}
  var PreviewText:String { 
警卫让lastMessage = session.messages.last else {
返回“”
}
如果lastMessage.isPhoto {
返回“附件:1张图片”
}其他{
返回lastMessage.text
}
}
  } 

它封装了Conversation并公开了previewText属性以包装格式设置规则。

与其将其与Conversation绑定,不如创建一个ConversationPresentable协议。 在我们的测试套件中,我们将其传递给FakeConversation以扩展所有规则。

我们还可以将搜索状态InboxPresentationInboxPresentation对象中。 让我们从一些重构开始。

  InboxPresentation类{ 
私人var store:MessageStore
var个对话: [ConversationPresentation] = []
 初始化(_ store:MessageStore){ 
self.store =商店
self.refreshConversations()
self.subscribeToNotifications()
}
 私人函式refreshConversations(){ 
self.conversations = store.conversations.map {转换为
返回ConversationPresentation(conv)
}
postInboxNotification()
}
  func messageStoreUpdated(__ notification:Notification){ 
self.refreshConversations()
}
  } 

为简便起见,我省略了通知中心的详细信息。 简而言之,我们的视图控制器订阅了此收件箱对象。 邮件存储更新时,我们的收件箱将刷新其conversations数组。

现在我们可以添加搜索行为:

  var searchText:String?  { 
didSet {
refreshConversations()
}
}
 私人函式refreshConversations(){ 
让filteredPresentations:Conversation
如果让searchText = self.searchText {
FilteredPresentations = store.conversations.filter {转换为
返回conv.bodyContains(searchText)
}
}其他{
filteredPresentations = store.conversations
}
  self.conversations = filteredPresentations.map {转换为 
对话表达(转换)
}
  postInboxNotification() 
}

我们的视图控制器只是读取:

  func searchFieldDidUpdate(_ sender:Any){ 
self.inbox.searchText = self.searchField.text
}

除了清理视图控制器之外,我们还隐藏了有关过滤消息时所涉及实体的详细信息。 稍后,我们可以重构事物以直接对数据库进行单个SQL调用。

请注意,演示模型与您可能听到的演示者不同。 从技术上讲,Apple的MVC是Model-View-Presenter模式。 不管出于什么原因,苹果公司都将其更名。 如果看到Presenter,则认为是Controller 。 如果您看到Presentation Model,那就是我们刚刚讨论的事情。

严格来说,福勒的演示模型会接收所有事件。 如果您的视图控制器是视图的委托,则您不会使用他的Presentation Model。 我认为这是一个很小的区别。 即使在MVC中,您也可以在“被动视图”和“监督控制器”之间找到一个范围。我认为还有很多术语可以避免……

MVVM呢?

如果您阅读过多的iOS博客,那么现在您的意思是:“这听起来很像MVVM。”人们一直在说MVVM,但我认为这并不意味着他们认为的含义。 如果您对软件人类学感兴趣,请继续阅读。 否则,最后跳到“权衡”。

福勒悬而未决:

您必须在Presentation Model中进行同步的一个特殊决定是哪个类应包含同步代码。

在2005年,Microsoft接受了Fowler的想法并付诸实践。 他们将MVVM编码为一个宏伟的目标:

[MVVM]专为现代UI开发平台量身定制,在该平台中,视图是设计者而非经典开发者的责任。 设计师通常是更具图形,艺术专心的人,与传统开发人员相比,经典代码的编写较少。 设计几乎总是以声明性形式(例如HTML或XAML)完成,并且经常使用诸如Dreamweaver,Flash或Sparkle之类的WYSIWYG工具进行设计。

Microsoft承认它受到了表示模型​​的启发:

它看起来惊人地类似于Presentation Model模式…实际上,唯一的区别是WPF和Silverlight的数据绑定功能的显式使用。

他们以为工程师只会将ViewModel扔给他们的设计师,而设计师可以在不编写任何代码的情况下围绕它构建GUI。 他们通过MVVM的最大定义特征来完成此任务: 数据绑定

但是,微软对MVVM的营销远不只是“一种利用绑定的有用设计模式。”微软出售MVVM作为其替代MVC的平台:

MVVM模式在某些方面类似于Model-View-Presenter(MVP)模式…两种模式都是Model-View-Controller(MVC)模式的变体,都是分离的展示模式,并且都设计为隔离细节从底层业务逻辑的用户界面,以增强可管理性和可测试性。

这将我们带到了MVVM的第二大特点:ViewModel 代替了Controller。 它是MVVM,而不是MVMVC。

视图模型还提供了应用程序的用户在视图中启动的命令的实现。

然后是太多的iOS开发人员误解了MVVM。 大多数iOS示例不使用绑定-考虑到iOS不提供绑定,这是有道理的。

在许多示例中,视图控制器仍然充当事件的中心枢纽。 这是有道理的,因为只有控制器才能获取基本事件,例如viewDidAppearviewDidAppear事件。 没有逃脱的MVC。

大多数人说MVVM的时候,他们实际上是在谈论更接近于Presentation Model的事物。 “ iOS MVVM”是一种反映。 是WaLuigi。

这些语义重要吗?

“人们真正表示表示模型时说MVVM是否重要? 甚至苹果公司也称MVC为真正的MVP。”

如果您的团队已将您的演示模型称为“视图模型”,则不值得重命名所有内容。 就我而言,将其称为杯形蛋糕模型。

但是,正如您在我重复的“苹果的MVC”中所注意到的那样,当有人选择现有术语时,这会造成混淆。 不幸的是,坚持要求苹果在其代码UIViewPresenter UIViewController重命名为UIViewPresenter是很疯狂的。 但是这艘船还没有驶向您的团队。

我知道这是自行车脱落,但是如果我需要为这些类型的型号添加后缀,则倾向于“展示”。 为什么?

  • ViewModel的重载含义给Google结果带来了影响
  • 演示文稿描述了对象的职责。 见经理
  • 这是一件很小的事情,但是“快速打开”所产生的“ pres”结果要少于“ view”结果。

权衡取舍

需要强调的是,表示模型不是新的体系结构。 不要将其粘贴在每个项目中,并说您正在练习MPVC。

考虑一个待办事项列表应用程序。 您有一个Task对象,并决定添加TaskPresentation.

  class TaskPresentation { 
var task:Task
init(_ task:Task){
self.task =任务
}
  var text:String { 
返回self.task.text
}
  var Important:布尔{ 
返回self.task.important
}
}

您最终会获得很多没有附加值的样板。 Swift可以提供工具来简化此过程吗? 我们不要去那里。

接下来,我们遇到诸如“ unread属性将属于表示层或域层吗?”这样的问题,请当心,否则最终结果将是随机分布在两个位置之间的行为。 这可能比原始问题更严重。

最后,有句老话:“计算机科学中的所有问题都可以通过另一层间接解决,除非存在太多的间接层。”

在前面的示例中,我们有ConversationPresentation ,但是如果为对话屏幕添加另一个演示模型,该怎么办? 我们最终将获得ConversationInboxPresentationConversationDetailPresentation 。 这听起来像企业软件。

在真实的项目中,从一组简单的实体开始。 不要只介绍演示模型来包装几个 陈述。 任何说“一切都必须经过考验”的人都生活在幻想中。 但是,这是主观的。