从MVC到MVVM:一个案例研究

在每个年轻的编码人员的生活中都有一段时间,他们被迫超越学习新技能集思考,抛弃实现框架和CocoaPods,并面临着考虑其设计模式的必要性。

与许多初学者的怀疑相反,这并不是专家编码人员在Xcode卫城上对理论进行激烈辩论的深奥阶梯。 无论您有多简单或复杂,每次开始布局项目时都会使用设计模式。 因此,某些项目需要非常简单的设计模式,也许是一个带有Storyboard的单个ViewController。 但是,随着项目的发展,有必要制定一个游戏计划:我应该在哪里初始化和存储我的对象? 他们应该如何传达观点? 定位服务,通知和网络通话如何?

背景知识:Apple推荐的设计模式是MVC,它代表Model-View-Controller。 从理论上讲,设计应该将您的代码划分为模型 (“对象封装了特定于应用程序的数据,并定义了处理和处理该数据的逻辑和计算”); 视图 (“用户可以看到的应用程序中的对象”); 和控制器 (视图对象和模型对象之间的中介)。 用苹果自己的话来说:

理想情况下,模型对象应该与显示其数据并允许用户编辑该数据的视图对象没有显式连接-不应与用户界面和表示问题有关。

就设计模式而言,MVC是一种很好的方法。 这很简单明了,将您的Model和Views分割为项目中的不同区域,同时始终通过ViewControllers连接它们。 几乎没有重叠,每个领域都承担着各自不同的责任-或理论认为。 实际上,有时候,MVC的一个相当普遍的问题是过分强调控制器。 根据您的项目,您的ViewController可能被迫采用多种协议,处理大量方法,同时保持视图与数据之间的清晰通信。 最终,这就是我看似简单的应用程序所发生的事情,并且是本案例研究的主题,该案例研究将从Model-View-Controller重构为Model-View-ViewModel。

我着手建立的项目称为TrainNapper:这是一个基于位置的闹钟,当您到达目的地火车站时会叫醒您。 当我说“看似简单的应用程序”时,我的意思是:主要部分是JSON序列化程序,用于解析LIRR / MetroNorth / NJTransit数据,该数据依次初始化了Stations字典([“ Name”:Station],其中Stations包含名称,CLLocation等),然后将其绘制到地图上。 除了工作站之外,唯一的另一个对象是Napper的单个实例(仅由CLLocation和目的地[Station]组成)。 一个Napper对象,一个Station数据存储,一个MapView,当然还有一个ViewController! 这个项目将变得轻而易举

你知道吗? 它是! JSON已序列化,Stations已初始化,Map已填充。 视图很干净,对象很原始,只需要向Napper添加一些功能即可。

由于它存在于ViewController中,因此有必要在其中添加方法,从tapToAddRemoveStation()开始。 可能还会添加一个过滤器按钮,以免淹没用户showHideTrainBranches()。 还不错! 但是现在我们必须跟踪Napper并初始化LocationManager,这意味着ViewController也必须采用所有这些委托:LocationManagerDelegate的didChangeAuthorization(),didUpdateLocation(),didEnterRegion(),didFailWithError()。

好的,但是我们仍然必须包括UNUserNotifications并在接收到它们时进行处理……并且应该有一个TableView来查看所有警报,因此这是几个其他的委托方法……而就在这时,事情开始变得越来越多了。有点笨拙。

我将为您节省代码段,但最后,此“功能非常强大”的ViewController最终总共约有400行代码。 我该如何向所有人展示此内容,并希望他们了解每个部分的交互方式? 此外,如果我在几个月后重新访问该项目,我是否还能理解我的想法? 大概是,但即使如此,设计模式的一个非常重要的方面是调试能力。 由于ViewController承担着如此多的责任,因此确定代码的哪些部分有问题并影响其他区域变得更加困难。

最终面对MVC的局限性之后,我开始探索替代设计模式,学习它们的方式,优点和缺点,并最终与志趣相投的好奇的编码人员分享这些发现。 我们走吧!

需要明确的是,没有“最佳”设计模式。 每个都特定于项目的实际布局和目标。 实际上,MVVM不会真正冲突或否定MVC。 如果首字母缩写词更准确,则它将被称为MV-VM-C,因为控制器仍然存在,尽管其作用已大大减少。 在研究了MVVM的准则之后,很明显,当前的目标是拆除ViewController中的几乎所有功能,以达到其主要目的只是显示视图和初始化对象的目的。

第一个目标:拆除ViewController

我的出发点是解决我创建的Massive-View-Controller。 它本身已经变得太大了,它采用了许多协议和各种Model-View连接方法。 但是,拆除它需要一些工作和远见。 最终的目标是划分不同的方法,并让ViewController承担尽可能少的责任。 使用MVVM,ViewController本质上只是呈现视图的工具,并且几乎发挥了零功能。 最后,您的ViewController应该是一台时尚,笨拙的机器; 没有与数据相关的任务,只需显示UI并初始化所需的内容即可。

第二目标:分而治之

确定了问题之后,下一步的逻辑就是解决它。 我的方法是查看ViewController中的所有方法,并根据是将它们应用于视图,Station数据还是Napper实例来创建逻辑的独立类。

最后,我决定分解方法的最佳方法是将其组织为与Stations数据(MapViewModel)相关的内容,与Napper的目的地/警报(DestinationViewModel)相关的内容以及与Napper的位置(LocationViewModel)相关的内容)。 尽管这听起来还很抽象,但希望在描述每个ViewModel时,您将更好地了解MVVM的实现方式。

第三目标:连接点

让我们从该项目的主要(基本上是唯一的)视图开始:MapView。 从理论上讲,此UIView类应仅处理与用户将看到的内容直接相关的方法。 因此,MapView应该负责显示GoogleMap标记,具体取决于它们是否已被过滤或选择。 但是,如果MapView仅负责与视图相关的方法,而不负责数据,那么它将如何知道要添加到地图的站点? 简单-通过创建和采用AddToMapDelegate! 由于MapViewModel负责Station数据,因此每当调用此委托方法时,信息就会传递到MapView,说明应该显示哪个Station。 这样,我就可以将MapViewModel中Station数据中的信息中继到MapView中的演示文稿,而不必承担任何责任。

MapViewModel本身非常简单,并且与MapView有着千丝万缕的联系。 例如,如果在MapView中触摸了FilterButton,则ButtonLabel信息会通过另一个FilterDelegate发送到ViewModel,该FilterDelegate会将特定站点从isHidden = false切换为true。 然后将这些站发送回MapView,以重新加载到地图上。 这样,View和ViewModel可以有效地通信显示哪些工作站和隐藏哪些工作站,同时始终将视图和数据的职责划分为两个单独的类。

我从此练习中学到的一件事是,在组织MVVM项目时,需要考虑的重要方面是在哪里采用您的代表。 不仅是为了方便中继信息而使用的,而且还包括例如GMSMapViewDelegate或UITableViewDelegate / DataSource之类的委托。 由于TableView依赖于一组数据,这些是否属于ViewModel的责任? 还是在地图上点击MarkerWindow并将Station添加到Napper的目标数组中,并且Station也更改为isSelected = true时,委托方法didTapInfoWindowOf_Marker()应该存储在哪里?

经过仔细考虑,采用这些委托方法的最明智的地方实际上是在它们出现的视图之内。 例如,如果您有GoogleMap GMSMapView,则在该视图中将所有相关方法包含在同一个类中是最有意义的,然后在以后找到必要时将与数据相关的方法分开的方法。 这样,MapView便是完整无缺且独立的,并且所有构建属性都不会散布在整个项目中。 与UITableViews相同; 在出现的视图中格式化TableView的格式,并找到最合适的方法将数据传递到该视图中。 这不仅使调试更加容易,因为您将确切地知道TableView的每个部分的位置,而且还可以帮助您组织项目,因为它知道与视图相关的方法和委托位于该View中,并且信息可以从另一个分区区域,可能通过ViewModel,数据存储区或单例。

处理完MapView和随附的ViewModel之后,下一个任务是分解与我的Napper单个实例有关的方法。 回想一下,Napper实际上只是两个组成部分:CLLocation? 及其桩号数组(他们自己的坐标将用作基于位置的通知)。 因此,我决定制作两个ViewModels-一个用于处理基于位置的任务,另一个用于处理通知。

我要说的是,将这两个代表分开可以使调试和一般组织变得更加愉快。 以前,当ViewController处理LocationManagerDelegate和UNUserNotifications时,现在可以在它们自己的特定ViewModel中专门找到与这两个协议相关的任何内容。 可以说,LocationViewModel仅负责更新Napper的位置。 DestinationViewModel通过NapperAlarmDelegate与MapView进行通信,以了解要从Napper的目标数组中添加或删除哪个Station; 这是通过使用MarkerWindow的Label并访问StationsDictionary [“ name”:Station]的数据存储区的sharedInstance()来完成的。 一旦在DestinationViewModel中标识了该Station,剩下的就是建立一个基于区域的通知,并开始监视Napper的位置何时接近其阵列中最近的Station。 瞧!

第四个目标:重新访问新的ViewController

通过这个博客已经走了很长一段路程,我赞扬到目前为止取得成功的任何人! 您说什么奖? 好吧,除了我谦卑的谢意,让我们来看看新的ViewController吧!

几乎无法识别! 仿佛一个沉重的负担已经从肩膀上解脱了,现在正飞向高高的天堂!

这里我们有所有Views和ViewModels的实例,但是没有多余的方法。 每个班级都知道自己的作用。 它们本质上是独立的实体,其目的是单一的,但彼此之间却相互依存,共同协作并产生功能齐全的应用程序。

这就是MVVM设计模式的本质。 我希望这个例子可以帮助您更好地了解是否以及何时考虑对自己的项目应用类似的方法。 如果您真的很冒险,我将在此处放置此项目的Github存储库。 您仍然可以在“广告”分支中看到从MVC一直到其当前状态的过渡。 (请原谅多余的方法和注释,我还没有时间整理一下!)

尝试新的设计模式,玩得开心! 继续探索Swift宇宙的无限可能性! 最重要的是,编码愉快!