Skip to content

Commit 338d6b2

Browse files
committed
Add Content
1 parent 3296188 commit 338d6b2

File tree

2 files changed

+129
-0
lines changed

2 files changed

+129
-0
lines changed

chapter6/final_thoughts.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,29 @@
11
# 终稿
2+
3+
MVVM是一个非常有趣的架构。在这方面我思考的越多,它对我的意义便越大。诚然,本章中的视图模型所呈现的业务逻辑都很轻量。我已经把它们上传到[Github仓库](https://github.com/ashfurrow/FunctionalReactivePixels)里了,但是本章作为一个MVVM的示例为初学者的开始提供了参考。
4+
5+
我想提供一个具体的例子来说明它比MVC更有竞争力,更具意义。
6+
7+
这个例子中视图控制器有很多业务逻辑。最近我创建的一个App中,我们有一堆数据,支持下拉刷新,每一个元素点击之后会推出详情页面。非常标准的东西。然而,这一堆数据彼此之间来路是不一样的,有的是主API入口的数据结果,有的是它们的搜索结果,有的是App在编译时就决定的静态元素。
8+
9+
使用MVC的话,我想到了两种方法来解决:
10+
11+
1. 在臃肿的视图控制器中创建一个类处理显示,并管理所有的数据内容
12+
2. 毫无疑问,另一种方法就是子类化一个视图控制器的抽象基类来包含所有内容的通用逻辑。
13+
14+
这是过去我所采用的方式,但这方式很难重构,比方说:有些所有类型的通用内容变得只对部分类型有效时。这同样也能被称为是一种黑客攻击,因为Objective-C不支持抽象类。
15+
16+
我采用的方法是使用符合该视图控制器所依赖的协议的不同的视图模型。通过将定制的业务逻辑放置于视图模型中,我避免了视图控制器的臃肿化,视图控制器仅需要根据视图模型的协议来知道如何显示即可。 MVVM是子类化视图控制器的一个很好的选择。
17+
18+
另外,如果你有多平台需求(比如说:iOS & OSX),他们可以共用一套视图模型,因为他们不牵扯到视图本身的逻辑。你甚至可以走得更远,用另外的语言来生成视图模型,然后生成指定的语言的视图模型对象比如:Objective-C、C#、Ruby、Java或者其他你需要的任何语言。疯狂吧这玩意~
19+
20+
最后,我们并没有真正地涉及到`RACCommand`的使用。我将利用Justin Spahr-Summers的观念在MVVM的范畴来解释它。
21+
22+
1. 控制(事件)与它交互
23+
2. 一个属于视图模型的命令被执行
24+
3. 视图模型的逻辑被运行(运行的是命令初始化时的signalBlock)
25+
4. 视图模型通过ReactiveCocoa来间接通知视图。在我们的例程中,视图会被更新。
26+
27+
再一次强调[Github仓库](https://github.com/ashfurrow/FunctionalReactivePixels)包含了我们在本书中没有能够涉及的,关于`RACCommand`的,使用的详细信息。去看一看吧!
28+
29+
MVVM效果很好,与ReactiveCocoa结合起来使用更好。你没有必要一下子就被它“招安”了。你可以从小处着手,先在一个视图控制器中使用,看看你到底能有多喜欢它。在你的下一个项目中尝试使用它把,你会看到它如何彻底简化你的视图控制器的复杂度。

chapter6/testing_viewModels.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,8 +353,109 @@ describe (@"FRPPhotoViewModel", ^{
353353
现在来看这个复杂的初始化方法,这东西看起来真巨大!近20行纯粹的未经测试的代码。哎呀!让我们来一点点简化这个事情,并逐步加上我们的测试代码。
354354

355355
```Objective-C
356+
- (instancetype)initWithModel:(FRPPhotoModel *)photoModel {
357+
self = [super initWithModel:photoModel];
358+
if(!self) return nil;
359+
360+
@weakify(self);
361+
[self.didBecomeActiveSignal subscribeNext:^(id x) {
362+
@strongify(self);
363+
[self downloadPhotoModelDetails];
364+
}];
365+
366+
RAC(self, photoImage) = [RACObserve(self.model, fullsizedData) map:^id (id value) {
367+
return [UIImage imageWithData:value];
368+
}];
369+
370+
return self;
371+
}
372+
373+
- (void)downloadPhotoModelDetails {
374+
self.loading = YES;
375+
[[FRPPhotoImporter fetchPhotoDetails:self.model] subscribeError:^(NSError *error) {
376+
NSLog(@"Could not fetch photo details : %@",error);
377+
} completed:^ {
378+
self.loading = NO;
379+
NSLog(@"Fetched photo details.");
380+
}];
381+
}
382+
383+
```
384+
385+
我们选择了不直接测试`fetchPhotoDetails:`,所以我们把它置于一个实例方法中,以便更容易对它进行测试。这个方法(即`fetchPhotoDetails:`)实现的细节在这里对我们不重要。
386+
387+
现在开始写关于它的测试代码吧:
388+
389+
```Objective-C
390+
it(@"should download photo model details when it becomes active", ^{
391+
FRPPhotoViewModel *viewModel = [[FRPPhotoViewModel alloc] initWithModel:nil];
392+
393+
id mockViewModel = [OCMockObject partialMockForObject:viewModel];
394+
[[mockViewModel expect] downloadPhotoModelDetails];
395+
396+
[mockViewModel setActive:YES];
397+
[mockViewModel verify];
398+
});
399+
```
400+
401+
注意看初始化方法中不产生(函数)副作用而是把这种副作用放在订阅`didBecomeActiveSignal`的Block块中时,测试视图模型的代码是多么简单!
402+
403+
现在我们需要测试剩下的那些视图模型,他们全部非常简单。我们使用更少的mock,因为很多的业务逻辑仅仅是视图模型的model值到他自己的属性的映射。
404+
405+
```Objective-C
406+
it (@"should return the photo's name property when photoName is invoked", ^{
407+
NSString *name = @"Ash";
408+
409+
id mockPhotoModel = [OCMockObject mockForClass:[FRPPhotoModel class]];
410+
[[[mockPhotoModel stub] andReturn:name] photoName];
411+
412+
FRPPhotoViewModel *viewModel = [[FRPPhotoViewModel alloc] initWithModel:nil];
413+
id mockViewModel = [OCMockObject partialMockForObject:viewModel];
414+
[[[mockViewModel stub] andReturn:mockPhotoModel] model];
415+
416+
id returnedName = [mockViewModel photoName];
417+
418+
expect(returnedName).to.equal(name);
419+
420+
[mockPhotoModel stopMocking];
421+
});
422+
423+
it (@"should correctly map image data to UIImage", ^{
424+
UIImage *image = [[UIImage alloc] init];
425+
NSData *imageData = [NSData data];
426+
427+
id mockImage = [OCMockObject mockForClass:[UIImage class]];
428+
[[[mockImage stub] andReturn:image] imageWithData:imageData];
429+
430+
FRPPhotoModel *photoModel = [[FRPPhotoModel alloc] init];
431+
432+
photoModel.fullsizedData = imageData;
433+
434+
__unused FRPPhotoViewModel *viewModel = [[FRPPhotoViewModel alloc] initWithModel:photoModel];
435+
436+
[mockImage verify];
437+
[mockImage stopMocking];
438+
439+
});
440+
441+
it(@"should return the correct photo name", ^{
442+
NSString *name = @"Ash";
443+
444+
FRPPhotoModel *photoModel = [[FRPPhotoModel alloc] init];
445+
photoModel.photoName = name;
446+
447+
FRPPhotoViewModel *viewModel = [[FRPPhotoViewModel alloc] initWithModel:photoModel];
448+
449+
NSString *returnedName = [viewModel photoName];
450+
451+
expect(name).to.equal(returnedName);
452+
});
356453
357454
```
358455

456+
这就是为视图模型撰写单元测试的全部内容了。
457+
458+
在理想的情况下,单元测试能帮助改进你的代码质量。小巧而高内聚的方法比随意的满是副作用的方法更招人待见。它简单而完美地诠释了函数响应型编程的精髓。
359459

460+
测试MVVM的好处是:我们不用触及UIKit。请记住,写得好的MVVM视图模型的特点是:该视图模型不会与用户交互的接口类有任何交互。
360461

0 commit comments

Comments
 (0)