首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

iOS APP 架构漫谈

最近看了一些有关 server 的东西,一些很简单的东西,不外乎是一些文档规范,另外结合最近看的 wwdc 的一些 video,觉得对软件架构(software architecture)认识又清楚了一些,这里记录下来。

software architecture 听上去是一个很大的概念,实际上也包括很多东西,里面的争议也很多。在我看来软件架构最好放在小的场景中理解。

问题 1

我们有 2 个页面。

  • 页面 A:主页面
  • 页面 B:详情页面

demo code 1.0.0

2 个页面分别显示一个数字,这个数字应该相同。详情会修改这个数字,这里我们发现,详情页面和主页面数字不一样。

数据不一致

数据不一致

问题 1 解决方法 A

这里首先的感觉就是,详情页面返回,主页面数据没有刷新,导致数据不一致。 那么 Fix 这个 Bug 的方法,就是在主页面出现的时候刷新界面

代码语言:javascript
复制
- (void)viewWillAppear:(BOOL)animated {    [super viewWillAppear:animated];
    self.displayLabel.text = [[CUDataDAO selectData].data stringValue];}

复制代码

现在来看,还不错。但是,我们调用 selectData 的次数则变得非常非常多。数据不是经常变化的。

demo code 1.0.1

问题 1 解决方法 B

我们发现既然数据的改变是在页面 B 进行的,那么页面 B 修改这个数据的时候,应该把数据变化”通知”给页面 A,那么我们写了一个 Delegate

代码语言:javascript
复制
@protocol CUDetailViewControllerDelegate <NSObject>
- (void)detailVC:(CUDetailViewController *)vc dataChanged:(NSNumber *)data;
@end

复制代码

在页面 B 修改数据之后,通过 delegate 通知给页面 A。

代码语言:javascript
复制
- (IBAction)changeButtonClicked:(id)sender {    int value = arc4random() % 100;    [CUDataDAO setData:value];
    self.displayLabel.text = [@(value) stringValue];
    if ([self.delegate respondsToSelector:@selector(detailVC:dataChanged:)]) {        [self.delegate detailVC:self dataChanged:@(value)];    }}

复制代码

到此场景 1 得到了不错的解决。

demo code 1.0.2

问题 2

这时我们增加了另一个页面 C。这个场景会稍微抽象一点,我们定义了 3 个数据

  • 页面 A 的数据 dataA
  • 页面 B 的数据 dataB
  • 页面 C 的数据 dataC

问题 1 中 dataA = dataB。在问题 2 中 dataA = dataB + dataC;

问题 2 解决方法 C

也就是说页面 C 的修改,也会影响页面 A 的数据,那么我们是不是也要写一个 XXXXDelegate 呢?

这时我们的大脑嗅出了一些不好的味道,如果再来个什么 dataD,dataE,我们要写这么多的 Delegate 么?对于多对一”通知”这种味道,很自然的想到了不用 Delegate,而是用NSNotification来做。让我们未雨绸缪一下,定义一个 Notificaiton

代码语言:javascript
复制
NSString *const kCUDataChangedNotification = @"CUDataChangedNotification";
[[NSNotificationCenter defaultCenter] postNotificationName:kCUDataChangedNotification                                                  object:nil                                                userInfo:nil];

复制代码

那这个变化 broadcast 到 listener,看上去是一个很赞的 idea。

demo code 1.0.3

问题 3

过了一段时间,我们发现问题 2 的方法有一个 Bug,当界面停在页面 B 的时候,切换到页面 C,修改数据,B 中再返回时,数据和页面 A 的数据不一致。

数据不一致

数据不一致

那也可以类比解决方法 B,得到了下面的方法

解决方法 D

既然 A 和 B 的数据不一致,而 A 的数据比 B 的新,那么保留一个 B 的指针,然后 A 变化的时候,更新 B 就好了。

代码语言:javascript
复制
- (void)handleDataChangedNotification {    [self updateLabel];    [self.vc updateLabel];}
// In a storyboard-based application, you will often want to do a little preparation before navigation- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{    if ([segue.identifier isEqualToString:@"push"]) {        CUDetailViewController *vc = [segue destinationViewController];        if ([vc isKindOfClass:[CUDetailViewController class]]) {            self.vc = vc;        }    }}

复制代码

demo code 1.0.4

问题 4

页面 C 实在是太简单了,这次我们希望在页面 C 中显示页面 A 的数据。因为上次我们就产生了一个数据不一致的问题,这次我们注意到了,那么怎么修改呢?

解决方法 E

在看了看整个 APP 各种通知之后,觉得挺麻烦,准备用一个取巧的方法。可以类比解决方法 A。在页面 C 出现的时候,刷新数据,至于什么性能问题,不管了,先 fix bug。

代码语言:javascript
复制
- (void)viewWillAppear:(BOOL)animated {    [self updateLabel];}
- (void)updateLabel {    int dataB = [[CUDataDAO selectData].data intValue];    int dataC = [[CUDataDAO selectOtherData].data intValue];
    self.dataLabel.text = [@(dataB + dataC) stringValue];}

复制代码

demo code 1.0.5

问题 5

这时的数据需要不断的变化,我们在CUDataDAO加了一个 timer 模拟数据变化,数据变化的原因可能是 server push 一些数据。client 本地数据库更新了数据,需要在页面 A、B、C 中显示。

页面 C 的数据又不一致了。。。。

问题到底在哪里呢

走到这里,我们需要重新思考为什么这个问题会不断的重复出现呢?software architecture就是来解决这个问题的。但是在提出一个合理的方案之前,先思考一个概念。

我们把数据库中的数据,显示到屏幕上,或是传递给 View 时,这个过程其实是对 data 做了一次 copy。而且只要不是通过引用或是指针这些方式,通过值传递的方式都是对 data 做了一次 copy。而这个 copy 的过程,非常类似 Cache

通常建立一个 Cache 会遇到 2 种问题。

  • Cache 情况 A: 与 original Data 数据不一致,没有及时更新
  • Cache 情况 B: 重复建立 Cache

让我们用这个思路来看我们的解决方案

解决方法 A

这是一个非常典型的Cache情况B。数据库的数据并没有变化,但我们却多次重复计算 cache

解决方法 B

页面之间的关系可以用下面来描述

这里我们隐隐能够感觉到问题,A 的数据变化依赖于 2 个地方。不急,再往后看

解决方法 C

解决方法 D

事情变得更糟了

解决方法 E

和解决方法 A 类似,同样的重复计算 Cache 问题。

实际上问题还会更糟

现在还是一个简单的 Model,如果 project 变得很大,那么就会变成这个样子

每一个X都可能是一个 Bug。

我们似乎已经找到问题了

《Advanced iOS Application Architecture and Patterns》 中,把这个图叫做 information flow。我们的直觉会告诉我们,这个信息的传递,应该是自上而下的树或是森林,而且最好是一个层次平衡结构,要清晰,每一个位置都有相对于的职责。那我们就需要制定一个规则。

在想这个规则之前,如果把上面的图背后的数据忘记,我们感觉这很类似内存模型。当然内存模型会比较复杂。但是我们可以借鉴很多”内存管理中的规则”,比如谁创建,谁销毁。同样,在我们的 information flow 中,我们希望谁创建 Cache,谁更新 Cache 变化

DAO 的数据库似乎很难做这件事情,我们引入了一个新的元素dataSource(当然他本身又是 DAO 的一个 Cache)。其中 A、B、C3 个都会显示数据,那么他们应该在一个层级,其中 B、C 会修改数据,他们会把这个数据返回给dataSource,而通过dataSource来把这个变化通知到 A、B、C。

这样带来的好处很明显,我们再添加一个 D,也不会对其他地方的数据产生任何影响,我们的 Unit Test、Mock 也更加好写。

我们之前的思路错在哪里呢?

从局部来看,我们之前的思路都没有任何问题,但是整体来看却把问题隐藏化。关键的问题是在于没有找到Truth,找到问题真正的地方。而找到真正的地方,需要我们在大脑中有一个清晰的information flow或是data flow。了解之间元素的相互关系,才能建立一个个的层。才能坐到真正的解耦,解耦并不是仅仅一个个的Manager,更重要的是建立一套清晰的 flow 机制,或是消息机制,如果没有一套 flow,中间引入的各种各样的方法,即便使用了各种设计模式,整个 software 依然是深度耦合

疑问

这个 APP 看上去交互非常复杂

上面的 model,有些同学还可能觉得这是交互上面的问题,这个交互看上去非常的复杂,不是一个好设计。

我这里列举一个实际的例子:

A 页面要创建动画,动画背后包括很多数据,这些数据会在 B,C 甚至更多的页面,或是后台被修改。动画本身实际上体现在 View,而这些 view 可能不仅仅在 A 中有,B,C 可能也会有部分的 View。

单例怎么样

当然我们可以用单例的法子。单例是个魔鬼,被很多滥用,这个场景用单例,其实仅仅是把全局变量合理的封装在了单例下,因为这份数据,并没有任何理由要一定是一份 copy。

recap

在了解这个概念后,再看一些 server 的架构,规则时,也会更容易理解这些层之间的关系。包括

  • 为什么要规定那些层之间,不能相互调用,不能有静态方法。
  • 一个层之间的 model,不能有重叠功能,不能连表查询。
  • 在哪个层才能调用另一个服务,而调用这个服务还必须要通过统一的接口

software architecture 涵盖的东西非常多。这篇只是一个引子,介绍了设计之前的准备工作。但是在实际过程中,我们的模型可能要比我这里写的还要复杂很多。下一篇会介绍一种策略用来处理更加复杂模型的情况。

最后附上一个完整功能的 demo code

参考

《Advanced iOS Application Architecture and Patterns》

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/6878af807ef7f225fae192a7c
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券