第一次重构的架构设计总结

2,244 阅读20分钟

前言:

每个开发心中都有一个架构的梦,虽然不能像大佬们一样直接直接给出系统级的架构,但是我们在日常的编码过程中,也可以慢慢积累一些自己的架构的见解,慢慢提高~

因为在学校自己一个人在写整个App,加之需求也不明确,时常需求变更(在学校的组织写项目的通病了),所以编写过程真的是越写越糟心,所以,不得已对已经开发的一小部分做了重构,以下是本小白在重构过程中总结的一些见解(不得不说,本科阶段讲的那些设计模式什么的,是真的很有用,只是当时根本理解不了这些精髓,等到重构时才发现都可以套原型)。

架构的几个方向:

  • view层的组织和调用设计
  • 本地持久化
  • 网络层设计(网络层会说的比较笼统)
  • 动态部署(Web App/Hybrid App/React-Native,这块也没咋说,因为目前没有涉猎)

架构设计的步骤:

  • 问题分类,分模块(这个很重要)
  • 搞清楚各个模块之间的依赖关系,设计好一套模块的交流规范并设计模块
  • 为架构保持一定量的超前性(血的教训)
  • 先实现基础模块,再组合基础模块形成初期架构

主要就是:自顶向下设计,自底向上实现,先量化数据再优化

敏捷原则:对扩展开放-对修改封闭


什么样app的架构叫好架构?

  • 代码整齐,分类明确:每个模块只负责模块内的事务
  • 不用文档,或很少文档,就能让业务方上手
  • 思路和方法要统一,尽量不要多元
  • 没有横向依赖,万不得已不出现跨层访问:(大概就是拓扑排序的原理)

1,当一个需求需要多业务合作开发时,如果直接依赖,会导致某些依赖层上端的业务工程师在前期空转,依赖层下端的工程师任务繁重,导致延期(就会挤压QA老铁的时间,然后再找PM撕*。。。。。别问我是怎么知道的) 2,当要开辟一个新业务时,如果已有各业务间直接依赖,新业务又依赖某个旧业务,就导致新业务的开发环境搭建困难,因为必须要把所有相关业务都塞入开发环境,新业务才能进行开发。 3,当某一个被其他业务依赖的页面有所修改时,比如改名,涉及到的修改面就会特别大。影响的是造成任务量和维护成本都上升的结果。

对应解决方法:依赖下沉,假如A、B、C三个模块存在横向依赖,这样的话引入新节点D,对A、B、C实现依赖下沉,当A调用B的某个页面的时候,将请求交给Mediater,然后由Mediater通过某种手段获取到B业务页面的实例,交还给A就行了。

  • 对业务方该限制的地方有限制,该灵活的地方要给业务方创造灵活- - 实现的条件
  • 易测试,易拓展
  • 保持一定量的超前性
  • 接口少,接口参数少
  • 高性能

关于不跨层访问说下:

跨层访问是指数据流向了跟自己没有对接关系的模块。有的时候跨层访问是不可避免的,比如网络底层里面信号从2G变成了3G变成了4G,这是有可能需要跨层通知到View的。但这种情况不多,一旦出现就要想尽一切办法在本层搞定或者交给上层或者下层搞定,尽量不要出现跨层的情况。跨层访问同样也会增加耦合度,当某一层需要整体替换的时候,牵涉面就会很大。

易测试性:

尽可能减少依赖关系,便于mock。另外,如果是高度模块化的架构,拓展起来将会是一件非常容易的事情。


架构分层:

梗概:

经常有‘三层架构MVC’这样的说法,以至于很多人就会认为三层架构就是MVC,MVC就是三层架构。其实不是的。三层架构里面其实没有Controller的概念,而且三层架构描述的侧重点是模块之间的逻辑关系。MVC有Controller的概念,它描述的侧重点在于数据流动方向。

三层架构

所有的模块角色只会有三种:

  • 数据管理者
  • 数据加工者
  • 数据展示者 意思也就是,笼统说来,软件只会有三层,每一层扮演一个角色。其他的第四层第五层,一般都是这三层里面的其中之一分出来的,最后都能归纳进这三层的某一层中去,所以用三层架构来描述就比较普遍。

View层设计:

View层的架构一旦实现或定型,在App发版后可修改的余地就已经非常之小了。因为它跟业务关联最为紧密,做决策时要拿捏好尺度。

View层架构是影响业务方迭代周期的因素之一:

因为View层架构是最贴近业务的底层架构

view层架构知识点主要包括:

  • 良好的编码/实现规范
  • 合适的设计模式(MVC、MVCS、MVVM、VIPER)
  • 根据业务情况针对ViewController做好拆分(瘦身),提供一些小工具方便开发

view层代码规范:(第4点不一定)

1 viewDidload:做addSubview的事情

2 viewWillAppear:严格来说这里通常不做视图位置的修改,而用来更新Form数据。原因见下一点:

3 布局(添加约束)时机:首先,Autolayout发生在viewWillAppear之后,所以我一般选择放到 - viewWilllayoutSubview或者- viewDidLayoutSubviews中。因为viewWillAppear在每次页面即将显示都会调用,viewWillLayoutSubviews虽然在lifeCycle里调用顺序在viewWillAppear之后,但是只有在页面元素需要调整时才会调用,避免了Constraints的重复添加

4 viewDidAppear里面做添加监听之类的事情

5 属性的初始化,则交给getter(懒加载)去做,这也就要求:所有的属性都使用getter和setter,并且getter,setter方法放到.m文件的最后写,这样可以提高开发效率。另外一种思路是将所有属性都放到 setUpPropertyConfig方法中,然后setUpPropertyConfig放到viewDidLoad中,两者均可,没有什么区别。

#pragma mark - life cycle

- (void)viewDidLoad
{
    [super viewDidLoad];

    [self.view addSubview:self.label];
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];

    self.label.frame = CGRectMake(1, 2, 3, 4);
}

#pragma mark - getters and setters

- (UILabel *)label
{
    if (_label == nil) {
        _label = [[UILabel alloc] init];
        _label.text = @"1234";
        _label.font = [UIFont systemFontOfSize:12];
        ... ...
    }
    return _label;
}
@end

6 每个delegate方法写到一块区域里面去,使用#pragma mark - UITableViewDelegate进行分割(这个是猪场搬砖看到黄师傅的代码学到的。。。之前一直没有这个意识,感谢)

7 VC里面尽量不要有私有方法

不是delegate方法的,不是event response(相应用户操作)方法的,不是life cycle(view didload这些方法)方法的,就是private method了,这些private methods一般是用于日期换算、图片裁剪啥的这种辅助的小功能。这些小功能一般都是单独抽出来写成模块的tool类或者系统Util类。

8 关于View的布局方法:

无外乎就是 storyboard+xib+代码撸的组合

借鉴一下@唐巧的分析脚本: 传送门: https://gist.github.com/tangqiaoboy/b149d03cfd0cd0c2f7a1 可见这个本来就是有争议的。

其实,实现简单的东西,用Code一样简单,实现复杂的东西,Code比StoryBoard更简单。

所以本渣一般采用: 1,复杂页面主体手撸代码(用的是masonry) 2,简单、静态的Cell以及封装的一些自定义小控件使用xib。

还有几点本人目前能力不够,不能够给出正确的见解:

A.是否需要让业务方统一派生ViewController。 B.


#MVC MVC架构基础请看象印笔记。

各个模块需要负责的事物:

  • M应该做的事: 1,给ViewController提供数据(网络获取API+本地的缓存获取API) 2,给ViewController存储数据提供接口(本地缓存的存储/更新新API) 3,提供经过抽象的业务基本组件(一般我会抽一个Manager(包括1,2)出来专门负责),供Controller调度
  • C应该做的事:(其中VC自带的View相当于C所管理的View的一个容器) 1,管理View Container的生命周期 2,负责生成所有的View实例,并放入View Container(就是C.view) 3,监听来自View与业务有关的事件,通过与Model的合作,来完成对应事件的业务。
  • V应该做的事: 1,响应与业务无关的事件,并因此引发动画效果,点击反馈(如果合适的话,尽量还是放在View去做)等。 2,界面元素表达

下面是MVCS、MVVM两个MVC设计模式的变种

可能还有一些别的设计模式,但是本人能力有限啊啊啊,所以只先介绍这俩

先说下:胖Model&瘦Model:

  • 胖Model:(MVVM的基本思想) 包含了部分弱业务逻辑。胖Model要达到的目的是,Controller从胖Model这里拿到数据之后,不用额外做操作或者只要做非常少的操作,就能够将数据直接应用在View上。

  • 瘦Model:(MVCS的基本思想) 瘦Model只负责业务数据的表达,所有业务无论强弱一律扔到Controller。瘦Model要达到的目的是,尽一切可能去编写细粒度Model,然后配套各种helper类或方法来对弱业务做抽象,强业务依旧交给Controller。

前言:首先不管MVVM也好,MVCS也好,他们的共识都是Controller会随着软件的成长,变很大很难维护很难测试。只不过两种架构思路的前提不同,MVCS是认为Controller做了一部分Model的事情,要把它拆出来变成Store,MVVM是认为Controller做了太多数据加工的事情,所以MVVM把数据加工的任务从Controller中解放了出来,使得Controller只需要专注于数据调配的工作,ViewModel则去负责数据加工并通过通知机制让View响应ViewModel的改变。

MVCS:

从概念上来说,它拆分的部分是Model部分,拆出来一个Store。这个Store专门负责数据存取。但从实际操作的角度上讲,它拆开的是Controller。

MVCS使用的前提是,它假设了你是瘦Model,同时数据的存储和处理都在Controller去做。所以对应到MVCS,它在一开始就是拆分的Controller。因为Controller做了数据存储的事情,就会变得非常庞大,那么就把Controller专门负责存取数据的那部分抽离出来,交给另一个对象去做,这个对象就是Store。这么调整之后,整个结构也就变成了真正意义上的MVCS。

MVVM:

ReactiveCocoa成熟之后,ViewModel和View的信号机制在iOS下终于有了一个相对优雅的实现(MVC中View和Model是不能直接通信的,需要Controller做一个协调者的身份)。MVVM本质上也是从MVC中派生出来的思想,MVVM着重想要解决的问题是尽可能地减少Controller的任务。

MVVM是基于胖Model的架构思路建立的,然后在胖Model中拆出两部分:Model和ViewModel。关于这个观点我要做一个额外解释:胖Model做的事情是先为Controller减负,然后由于Model变胖,再在此基础上拆出ViewModel,跟业界普遍认知的MVVM本质上是为Controller减负这个说法并不矛盾,因为胖Model做的事情也是为Controller减负。

另外,MVVM把数据加工的任务从Controller中解放出来,跟MVVM拆分的是胖Model也不矛盾。要做到解放Controller,首先你得有个胖Model,然后再把这个胖Model拆成Model和ViewModel。

在MVVM中,Controller扮演的角色:

  • MVVM的名称里没有C造成了MVVM不需要Controller的错觉,其实MVVM是一定需要Controller的参与的,虽然MVVM在一定程度上弱化了Controller的存在感,并且给Controller做了减负瘦身(这也是MVVM的主要目的)。其实MVVM应该是Model-ViewModel-Controller-View这样的架构,并不是不需要Controller。

  • Controller夹在View和ViewModel之间做事情: 1,最主要事情就是将View和ViewModel进行绑定。在逻辑上,Controller知道应当展示哪个View,Controller也知道应当使用哪个ViewModel,然而View和ViewModel它们之间是互相不知道的,所以Controller就负责控制他们的绑定关系。 2,常规的UI逻辑处理

一句话总结:

在MVC的基础上,把Controller拆出一个ViewModel专门负责数据处理的事情,就是MVVM。

  • 关于MVVM是否必须要使用ReactiveCocoa? 当然不是,只是因为苹果本身并没有提供一个比较适合这种情况的绑定方法。虽然有KVO,Notification,block,delegate用来做数据通信,从而来实现绑定,但都不如ReactiveCocoa提供的RACSignal来的优雅简单,如果不用ReactiveCocoa,绑定关系可能就做不到那么松散那么好,但并不影响它还是MVVM。

再深层次的我就不能很好解释了:如果需要了解,可以细看: https://www.teehanlax.com/blog/model-view-viewmodel-for-ios/

关于项目究竟使用哪种设计模式:

MVC其实是非常高Level的抽象,意思也就是,在MVC体系下还可以再衍生无数的架构方式,但万变不离其宗的是,它一定符合MVC的规范。 所以我的建议是:

  • 只要不是Controller的核心逻辑,都可以考虑拆出去,然后在架构的时候作为一个独立模块去定义,以及设计实现,但是不要为了拆分而拆分。
  • 拆分出的模块尽量提高复用性,降低强业务相关性。
  • 拆分的粒度要尽可能大一点,封装得要透明一些。

网络层:

首先先说下跨层访问:

关于跨层数据流通:

当存在A<-B<-C这样的结构时。当C有事件,通过某种方式告知B,然后B执行相应的逻辑。一旦告知方式不合理,让A有了跨层知道C的事件的可能,你 就很难保证A层业务工程师在将来不会对这个细节作处理。一旦业务工程师在A层产生处理操作,有可能是补充逻辑,也有可能是执行业务,那么这个细节的相关处理代码就会有一部分散落在A层。然而前者是不应该散落在A层的,后者有可能是需求。另外,因为B层是对A层抽象的,执行补充逻辑的时候,有可能和B层针对这个事件的处理逻辑产生冲突,这是我们很不希望看到的。 但有时跨层数据流通也是不可避免的: 比如,信号从2G变成3G变成4G变成Wi-Fi,这个就是需要跨层数据交流的。

再考虑下文:

数据以什么方式交付给业务层:

大多数App在网络层所采用的方案主要集中于这三种:Delegate,Notification,Block。 一般都是组合使用,这里我只能说下个人的选择,毕竟个人涉猎有限,所以只能结合自身所采用的模式说下好处: 之前在猪场某部门实习,网络层采用的是block为主进行数据交付。

当回调之后要做的任务在每次回调时都是一致的情况下,选择delegate,在回调之后要做的任务在每次回调时无法保证一致,选择block。

  • Delegate为主,Notification为辅(苹果的原生网络请求就是delegate。。但是AFN采用block做回调)。 所以我一般都是采用AFN做网络(毕竟方便省事),所以一般采用如下形式进行请求:
//请求发起采用AFN的Block,回调使用delegate方式,这样在业务方这边回调函数就能够比较统一,便于维护。
  [AFNetworkingAPI callApiWithParam:self.param successed:^(Response *response){
        if ([self.delegate respondsToSelector:@selector(successWithResponse:)]) {
            [self.delegate successedWithResponse:response];
        }
    } failed:^(Request *request, NSError *error){
        if ([self.delegate respondsToSelector:@selector(failedWithResponse:)]) {
            [self failedWithRequest:request error:error];
        }
    }];

原因:使用Delegate能够很好地避免跨层访问,同时限制了响应代码的形式,相比Notification而言有更好的可维护性,而Notification则解决了跨层数据流通的相应需求。

但是使用Notification一定要约定好命名规范,不然会引发后期维护的灾难。

集约型API调用方式和离散型API调用方式的选择:

  • 集约型API调用其实就是所有API的调用只有一个类,然后这个类接收API名字,API参数,以及回调着陆点(block,或者delegate等各种模式的着陆点)作为参数。然后执行类似startRequest这样的方法,它就会去根据这些参数起飞去调用API了,然后获得API数据之后再根据指定的着陆点去着陆。

  • 离散型API调用是这样的,一个API对应于一个APIManager,然后这个APIManager只需要提供参数就能起飞,API名字、着陆方式都已经集成入APIManager中。

交付什么样的数据给业务层?【(Adapator适配器)其实这就属于MVVM的VM层做的事了,真的是每一处都是设计模式。。。。】

理想情况是希望API的数据下发之后就能够不需要进一步处理直接被View所展示。首先要说的是,这种情况非常少。另外,这种做法使得View和API联系紧密,也是不应该发生的。 举个栗子:

先定义一个protocol:
@protocol AdapatorProtocol <NSObject>
- (NSDictionary)reformDataWithManager:(APIManager *)manager;
@end

在Controller里是这样:
@property (nonatomic, strong) id< AdapatorProtocol > XAdapator;

#pragma mark - APIManagerDelegate
- (void)apiManagerDidSuccess:(APIManager *)manager
{
    NSDictionary *XData = [manager fetchDataWithReformer:self. XAdapator];
    [self.XView configWithData:XData];
}

在APIManager里面,fetchDataWithReformer是这样:
- (NSDictionary)fetchDataWithReformer:(id< AdapatorProtocol >)adapator{
    if (adapator == nil) {
        return self.rawData;
    } else {
//adapaor进行处理数据
        return [adapator reformDataWithManager:self];
    }
}

使用适配器模式带来的好处:

  • 减轻Controller压力,降低了代码复杂度,同时提高了灵活性,任何时候切换reformer而不必切换业务逻辑就可以应对不同View对数据的需要
  • 在处理单View对多API,以及在单API对多View的情况时,reformer提供了非常优雅的手段来响应这种需求,隔离了转化逻辑和主体业务逻辑,避免了维护灾难。
  • 转化逻辑集中,且将转化次数转为只有一次。使用数据原型的转化逻辑至少有两次,第一次是把JSON映射成对应的原型,第二次是把原型转变成能被View处理的数据。reformer一步到位。另外,转化逻辑在Adapator里面,将来如果API数据有变,就只要去找到对应Adapator然后改掉就好了,方便后期维护
  • 业务数据和业务有了适当的隔离。这么做的话,将来如果业务逻辑有修改,换一个Adapator就好了。如果其他业务也有相同的数据转化逻辑,其他业务直接拿这个Adapator就可以用了,不用重写。另外,如果controller有修改(比如UI交互方式改变),可以放心换controller,完全不用担心业务数据的处理。

网络层优化:

1,使用缓存(本地混存+URL缓存)进行请求次数的减少,能不发请求的就尽量不发请求,必须要发请求时,能合并请求的就尽量合并请求。 2,需要上传的日志,积满一定数量再上传 3,一般项目都有多个服务器,应用启动的时候获得本地列表中所有IP的ping值,然后将Dev_URL中的HOST修改为我们找到的最快的IP。另外,这个本地IP列表也会需要通过一个API来维护,一般是每天第一次启动的时候ping一下,然后更新到本地。 4,比较大的数据压缩再上传。


数据持久化:

首先有非常多的方案可供选择: 1、NSUserDefault: 一般是小规模数据,弱业务相关数据,NSUserDefault变大会影响App启动的时间,敏感数据不要放NSUserDefault,虽然NSUserDefault的存取真的是很方便。 2、KeyChain:Keychain是苹果提供的带有可逆加密的存储机制,普遍用在各种存密码的需求上。另外,由于App卸载只要系统不重装,Keychain中的数据依旧能够得到保留,以及可被iCloud同步的特性,大家都会在这里存储用户唯一标识串。所以有需要加密、需要存iCloud的敏感小数据,一般都会放在Keychain。 3、File:主要包括:

1,Plist 2,archive(归档):只适合存一些不经常使用的,大量的数据,读取之后直接换变为对象/直接将对象存储(需要支持) 3,Stream(直接存文件):适合数据较大且经常使用,但是文件一般都是遍历才能拿到,所以建议为文件建立数据库索引

4、基于数据库的无数子方案(YYCache,FMDB,sqlite,CoreData[本人不喜欢用])。数据库中的数据应该都是强业务相关的,并且不能是很大的文件,比如一个大图片或者视频之类的,一般是存文件,然后数据中存放文件路径这样配合,而推荐直接使用YYCache,一站式服务。

因此,当有需要持久化的需求的时候,我们首先考虑的是应该采用什么手段去进行持久化。

数据库记得做线程处理(原理跟iOS的属性的线程安全一样) 比如SQLite库就推荐使用Serialized(默认):串行队列访问,虽然会慢一丢丢,但是方便易维护。

持久层有专门负责对接View层模块或业务的DataCenter,它们之间通过Record来进行交互。DataCenter向上层提供业务友好的接口,这一般都是强业务:比如根据用户筛选条件返回符合要求的数据等。然后DataCenter在这个接口里面调度各个Table,做一系列的业务逻辑,最终生成record对象,交付给View层业务。

DataCenter为了要完成View层交付的任务,会涉及数据组装和跨表的数据操作。数据组装因为View层要求的不同而不同,因此是强业务。跨表数据操作本质上就是各单表数据操作的组合,DataCenter负责调度这些单表数据操作从而获得想要的基础数据用于组装。那么,这时候单表的数据操作就属于弱业务,这些弱业务就由Table映射对象来完成。 Table对象通过QueryCommand来生成相应的SQL语句,并交付给数据库引擎去查询获得数据,然后交付给DataCenter。

差不多就这些,等重构完再补充~