当前位置: 首页 > 科技观察

一次高效简单的代码组织——简单的ViewModel实践

时间:2023-03-15 23:02:57 科技观察

前言不知不觉中,作者已经码字一年多了。随着代码数量的激增,如何高效、简洁地组织代码常常引起作者的思考。作为一种方法论及其实践者(这个定义是作者自创的),我一直希望能找到一些简单有效的方法来解决问题,于是,我也开始了构建代码的实践经验。这次想分享的是我在长期实践MVVM架构后对MVVM框架的理解和自己的工作流程。可能还有一些地方没有处理好。希望大家多多交流。基于MVVM结构提出前戏ViewModel的概念。全称应该叫Model-View-ViewModel。从结构上来说,应该是Model-ViewModel-ViewController-View。简单来说,就是在MVC结构的基础上,将ViewController中与数据相关的功能分离出来,形成一个单独的结构层次。ViewModel的详细定义可以参考这篇MVVM介绍。另外,在工作流程上,作者一定程度上参考了BDD的代码构建思路。虽然没有真正意义上的根据行为来构建测试代码,但是编写过程确实和BDD有相似之处。BDD可以参考这个Behavior-driventest。本文写的demo已经上传到Github:传送门~好了,开始吧。ViewModel和ViewController基类那么,在这里,您需要使用经典的OOP继承模式。我们不打算过多地构建ViewModel的功能,因此它只需要一个指向其自己的ViewController的指针,以及一个用于分配ViewController的工厂方法。就像下面的代码://BCBaseViewModel.h@interfaceBCBaseViewModel:NSObject@property(nonatomic,weak,readonly)UIViewController*viewController;+(BCBaseViewModel*)modelWithViewController:(UIViewController*)viewController;@endViewModel只需要一个弱类型的viewController指针指向自己的viewController,viewModel由viewController使用强指针持有,避免循环引用。这样,就够了。为了让ViewModel和ViewController的关系更加清晰,并且能够批量生产ViewModel,delegator和agent需要定义ViewModel和ViewController的结构特征。分析了ViewModel划分的原因和主要功能,我们大致可以总结出以下特点:ViewModel和ViewController是一一对应的,ViewModel实现的功能与ViewController是分开的。ViewModel是ViewController的辅助对象。根据以上特点,最容易想到的类之间的关系应该是代理/委托关系。把一眼就能看出来的关系搞复杂了,可能会被骂,但是对于后面的讨论,上面的或多或少会起到决定性的作用。例如,虽然代理和委托是确定的,但谁是代理人,谁是委托人?换句话说,谁是协议的制定者,谁又是实施者?作者在这里给出了两个证据来证实。协议方法是一种被动调用方法,即反向调用。基于此,协议的实施者应该同时是事件的响应者,以事件驱动正向调用,进而触发反向调用。协议实现者实现的方法是通用的和抽象的。相反,协议制定者需要实现更难抽象或更具体的方法。这个基础也可以从另一个层面来理解,即协议的实现者的可替换性要更强。第一个基础相对来说是毋庸置疑的。ViewController毕竟是View的持有者和管理者,是View和ViewModel交互的唯一通道。让ViewModel驱动UIViewController作为View事件的响应者在结构上没有意义。二是实践总结。在实际开发中,从外到内,视图的修改频率往往大于数据的修改频率。所以重构ViewController的概率也大于重构ViewModel的概率。然而,这个归纳结论不能一言以蔽之。相反,它会建议你在实际开发过程中根据这些开发需求对结构进行更灵活的调整和优化。在此实践中,代码将使用作为协议开发者的ViewModel构建。让协议更轻量在OC中,有一系列与@protocol相关的语法,专门用于声明所有与实现协议相关的函数。但是考虑到具体的ViewModels和ViewControllers之间的相互调用是不同的,如果我们为每一组ViewModels和ViewControllers声明一个协议,交给彼此来实现和调用,那么代码量的激增基本上是不可避免的。向上。为了让整个协议结构更轻量,这里没有使用@protocol相关的语法,而是使用了如下代码:@end这段代码做了几件事:通过分类,扩展了UIViewController的ViewModel相关的回调方法声明。功能类似于父类声明的抽象接口,由子类实现。接口支持参数传递,具体的类不再指定协议方法,只指定协议参数。在ViewModel的基类中声明这个分类,可以保证ViewModel可见的所有UIViewControllers都实现了protocol方法,就不用写@protocol段了。在具体的ViewModel和ViewController子类中,只需要根据具体需求设计回调参数,构造相应的枚举即可。降低整个协议结构重量的主要原因是因为协议的内容变化频繁。使用枚举代替协议可以减少改动的范围,代码量也少,便于定制。作者也尝试过定义双向抽象方法,即为ViewModel做一些抽象方法,让双方只能按照基类约定的约定来工作。但在实践中,ViewModel的方法并不容易抽象,因为它的公共方法往往直接反映了ViewController的数据需求。如果强行制定抽象方法,在构建具体类时会造成归纳混乱。最坏的结果就是放弃遵守协议,整个代码将变得难以维护。将需求转化为行为在开发过程中,最常见的开发流程是需求驱动的开发流程。说白了,就是丢给你一个示意图,有时候运气好的话,会有交互原型(运气不好的话,就是别人的App==),然后你就锤子写了和一根棍子。写和画。这时,建议适当规划开发过程。主要考虑如下:开发层次和顺序;单位时间内只关心尽可能少的事情;易于构建和调试;合理简化重复性工作。其实简单来说,就是让整个工作流程有规律、有条不紊,确保开发有根有据、可控。此外,它可以有效避免错误的频率和严重程度。在这里,作者厚颜无耻地分享他简单的工作流程。整个过程并不复杂。其实就是先接触ViewController接口,遇到需要数据的地方,在ViewModel中声明一个方法,然后假装去调用。撸的代码概要是这样的:typedefNS_ENUM(BCViewControllerAction,BCTopViewCallBackAction){BCTopViewCallBackActionReloadTable=1<<0,BCTopViewCallBackActionReloadResult=1<<1};@interfaceBCTopViewModel:BCBaseViewModel-(NSString*)LEDString;-(NSUInteger)operationCountNSString*)operationTextAtIndex:(NSUInteger)index;-(void)undo;-(void)clear;@end@interfaceBCTopViewController()@property(nonatomic,strong)BCTopViewModel*model;@property(nonatomic,weak)IBOutletUITableView*operationTable;@property(nonatomic,weak)IBOutletUILabel*result;@end@implementationBCTopViewController-(void)viewDidLoad{[superviewDidLoad];self.operationTable.tableFooterView=UIView.new;}#pragmamark-action-(IBAction)undo:(UIButton*)发件人{[self.modelundo];}-(IBAction)clear:(UIButton*)sender{[self.modelclear];}#pragmamark-callback-(void)callBackAction:(BCViewControllerAction)action{if(action&BCTopViewCallBackActionReloadTable){[self.operationTablereloadData];}if(action&BCTopViewCallBackActionReloadResult){self.result.text=self.model.LEDString;}}#pragmamark-tableViewdatasource&delegate-(NSInteger)tableView:(UITableView*)tableViewnumberOfRowsInSection:(NSInteger)section{returnsself.model.operationCount;}-(UITableViewCell*)tableView:(UITableView*)tableViewcellForRowAtIndexPath:(NSIndexPath*)indexPath{UITableViewCell*cell=[tableViewdequeueReusableCellWithIdentifier:@"cell"forIndexPath:indexPath];cell.textLabel.text=[self.modeloperationTextAtIndex:indexPath.row];returncell;}@end的开发使用了一个Runtimetrick,即Nil可以响应任何消息。因此,虽然我们只是声明了方法,并没有实现它,但是上面的代码也是可以随时运行的。换句话说,您可以随时运行它来调试界面,而不必担心ViewModel的实现。比较麻烦的是测试回调方法。笔者自己的建议是写好回调方法后ViewController中对应的ViewModel调用forward后直接调用自己的回调即可。如果遇到可能的网络请求或者回调需要延迟,也可以考虑写一个基于dispatch_after的测试宏来测试回调。一般来说,视图界面层的开发永远是所见即所得,所以测试标准就是页面需求本身。当所有肉眼可见的需求都实现了,我们的界面编写也就告一段落了。当然这时候的代码还是很脆弱的,因为我们只做了正向实现,没有做边界用例测试,不知道异常情况下会不会有什么奇怪的东西。幸运的是,我们已经成功隔离了ViewController中与数据相关的部分。在以后的测试中,我们发现任何数据相关的bug,我们可以拍着胸脯说,一定和ViewController没有关系。另外,正如我所说,要求本身就是页面的测试标准。也就是说,当你实现需求的时候,你的视图层就已经通过了测试。是的,我开始应用TDD思维方式。我们把需求作为测试用例,一一通过。而当我们完成了ViewController的开发之后,我们也为ViewModel声明了所有的public方法,并在相应的位置进行了调用。BDD的关键点是It...when...should的行为断言。在这个环境下,就是ViewModel,每次调用ViewController的时候,都应该对应ViewModel派生的所有数据接口。改变。换句话说,我们可能无法从界面上看到所有由行为引起的变化,但我们在ViewModel实现之前已经搭建了一个可测试的环境。如果时间充裕,此时首先应该根据具体的调用环境,为每个公共方法编写足够强的测试代码,避免数据错误。顺便在风月庭说几句假话。在构建程序时,面向接口优于面向实现,因为在任何系统中,信息的传递决定了系统本身是否强大,而不是信息的产生。编写代码时,先将抽象的函数方法具体化,再将数据逐步抽象,穿梭穿梭,更能完美实现“高内聚、低耦合”的目标。FatModel如果仅仅从ViewModel实践的角度,上面的内容已经讲解的差不多了。不过由于笔者摸索了整个Demo,所以我会说明其他几个地方的设计。首先是关于脂肪模型的设计。胖瘦Model概念的作者最近才从这篇iOS应用架构谈起view层的组织和调用方案。在此之前,我只是和朋友讨论过,Model和Model应该有区别。模型的胖瘦是根据业务相关性来划分的。因此,作者有时会直接将胖模型称为业务层模型,以区别瘦模型。在示例代码中,CalculatorBrain应该算是一个比较标准的业务层Model。如果遇到单个ViewModel(或者MVC中的Controller)解决不了的需求,就需要将整体业务下沉,交给一个相对独立的Model来解决。上层只持有Model打开的接口,由此促成的业务层Model有明显的业务痕迹。说白了就是不容易复用。但是笔者自己的开发观点是,弱业务相关模块的复用性要强,即功能尽量单元化。与业务强相关的模块应该有更好的重构和替换性能,即尽可能多的功能内聚。简单的说,比如这个Demo已经不是计算器了,需要改成计数器什么的。只有CalculatorBrain类需要重构。(当然,这只是基于假设,我无法想象需要保持接口不变,底层数据疯狂变化……)另一方面,在整个MVVM框架中,每个单独的ViewModel都可以也可以看作是管道。在整个业务链上做了双向抽象,提高了整个业务链各部分的可替换性。笔者个人倾向于将此理解为通过设计中间层来平衡上下层的复杂度。LighterViewControllersRobjccn.io第一期的第一篇文章是LighterViewControllers。如文章中所述,可以通过将每个协议的实现移出ViewController来精简ViewController。笔者也是这个建议的实践者之一,甚至一度认为这也是ViewModel的主要功能。但是随着开发时间的拉长,笔者不得不重新审视这个问题。首先,这种方法创建了许多额外的接口。我们仍然以UITableView为例。假设我们让ViewModel实现UITableViewDelegate和UITableViewDataSource协议。这时候如果ViewController的另外一个控件要根据tableView的滚动位置进行响应怎么办?由于ViewModel是tableView的delegate,所以我们需要为ViewController声明额外的public方法,以便ViewModel在回调方法中调用。不难发现,基本上所有视图控件的Delegate协议都会涉及到视图本身的响应。只要涉及同一界面下不同控件元素的交互,就少不了ViewController的参与。作者也尝试过在ViewController中实现UITableViewDelegate,将UITableViewDataSource委托给ViewModel。痛苦的事情发生在动态高度Cell的实现上。一方面,我们向ViewModel内部的tableView:cellForRowAtIndexPath输入数据,但另一方面,我们必须为tableView:heightForRowAtIndexPath:提供相同的数据来计算高度。作者最后总结了原因,因为View层和ViewController层本身存在holding和beingholding的依赖关系,所以任何类作为ViewController的实例来实现协议回调,实际上都是跨层调用的,所以,这注定是以增加接口为代价的,换句话说,ViewController的内聚性变差了。另一方面,原因与测试有关。之所以说ViewController难测,是因为在大多数情况下,它没有几个像样的公有方法,而私有方法大多是传参回调。如果我们在另一个类中实现这些协议,它不会提高它们的可测试性。更有效的方法应该是将协议的实现与数据接口隔离开来,让实现者通过接口而不是自己来填充数据。Demo中TopViewModel提供了operationCount、operationTextAtIndex:等数据接口,用于单元格内容填充。我相信为这样的数据接口构建测试环境要比为tableView:cellForRowAtIndexPath方法构建测试环境简单的多。从侧面看,这样的接口更适合做测试覆盖。基于以上两个原因,在后续的开发中,作者开始将越来越多的协议请回ViewController。而且,由于ViewModel的存在,作者更喜欢将ViewController构建成一个独立实现的类,只负责实现界面布局和逻辑,这样一个类可以做的事情更少,但做得更好。后记本文中的相关demo,实现的功能并不复杂,甚至有些简陋丑陋。错误是由于作者缺乏想象力。本着实际演示的精神,仅作参考。作者自称是一种方法论及其实践者,认同“构建代码的方法比代码更有价值”的观点。写出一两句令人惊叹的代码可能是运气,但掌握构建代码的方法本身就是战斗力。尽量让自己的代码每一句都说得过去,而不是随心所欲,觉得这样会更有责任感,至少写的时候会有一个解释。以上总结知识有限,可能有很多地方有疏漏。希望与您交流。如能指出疏漏甚至错误观点,将不胜感激。还有,说点不丢人的话。截至本博文撰写之时,虽然对“设计模式”的相关概念有各种并排验证和查询,但相关概念还没有被系统地研究过。说来惭愧,有时候费了好大功夫去揣摩和思考答案,突然发现某本书或某篇文章已经把几句话解释清楚了,其实挺郁闷的。如果次数太多,你甚至可能对未知的知识产生抵触情绪,以此来安慰自己。这也是我特意声明没有系统学习的原因。然而,发展之路任重而道远。其实谁都知道,我们只是站在巨人肩膀上的搬砖人。回头看看脚下的路,每一块砖都足以让我自惭形秽,自欺欺人,只是浮躁和尴尬。所以在写这篇文章的过程中,笔者已经购买了《设计模式:可复用面向对象软件的基础》这本书,希望能够系统地学习一些代码构建技巧(以后继续吹牛==)。结语结语作者目前正在找新工作,意向还是iOS开发,坐标还是深圳。如果您有机会看到这篇文章,希望我成为您并肩作战的战友,或者您有好的地方可以推荐,希望您与我联系。提前致谢~