MVC & MVVM & ReactiveCoc
一. MVC
Models--负责主要的数据或者操作数据的数据访问层,可以想象 Perspn 和 PersonDataProvider 类。
Views--负责展示层(GUI),对于iOS环境可以联想一下以 UI 开头的所有类。
Controller/Presenter/ViewModel--负责协调 Model 和 View,通常根据用户在View上的动作在Model上作出对应的更改,同时将更改的信息返回到View上。
优劣
更好的理解它们之间的关系
复用(尤其是对于View和Model)
独立的测试
二. MVVM
ViewModel: 相比较于MVC新引入的视图模型。是视图显示逻辑、验证逻辑、网络请求等代码存放的地方,唯一要注意的是,任何视图本身的引用都不应该放在VM中,换句话说就是VM中不要引入UIKit.h
缺点:
我所知道这个模式最清楚的地方就是: 它将view controller 中的(view controller不是一个你拥有的对象,因为它是Apple class 的一个子类) 代码移动到了你拥有的view model 中。这样view controller 就很容易的能够聚焦view生命周期时间。尽管这样,我们的代码还是大量的聚集在一起,它只是从view controller 移动到了 view model 中而已。
因为view model 只有一个,我们还没有解决这个复杂的问题。如果你创建一个view model 来避免你的控制器过于臃肿,那么如果你的app代码又增加了一倍要怎么办?或许到那时候,我们可以加一个controller-model.
这种view model 解决方法不是很好,它只是在问题上贴了一个创可贴,问题还会继续出现。我们需要一个更好的,能从根本上解决问题的办法。就像一个能够在对象变得更大时,你能够对其进行持续性的划分,好比正在进行有丝分裂的细胞。view model它只是个一次性的补丁。
其他的团体已经经历过这样的问题
Rails community 在几年前就经历过这个问题,我们能够从他们的故事中有所借鉴。首先,他们controller非常臃肿 ,除了persistence(持久性数据)外,几乎是一个空的模型。这样的代码几乎无法测试,于是他们将所有的逻辑移动到了model中,这样就有了简洁的controller和臃肿的model。由于臃肿的model依靠于ActiveRecord, 这样的话数据库依旧很难测试并且需要拆分成很小的单元。
View model作为一种解决方法,并不适用于现代编程模式的变化。他们是一种定义不清的结构,不知道应该包含什么,这就导致了它们会遇到和view controller 一样的问题。它们只是当我们遇到复杂问题的临时补丁,如果我们忽略这些问题,在不久之后问题再次出现时,我们还是要解决同样的问题。
三. ReactiveCocoa
响应式编程思想:不需要考虑调用顺序,只需要知道考虑结果,类似于蝴蝶效应,产生一个事件,会影响很多东西,这些事件像流一样的传播出去,然后影响结果,借用面向对象的一句话,万物皆是流。
代表:KVO运用。
函数式编程思想:是把操作尽量写成一系列嵌套的函数或者方法调用。
函数式编程特点:每个方法必须有返回值(本身对象),把函数或者Block当做参数,block参数(需要操作的值)block返回值(操作结果)
代表:ReactiveCocoa。
链式编程思想
链式编程 是将多个操作(多行代码)通过点号(.)链接在一起成为一句代码,使代码可读性好。如:
make.add(1).add(2).sub(5).muilt(-4).divide(4);
特点:方法的返回值是block,block必须有返回值(本身对象),block参数(需要操作的值)
代表:masonry框架。
ReactiveCocoa 可以说是结合了函数式编程和响应式编程的框架,也可称其为函数响应式编程(FRP)框架,强调一点,RAC虽然最大的优点是提供了一个单一的、统一的方法去处理异步的行为,包括delegate方法,blocks回调,target-action机制,notifications和KVO.但是不要简单的只是单纯的认为他仅仅就是减少代码复杂度,它最大的与众不同是提供了一种新的写代码的思维,由于RAC将Cocoa中KVO、UIKit event、delegate、selector等都增加了RAC支持,所以都不用去做很多跨函数的事。
如果全工程都使用RAC来实现,对于同一个业务逻辑终于可以在同一块代码里完成了,将UI事件,逻辑处理,文件或数据库操作,异步网络请求,UI结果显示,这一大套统统用函数式编程的思路嵌套起来,进入页面时搭建好这所有的关系,用户点击后妥妥的等着这一套联系一个个的按期望的逻辑和次序触发,最后显示给用户。
ReactiveCocoa 主要解决了以下这些问题:
1.UI数据绑定:
UI控件通常需要绑定一个事件,RAC可以很方便的绑定任何数据流到控件上。
2.用户交互事件绑定:
RAC为可交互的UI控件提供了一系列能发送Signal信号的方法。这些数据流会在用户交互中相互传递。
3.解决状态以及状态之间依赖过多的问题:
有了RAC的绑定之后,可以不用在关心各种复杂的状态,isSelect,isFinish……也解决了这些状态在后期很难维护的问题。
4.消息传递机制的大统一:
OC中编程原来消息传递机制有以下几种:Delegate,Block Callback,Target-Action,Timers,KVO,objc上有一篇关于OC中这5种消息传递方式。现在有了RAC之后,以上这5种方式都可以统一用RAC来处理。
操作思想
运用的是Hook(钩子)思想,Hook是一种用于改变API(应用程序编程接口:方法)执行结果的技术.
Hook用处:截获API调用的技术。
操作方法
bind(绑定)- ReactiveCocoa核心方法
ReactiveCocoa 操作的核心方法是 bind(绑定),而且也是RAC中核心开发方式。之前的开发方式是赋值,而用RAC开发,应该把重心放在绑定,也就是可以在创建一个对象的时候,就绑定好以后想要做的事情,而不是等赋值之后在去做事情。
列如,把数据展示到控件上,之前都是重写控件的 setModel 方法,用RAC就可以在一开始创建控件的时候,就绑定好数据。
作用
RAC底层都是调用bind, 在开发中很少直接使用 bind 方法,bind属于RAC中的底层方法,我们只需要调用封装好的方法,bind用作了解即可.
bind方法使用步骤:
传入一个返回值 RACStreamBindBlock 的 block。
描述一个 RACStreamBindBlock 类型的 bindBlock作为block的返回值。
描述一个返回结果的信号,作为 bindBlock 的返回值。
注意:在bindBlock中做信号结果的处理。
bind方法参数:
RACStreamBindBlock:
typedef RACStream * (^RACStreamBindBlock)(id value, BOOL stop);
参数一(value):表示接收到信号的原始值,还没做处理
参数二(stop):用来控制绑定Block,如果*stop = yes,那么就会结束绑定。
返回值:信号,做好处理,在通过这个信号返回出去,一般使用 RACReturnSignal,需要手动导入头文件RACReturnSignal.h
- 函数式编程的特点是:
数据结构比较少,鼓励函数的重用,通过组合不同的函数行程高阶函数来满足需求
面向对象通过封装不确定因素来使得代码被人理解,而函数式编程通过减少不确定因素来使得代码被人理解
函数式的架构都是基于值不可变无副作用这个特点。 - 响应式编程特点是:
面向数据流和变化传播的编程范式
a=b+c, a的值随着b和c的更新而更新,就像Excel一样
可以显示的使用箭头来表示数据流向
RACSiganl:信号类,一般表示将来有数据传递,只要有数据改变,信号内部接收到数据,就会马上发出数据。
注意:
信号类(RACSiganl),只是表示当数据改变时,信号内部会发出数据,它本身不具备发送信号的能力,而是交给内部一个订阅者去发出。
默认一个信号都是冷信号,也就是值改变了,也不会触发,只有订阅了这个信号,这个信号才会变为热信号,值改变了才会触发。
如何订阅信号:调用信号RACSignal的subscribeNext就能订阅。
RACSiganl简单使用:
RACSignal使用步骤:// 1.创建信号 + (RACSignal *)createSignal:(RACDisposable * (^)(id subscriber))didSubscribe// 2.订阅信号,才会激活信号. - (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock// 3.发送信号 - (void)sendNext:(id)value// RACSignal底层实现:// 1.创建信号,首先把didSubscribe保存到信号中,还不会触发。// 2.当信号被订阅,也就是调用signal的subscribeNext:nextBlock// 2.2 subscribeNext内部会创建订阅者subscriber,并且把nextBlock保存到subscriber中。// 2.1 subscribeNext内部会调用siganl的didSubscribe// 3.siganl的didSubscribe中调用[subscriber sendNext:@1];// 3.1 sendNext底层其实就是执行subscriber的nextBlock// 1.创建信号RACSignal *siganl = [RACSignal createSignal:^RACDisposable *(id subscriber) {// block调用时刻:每当有订阅者订阅信号,就会调用block。// 2.发送信号[subscriber sendNext:@1];// 如果不在发送数据,最好发送信号完成,内部会自动调用[RACDisposable disposable]取消订阅信号。[subscriber sendCompleted];return[RACDisposable disposableWithBlock:^{// block调用时刻:当信号发送完成或者发送错误,就会自动执行这个block,取消订阅信号。// 执行完Block后,当前信号就不在被订阅了。NSLog(@"信号被销毁"); }]; }];// 3.订阅信号,才会激活信号.[siganl subscribeNext:^(idx) {// block调用时刻:每当有信号发出数据,就会调用block.NSLog(@"接收到数据:%@",x); }];
RACSubscriber:表示订阅者的意思,用于发送信号,这是一个协议,不是一个类,只要遵守这个协议,并且实现方法才能成为订阅者。通过create创建的信号,都有一个订阅者,帮助他发送数据。
RACDisposable:用于取消订阅或者清理资源,当信号发送完成或者发送错误的时候,就会自动触发它。
使用场景:不想监听某个信号时,可以通过它主动取消订阅信号。
RACSubject:RACSubject:信号提供者,自己可以充当信号,又能发送信号。
使用场景:通常用来代替代理,有了它,就不必要定义代理了。
RACReplaySubject:重复提供信号类,RACSubject的子类。
RACReplaySubject与RACSubject区别:
RACReplaySubject可以先发送信号,在订阅信号,RACSubject就不可以。
// RACSubject使用步骤
// 1.创建信号 [RACSubject subject],跟RACSiganl不一样,创建信号时没有block。
// 2.订阅信号 - (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock
// 3.发送信号 sendNext:(id)value
// RACSubject:底层实现和RACSignal不一样。
// 1.调用subscribeNext订阅信号,只是把订阅者保存起来,并且订阅者的nextBlock已经赋值了。
// 2.调用sendNext发送信号,遍历刚刚保存的所有订阅者,一个一个调用订阅者的nextBlock。
// 1. 创建信号
RACSubject *subject = [RACSubject subject];
// 2.订阅信号
[subject subscribeNext:^(id x) {
// block调用时机:当信号发出新值,就会调用
NSLog(@"收到信号");
}];
// 3.发送信号
NSLog(@"发送信号");
[subject sendNext:@"1"];
Signal and Subscriber
这是RAC最核心的内容,这里我想用插头和插座来描述,插座是Signal,插头是Subscriber。想象某个遥远的星球,他们的电像某种物质一样被集中存储,且很珍贵。插座负责去获取电,插头负责使用电,而且一个插座可以插任意数量的插头。当一个插座(Signal)没有插头(Subscriber)时什么也不干,也就是处于冷(Cold)的状态,只有插了插头时才会去获取,这个时候就处于热(Hot)的状态。
Signal获取到数据后,会调用Subscriber的sendNext, sendComplete, sendError方法来传送数据给Subscriber,Subscriber自然也有方法来获取传过来的数据,如:[signal subscribeNext:error:completed]。这样只要没有sendComplete和sendError,新的值就会通过sendNext源源不断地传送过来,举个简单的例子:
[RACObserve(self, username) subscribeNext: ^(NSString *newName){
NSLog(@"newName:%@", newName);
}];
RACObserve使用了KVO来监听property的变化,只要username被自己或外部改变,block就会被执行。但不是所有的property都可以被RACObserve,该property必须支持KVO,比如NSURLCache的currentDiskUsage就不能被RACObserve。
冷信号(Cold)和热信号(Hot)
区别是:
Hot Observable是主动的,尽管你并没有订阅事件,但是它会时刻推送,就像鼠标移动;而Cold Observable是被动的,只有当你订阅的时候,它才会发布消息。
Hot Observable可以有多个订阅者,是一对多,集合可以与订阅者共享信息;而Cold Observable只能一对一,当有不同的订阅者,消息是重新完整发送。
在纯函数式语言(例如Haskell)中对此可以进行一定的优化,也就是说纯函数的调用在相同参数下的返回值第二次不需要计算,所以在纯函数式语言里面的FRP并没有冷信号的担忧。然而Objective-C语言中并没有这种纯函数优化,因此有大规模运算的冷信号对性能是有一定影响的。
上面提到过这两个概念,冷信号默认什么也不干,比如下面这段代码
RACSignal *signal = [RACSignal createSignal:^ RACDisposable * (id subscriber) {
NSLog(@"triggered");
[subscriber sendNext:@"foobar"];
[subscriber sendCompleted];
return nil;
}];
我们创建了一个Signal,但因为没有被subscribe,所以什么也不会发生。加了下面这段代码后,signal就处于Hot的状态了,block里的代码就会被执行。
[signal subscribeCompleted:^{
NSLog(@"subscription %u", subscriptions);
}];
或许你会问,那如果这时又有一个新的subscriber了,signal的block还会被执行吗?这就牵扯到了另一个概念:Side Effect
Side Effect
还是上面那段代码,如果有多个subscriber,那么signal就会又一次被触发,控制台里会输出两次triggered。这或许是你想要的,或许不是。如果要避免这种情况的发生,可以使用 replay 方法,它的作用是保证signal只被触发一次,然后把sendNext的value存起来,下次再有新的subscriber时,直接发送缓存的数据。