代码规范&&架构&&设计

#pragma mark - life cycle
#pragma mark - UITableViewDelegate
#pragma mark - UITableViewDataSource
#pragma mark - event response
#pragma mark - private methods
#pragma mark - getters and setters

#pragma mark - life cycle
- (void)viewDidLoad
{
    [super viewDidLoad];

    self.view.backgroundColor = [UIColor whiteColor];
    [self.view addSubview:self.firstTableView];
    [self.view addSubview:self.secondTableView];
    [self.view addSubview:self.firstFilterLabel];
    [self.view addSubview:self.secondFilterLabel];
    [self.view addSubview:self.cleanButton];
    [self.view addSubview:self.originImageView];
    [self.view addSubview:self.processedImageView];
    [self.view addSubview:self.activityIndicator];
    [self.view addSubview:self.takeImageButton];
}

这样即便在属性非常多的情况下,还是能够保持代码整齐,view的初始化都交给getter去做了。总之就是尽量不要出现以下的情况:

{
    [super viewDidLoad];

    self.textLabel = [[UILabel alloc] init];
    self.textLabel.textColor = [UIColor blackColor];
    self.textLabel ... ...
    self.textLabel ... ...
    self.textLabel ... ...
    [self.view addSubview:self.textLabel];
}

这种做法就不够干净,都扔到getter里面去就好了

getter和setter全部都放在最后

因为一个ViewController很有可能会有非常多的view,就像上面给出的代码样例一样,如果getter和setter写在前面,就会把主要逻辑扯到后面去,其他人看的时候就要先划过一长串getter和setter,这样不太好。然后要求业务工程师写代码的时候按照顺序来分配代码块的位置,先是life cycle,然后是Delegate方法实现,然后是event response,然后才是getters and setters。这样后来者阅读代码时就能省力很多。

每一个delegate都把对应的protocol名字带上,delegate方法不要到处乱写,写到一块区域里面去

比如UITableViewDelegate的方法集就老老实实写上#pragma mark - UITableViewDelegate。这样有个好处就是,当其他人阅读一个他并不熟悉的Delegate实现方法时,他只要按住command然后去点这个protocol名字,Xcode就能够立刻跳转到对应这个Delegate的protocol定义的那部分代码去,就省得他到处找了。

event response专门开一个代码区域

所有button、gestureRecognizer的响应事件都放在这个区域里面,不要到处乱放。

关于private methods,正常情况下ViewController里面不应该写

不是delegate方法的,不是event response方法的,不是life cycle方法的,就是private method了。对的,正常情况下ViewController里面一般是不会存在private methods的,这个private methods一般是用于日期换算、图片裁剪啥的这种小功能。这种小功能要么把它写成一个category,要么把他做成一个模块,哪怕这个模块只有一个函数也行。

ViewController基本上是大部分业务的载体,本身代码已经相当复杂,所以跟业务关联不大的东西能不放在ViewController里面就不要放。另外一点,这个private method的功能这时候只是你用得到,但是将来说不定别的地方也会用到,一开始就独立出来,有利于将来的代码复用。

为什么要这样要求?

定义好这个规范,就能使得ViewController条理清晰,业务方程序员很能够区分哪些放在ViewController里面比较合适,哪些不合适。另外,也可以提高代码的可维护性和可读性。

继承的3大要点:

  1. 父类只是给子类提供服务,并不涉及子类的业务逻辑
  Object并不影响Model, View, Controller的执行逻辑和业务  
    Object为子类提供基础服务,例如内存计数等

    ApiManager并不影响其他的Manager  
    ApiManager只是给派生的Manager提供服务而已,ApiManager做的只会是份内的是,对于子类做的事情不参与。
  1. 层级关系明显,功能划分清晰,父类和子类各做各的。
  Object并不参与MVC的管理中,那些都只是各自派生类自己要处理的事情

    DetailManager, ListManager, CityManager都只是处理各自业务的对象  
    ApiManager并不应该涉足对应的业务。
  1. 父类的所有变化,都需要在子类中体现,也就是说此时耦合已经成为需求
 Object对类的描述,对内存引用的计数方式等,都是普遍影响派生类的。  
  
    ApiManager中对于网络请求的发起,网络状态的判断,是所有派生类都需要的。  
    此时,牵一发动全身就已经成为了需求,是适用继承的

总结

可见,代码复用也是分类别的,如果当初只是出于代码复用的目的而不区分类别和场景,就采用继承是不恰当的。我们应当考虑以上3点要素看是否符合,才能决定是否使用继承。就目前大多数的开发任务来看,继承出现的场景不多,主要还是代码复用的场景比较多,然而通过组合去进行代码复用显得要比继承麻烦一些,因为组合要求你有更强的抽象能力,继承则比较符合直觉。然而从未来可能产生的需求变化和维护成本来看,使用组合其实是很值得的。另外,当你发现你的继承超过2层的时候,你就要好好考虑是否这个继承的方案了,第三层继承正是滥用的开端。确定有必要之后,再进行更多层次的继承。

中心思想:万不得已不要用继承,优先考虑组合

关于View的布局

Autolayout这边可以考虑使用Masonry,代码的可读性就能好很多。如果还有使用Frame的,可以考虑一下使用这个项目

何时使用storyboard,何时使用nib,何时使用代码写View

所以在针对View层这边的要求时,我也是建议不要用StoryBoard。实现简单的东西,用Code一样简单,实现复杂的东西,Code比StoryBoard更简单。所以我更加提倡用code去画view而不是storyboard。

是否有必要让业务方统一派生ViewController

有的时候我们出于记录用户操作行为数据的需要,或者统一配置页面的目的,会从UIViewController里面派生一个自己的ViewController,来执行一些通用逻辑。比如客户端要求所有的ViewController都要继承自TMViewController。这个统一的父类里面针对一个ViewController的所有生命周期都做了一些设置,至于这里都有哪些设置对于本篇文章来说并不重要。在这里我想讨论的是,在设计View架构时,如果为了能够达到统一设置或执行统一逻辑的目的,使用派生的手段是有必要的吗?

不使用派生!
使用派生比不使用派生更容易增加业务方的使用成本
不使用派生手段一样也能达到统一设置的目的

为什么使用了派生,业务方的使用成本会提升?

  • 集成成本
  • 上手接受成本
  • 架构的维护难度

使用派生比不使用派生更容易增加业务方的使用成本
不使用派生手段一样也能达到统一设置的目的

那么如果不使用派生,我们应该使用什么手段?

不赞成Category的过度使用,但鉴于Category是最典型的化继承为组合的手段,在这个场景下还是适合使用的。还有的就是,关于Method Swizzling手段实现方法拦截,业界也已经有了现成的开源库:Aspects,我们可以直接拿来使用。

MVC

  • M应该做的事:

    给ViewController提供数据
    给ViewController存储数据提供接口
    提供经过抽象的业务基本组件,供Controller调度

  • C应该做的事:

    管理View Container的生命周期
    负责生成所有的View实例,并放入View Container
    监听来自View与业务有关的事件,通过与Model的合作,来完成对应事件的业务。

  • V应该做的事:

    响应与业务无关的事件,并因此引发动画效果,点击反馈(如果合适的话,尽量还是放在View去做)等。
    界面元素表达

MVCS

  • 什么叫胖Model?
    胖Model包含了部分弱业务逻辑。胖Model要达到的目的是,Controller从胖Model这里拿到数据之后,不用额外做操作或者只要做非常少的操作,就能够将数据直接应用在View上。举个例子
Raw Data:
    timestamp:1234567

FatModel:
    @property (nonatomic, assign) CGFloat timestamp;
    - (NSString *)ymdDateString; // 2015-04-20 15:16
    - (NSString *)gapString; // 3分钟前、1小时前、一天前、2015-3-13 12:34

Controller:
    self.dateLabel.text = [FatModel ymdDateString];
    self.gapLabel.text = [FatModel gapString];

把timestamp转换成具体业务上所需要的字符串,这属于业务代码,算是弱业务。FatModel做了这些弱业务之后,Controller就能变得非常skinny,Controller只需要关注强业务代码就行了。众所周知,强业务变动的可能性要比弱业务大得多,弱业务相对稳定,所以弱业务塞进Model里面是没问题的。另一方面,弱业务重复出现的频率要大于强业务,对复用性的要求更高,如果这部分业务写在Controller,类似的代码会洒得到处都是,一旦弱业务有修改(弱业务修改频率低不代表就没有修改),这个事情就是一个灾难。如果塞到Model里面去,改一处很多地方就能跟着改,就能避免这场灾难。

然而其缺点就在于,胖Model相对比较难移植,虽然只是包含弱业务,但好歹也是业务,迁移的时候很容易拔出萝卜带出泥。另外一点,MVC的架构思想更加倾向于Model是一个Layer,而不是一个Object,不应该把一个Layer应该做的事情交给一个Object去做。最后一点,软件是会成长的,FatModel很有可能随着软件的成长越来越Fat,最终难以维护。

  • 什么叫瘦Model?
    瘦Model只负责业务数据的表达,所有业务无论强弱一律扔到Controller。瘦Model要达到的目的是,尽一切可能去编写细粒度Model,然后配套各种helper类或方法来对弱业务做抽象,强业务依旧交给Controller。举个例子:
Raw Data:
{
    "name":"casa",
    "sex":"male",
}

SlimModel:
    @property (nonatomic, strong) NSString *name;
    @property (nonatomic, strong) NSString *sex;

Helper:
    #define Male 1;
    #define Female 0;
    + (BOOL)sexWithString:(NSString *)sex;

Controller:
    if ([Helper sexWithString:SlimModel.sex] == Male) {
        ...
    }

由于SlimModel跟业务完全无关,它的数据可以交给任何一个能处理它数据的Helper或其他的对象,来完成业务。在代码迁移的时候独立性很强,很少会出现拔出萝卜带出泥的情况。另外,由于SlimModel只是数据表达,对它进行维护基本上是0成本,软件膨胀得再厉害,SlimModel也不会大到哪儿去。

缺点就在于,Helper这种做法也不见得很好,这里有一篇文章批判了这个事情。另外,由于Model的操作会出现在各种地方,SlimModel在一定程度上违背了DRY(Don't Repeat Yourself)的思路,Controller仍然不可避免在一定程度上出现代码膨胀

MVCS是基于瘦Model的一种架构思路,把原本Model要做的很多事情中的其中一部分关于数据存储的代码抽象成了Store,在一定程度上降低了Controller的压力。

MVVM

在MVC的基础上,把C拆出一个ViewModel专门负责数据处理的事情,就是MVVM。然后,为了让View和ViewModel之间能够有比较松散的绑定关系,于是我们使用ReactiveCocoa
所以Controller在MVVM中,一方面负责View和ViewModel之间的绑定,另一方面也负责常规的UI逻辑处理。

拆分的心法

  • 第一心法:保留最重要的任务,拆分其它不重要的任务
    在iOS开发领域内,UIViewController承载了非常多的事情,比如View的初始化,业务逻辑,事件响应,数据加工等等,当然还有更多我现在也列举不出来,但是我们知道有一件事情Controller肯定逃不掉要做:协调V和M。也就是说,不管怎么拆,协调工作是拆不掉的。

那么剩下的事情我们就可以拆了,比如UITableView的DataSource。唐巧的博客有一篇文章提到他和另一个工程师关于是否要拆分DataSource争论了好久。拆分DataSource这个做法应该也算是通用做法,在不复杂的应用里面,它可能确实看上去只是一个数组而已,但在复杂的情况下,它背后可能涉及了文件内容读取,数据同步等等复杂逻辑,这篇文章的第一节就提倡了这个做法,我其实也蛮提倡的。

前面的文章里面也提了很多能拆的东西,我就不搬运了,大家可以进去看看。除了这篇文章提到的内容以外,任何比较大的,放在ViewController里面比较脏的,只要不是Controller的核心逻辑,都可以考虑拆出去,然后在架构的时候作为一个独立模块去定义,以及设计实现。

  • 第二心法:拆分后的模块要尽可能提高可复用性,尽量做到DRY
    根据第一心法拆开来的东西,很有可能还是强业务相关的,这种情况有的时候无法避免。但我们拆也要拆得好看,拆出来的部分最好能够归成某一类对象,然后最好能够抽象出一个通用逻辑出来,使他能够复用。即使不能抽出通用逻辑,那也尽量抽象出一个protocol,来实现IOP。这里有篇关于IOP的文章,大家看了就明白优越性了。

  • 第三心法:要尽可能提高拆分模块后的抽象度

也就是说,拆分的粒度要尽可能大一点,封装得要透明一些。唐巧说一切隐藏都是对代码复杂性的增加,除非它带来了好处,这在一定程度上有点道理,没有好处的隐藏确实都不好(笑)。提高抽象度事实上就是增加封装的力度,将一个负责的业务抽象成只需要很少的输入就能完成,就是高度抽象。嗯,继承很多层,这种做法虽然也提高了抽象程度,但我不建议这么玩。我不确定唐巧在这里说的隐藏跟我说的封装是不是同一个概念,但我在这里想提倡的是尽可能提高抽象程度。

提高抽象程度的好处在于,对于业务方来说,他只需要收集很少的信息(最小充要条件),做很少的调度(Controller负责大模块调度,大模块里面再去做小模块的调度),就能够完成任务,这才是给Controller减负的正确姿势。

如果拆分出来的模块抽象程度不够,模块对外界要求的参数比较多,那么在Controller里面,关于收集参数的代码就会多了很多。如果一部分参数的收集逻辑能够由模块来完成,那也可以做到帮Controller减轻负担。否则就感觉拆得不太干净,因为Controller里面还是多了一些不必要的参数收集逻辑。

如果拆分出来的粒度太小,Controller在完成任务的时候调度代码要写很多,那也不太好。导致拆分粒度小的首要因素就是业务可能本身就比较复杂,拆分粒度小并不是不好,能大就大一点,如果小了,那也没问题。针对这种情况的处理,就需要采用strategy模式。

针对拆分粒度小的情况,我来举个实际例子,这个例子来源于我的一个朋友他在做聊天应用的消息发送模块。当消息是文字时,直接发送。当消息是图片时,需要先向服务器申请上传资源,获得资源ID之后再上传图片,上传图片完成之后拿到图片URL,后面带着URL再把信息发送出去。

这时候我们拆模块,可以拆成:数据发送(叫A模块),上传资源申请(叫B模块),内容上传(叫C模块)。那么要发送文字消息,Controller调度A就可以了。如果要发送图片消息,Controller调度B->C->A,假设将来还有上传别的类型消息的任务,他们又要依赖D/E/F模块,那这个事情就很蛋疼,因为逻辑复杂了,Controller要调度的东西要区分的情况就多了,Controller就膨胀了。

那么怎么处理呢?可以采用Strategy模式。我们再来分析一下,Controller要完成任务,它初始情况下所具有的条件是什么?它有这条消息的所有数据,也知道这个消息的类型。那么它最终需要的是什么呢?消息发送的结果:发送成功或失败。

                   send msg
    Controller ------------------> MessageSender
        ^                                |
        |                                |
        |                                |
        ----------------------------------
                 success / fail

上面就是我们要实现的最终结果,Controller只要把消息丢给MessageSender,然后让MessageSender去做事情,做完了告诉Controller就好了。那么MessageSender里面怎么去调度逻辑?MessageSender里面可以有一个StrategyList,里面存放了表达各种逻辑的Block或者Invocation(Target-Action)。那么我们先定义一个Enum,里面规定了每种任务所需要的调度逻辑。

typedef NS_ENUM (NSUInteger, MessageSendStrategy)
{
    MessageSendStrategyText = 0,
    MessageSendStrategyImage = 1,
    MessageSendStrategyVoice = 2,
    MessageSendStrategyVideo = 3
}

然后在MessageSender里面的StrategyList是这样:

@property (nonatomic, strong) NSArray *strategyList;

self.strategyList = @[TextSenderInvocation, ImageSenderInvocation, VoiceSenderInvocation, VideoSenderInvocation];

// 然后对外提供一个这样的接口,同时有一个delegate用来回调

- (void)sendMessage:(BaseMessage *)message withStrategy:(MessageSendStrategy)strategy;

@property (nonatomic, weak) id delegate;

@protocol MessageSenderDelegate

  @required
      - (void)messageSender:(MessageSender *)messageSender
      didSuccessSendMessage:(BaseMessage *)message
                   strategy:(MessageSendStrategy)strategy;

      - (void)messageSender:(MessageSender *)messageSender
         didFailSendMessage:(BaseMessage *)message
                   strategy:(MessageSendStrategy)strategy
                      error:(NSError *)error;
@end

Controller里面是这样使用的:

 [self.messageSender sendMessage:message withStrategy:MessageSendStrategyText];

MessageSender里面是这样的:

    [self.strategyList[strategy] invoke];

然后在某个Invocation里面,就是这样的:

    [A invoke];
    [B invoke];
    [C invoke];

这样就好啦,即便拆分粒度因为客观原因无法细化,那也能把复杂的判断逻辑和调度逻辑从Controller中抽出来,真正为Controller做到了减负。总之能够做到大粒度就尽量大粒度,实在做不到那也行,用Strategy把它hold住。这个例子是小粒度的情况,大粒度的情况太简单,我就不举了。

设计心法

  • 第一心法:尽可能减少继承层级,涉及苹果原生对象的尽量不要继承

继承是罪恶,尽量不要继承。就我目前了解到的情况看,除了安居客的Pad App没有在框架级针对UIViewController有继承的设计以外,其它公司或多或少都针对UIViewController有继承,包括安居客iPhone app(那时候我已经对此无能为力,可见View的架构在一开始就设计好有多么重要)。甚至有的还对UITableView有继承,这是一件多么令人发指,多么惨绝人寰,多么丧心病狂的事情啊。虽然不可避免的是有些情况我们不得不从苹果原生对象中继承,比如UITableViewCell。但我还是建议尽量不要通过继承的方案来给原生对象添加功能,前面提到的Aspect方案和Category方案都可以使用。用Aspect+load来实现重载函数,用Category来实现添加函数,当然,耍点手段用Category来添加property也是没问题的。这些方案已经覆盖了继承的全部功能,而且非常好维护,对于业务方也更加透明,何乐而不为呢。

不用继承可能在思路上不会那么直观,但是对于不使用继承带来的好处是足够顶得上使用继承的坏处的。顺便在此我要给Category正一下名:业界对于Category的态度比较暧昧,在多种场合(讲座、资料文档)都宣扬过尽可能不要使用Category。它们说的都有一定道理,但我认为Category是苹果提供的最好的使用集合代替继承的方案,但针对Category的设计对架构师的要求也很高,请合理使用。而且苹果也在很多场合使用Category,来把一个原本可能很大的对象,根据不同场景拆分成不同的Category,从而提高可维护性。

不使用继承的好处我在这里已经说了,放到iOS应用架构来看,还能再多额外两个好处:1. 在业务方做业务开发或者做Demo时,可以脱离App环境,或花更少的时间搭建环境。2. 对业务方来说功能更加透明,也符合业务方在开发时的第一直觉。

  • 第二心法:做好代码规范,规定好代码在文件中的布局,尤其是ViewController

这主要是为了提高可维护性。在一个文件非常大的对象中,尤其要限制好不同类型的代码在文件中的布局。比如在写ViewController时,我之前给团队制定的规范就是前面一段全部是getter setter,然后接下来一段是life cycle,viewDidLoad之类的方法都在这里。然后下面一段是各种要实现的Delegate,再下面一段就是event response,Button的或者GestureRecognizer的都在这里。然后后面是private method。一般情况下,如果做好拆分,ViewController的private method那一段是没有方法的。后来随着时间的推移,我发现开头放getter和setter太影响阅读了,所以后面改成全放在ViewController的最后。

  • 第三心法:能不放在Controller做的事情就尽量不要放在Controller里面去做

Controller会变得庞大的原因,一方面是因为Controller承载了业务逻辑,MVC的总结者(在正式提出MVC之前,或多或少都有人这么设计,所以说MVC的设计者不太准确)对Controller下的定义也是承载业务逻辑,所以Controller就是用来干这事儿的,天经地义。另一方面是因为在MVC中,关于Model和View的定义都非常明确,很少有人会把一个属于M或V的东西放到其他地方。然后除了Model和View以外,还会剩下很多模棱两可的东西,这些东西从概念上讲都算Controller,而且由于M和V定义得那么明确,所以直觉上看,这些东西放在M或V是不合适的,于是就往Controller里面塞咯。

正是由于上述两方面原因导致了Controller的膨胀。我们再细细思考一下,Model膨胀和View膨胀,要针对它们来做拆分其实都是相对容易的,Controller膨胀之后,拆分就显得艰难无比。所以如果能够在一开始就尽量把能不放在Controller做的事情放到别的地方去做,这样在第一时间就可以让你的那部分将来可能会被拆分的代码远离业务逻辑。所以我们要稍微转变一下思路:模棱两可的模块,就不要塞到Controller去了,塞到V或者塞到M或者其他什么地方都比塞进Controller好,便于将来拆分

所以关于前面我按下不表的关于胖Model和瘦Model的选择,我的态度是更倾向于胖Model。客观地说,业务膨胀之后,代码规模肯定少不了的,不管你技术再好,经验再丰富,代码量最多只能优化,该膨胀还是要膨胀的,而且优化之后代码往往也比较难看,使用各种奇技淫巧也是有代价的。所以,针对代码量优化的结果,往往要么就是牺牲可读性,要么就是牺牲可移植性(通用性),Every magic always needs a pay, you have to make a trade-off.

那么既然膨胀出来的代码,或者将来有可能膨胀的代码,不管放在MVC中的哪一个部分,最后都是要拆分的,既然迟早要拆分,那不如放Model里面,这样将来拆分胖Model也能比拆分胖Cotroller更加容易。在我还在安居客的时候,安居客Pad app承载最复杂业务的ViewController才不到600行,其他多数Controller都是在300-400行之间,这就为后面接手的人降低了非常多的上手难度和维护复杂度。拆分出来的东西都是可以直接迁移给iPhone app使用的。现在看天猫的ViewControler,动不动就几千行,看不了多久头就晕了,问了一下,大家都表示很习惯这样的代码长度,摊手。

  • 第四心法:架构师是为业务工程师服务的,而不是去使唤业务工程师的

架构师在公司里的职级和地位往往都是要高于业务工程师的,架构师的技术实力和经验往往也都是高于业务工程师的。所以你值得在公司里获得较高的地位,但是在公司里的地位高不代表在软件工程里面的角色地位也高。架构师是要为业务工程师服务的,是他们使唤你而不是你使唤他们。另外,制定规范一方面是起到约束业务工程师的代码,但更重要的一点是,这其实是利用你的能力帮助业务工程师避免他无法预见的危机,所以地位高有一定的好处,毕竟夏虫不可语冰,有的时候不见得能够解释得通,因此高地位随之而来的就是说服力会比较强。但在软件工程里,一定要保持谦卑,一定要多为业务工程师考虑。

一个不懂这个道理的架构师,设计出来的东西往往复杂难用,因为他只愿意做核心的东西,周边不愿意做的都期望交给业务工程师去做,甚至有的时候就只做了个Demo,然后就交给业务工程师了,业务工程师变成给他打工的了。但是一个懂得这个道理的架构师,设计出来的东西会非常好用,业务方只需要扔很少的参数然后拿结果就好了,这样的架构才叫好的架构。

举一个保存图片到本地的例子,一种做法是提供这样的接口:- (NSString *)saveImageWithData:(NSData *)imageData,另一种是- (NSString *)saveImage:(UIImage *)image。后者更好,原因自己想。

你的态度越谦卑,就越能设计出好的架构,这是我设计心法里的最后一条,也是最重要的一条。即使你现在技术实力不是业界大牛级别的,但只要保持这个心态去做架构,去做设计,就已经是合格的架构师了,要成为业界大牛也会非常快。

小总结

其实针对View层的架构设计,还是要做好三点:代码规范,架构模式,工具集。

代码规范对于View层来说意义重大,毕竟View层非常重业务,如果代码布局混乱,后来者很难接手,也很难维护。

架构模式具体如何选择,完全取决于业务复杂度。如果业务相当相当复杂,那就可以使用VIPER,如果相对简单,那就直接MVC稍微改改就好了。每一种已经成为定式的架构模式不见得都适合各自公司对应的业务,所以需要各位架构师根据情况去做一些拆分或者改变。拆分一般都不会出现问题,改变的时候,只要别把MVC三个角色搞混就好了,M该做啥做啥,C该做啥做啥,V该做啥做啥,不要乱来。关于大部分的架构模式应该是什么样子,这篇文章里都已经说过了,不过我认为最重要的还是后面的心法,模式只是招术,熟悉了心法才能大巧不工。

View层的工具集主要还是集中在如何对View进行布局,以及一些特定的View,比如带搜索提示的搜索框这种。这篇文章只提到了View布局的工具集,其它的工具集相对而言是更加取决于各自公司的业务的,各自实现或者使用CocoaPods里现成的都不是很难。

对于小规模或者中等规模iOS开发团队来说,做好以上三点就足够了。在大规模团队中,有一个额外问题要考虑,就是跨业务页面调用方案的设计。

跨业务页面调用方案的设计

跨业务页面调用是指,当一个App中存在A业务,B业务等多个业务时,B业务有可能会需要展示A业务的某个页面,A业务也有可能会调用其他业务的某个页面。在小规模的App中,我们直接import其他业务的某个ViewController然后或者push或者present,是不会产生特别大的问题的。但是如果App的规模非常大,涉及业务数量非常多,再这么直接import就会出现问题。
那么应该怎样处理这个问题?

截屏2021-03-17 上午11.17.52.png

可以看出,跨业务的页面调用在多业务组成的App中会导致横向依赖。那么像这样的横向依赖,如果不去设法解决,会导致什么样的结果?

  1. 当一个需求需要多业务合作开发时,如果直接依赖,会导致某些依赖层上端的业务工程师在前期空转,依赖层下端的工程师任务繁重,而整个需求完成的速度会变慢,影响的是团队开发迭代速度。

  2. 当要开辟一个新业务时,如果已有各业务间直接依赖,新业务又依赖某个旧业务,就导致新业务的开发环境搭建困难,因为必须要把所有相关业务都塞入开发环境,新业务才能进行开发。影响的是新业务的响应速度。

  3. 当某一个被其他业务依赖的页面有所修改时,比如改名,涉及到的修改面就会特别大。影响的是造成任务量和维护成本都上升的结果。

那么应该怎样处理这个问题?

让依赖关系下沉。

怎么让依赖关系下沉?引入Mediator模式。

所谓引入Mediator模式来让依赖关系下沉,实质上就是每次呼唤页面的时候,通过一个中间人来召唤另外一个页面,这样只要每个业务依赖这个中间人就可以了,中间人的角色就可以放在业务层的下面一层,这就是依赖关系下沉。

截屏2021-03-17 上午11.18.18.png

当A业务需要调用B业务的某个页面的时候,将请求交给Mediater,然后由Mediater通过某种手段获取到B业务页面的实例,交还给A就行了。在具体实现这个机制的过程中,有以下几个问题需要解决:

  1. 设计一套通用的请求机制,请求机制需要跟业务剥离,使得不同业务的页面请求都能够被Mediater处理
  2. 设计Mediater根据请求如何获取其他业务的机制,Mediater需要知道如何处理请求,上哪儿去找到需要的页面

这个看起来就非常像我们web开发时候的URL机制,发送一个Get或Post请求,CGI调用脚本把请求分发给某个Controller下的某个Action,然后返回HTML字符串到浏览器去解析。苹果本身也实现了一套跨App调用机制,它也是基于URL机制来运转的,只不过它想要解决的问题是跨App的数据交流和页面调用,我们想要解决的问题是降低各业务的耦合度。

不过我们还不能直接使用苹果原生的这套机制,因为这套机制不能够返回对象实例。而我们希望能够拿到对象实例,这样不光可以做跨业务页面调用,也可以做跨业务的功能调用。另外,我们又希望我们的Mediater也能够跟苹果原生的跨App调用兼容,这样就又能帮业务方省掉一部分开发量。

关于Getter和Setter?
我比较习惯一个对象的"私有"属性写在extension里面,然后这些属性的初始化全部放在getter里面做,在init和dealloc之外,是不会出现任何类似_property这样的写法的。就是这样:

@interface CustomObject()
@property (nonatomic, strong) UILabel *label;
@end

@implement

#pragma mark - life cycle

- (void)viewDidLoad
{
    [super viewDidLoad];

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

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

  }

#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

总结

要做一个View层架构,主要就是从以下三方面入手:

制定良好的规范
选择好合适的模式(MVC、MVCS、MVVM、VIPER)
根据业务情况针对ViewController做好拆分,提供一些小工具方便开发
当然,你还会遇到其他的很多问题,这时候你可以参考这篇文章里提出的心法,在后面提到的跨业务页面调用方案的设计中,你也能够看到我的一些心法的影子。

对于iOS客户端来说,它并不像其他语言诸如Python、PHP他们有那么多的非官方通用框架。客观原因在于,苹果已经为我们做了非常多的事情,做了很多的努力。在苹果已经做了这么多事情的基础上,架构师要做针对View层的方案时,最好还是尽量遵守苹果已有的规范和设计思想,然后根据自己过去开发iOS时的经验,尽可能给业务方在开发业务时减负,提高业务代码的可维护性,就是View层架构方案的最大目标。

关于AOP

AOP(Aspect Oriented Programming),面向切片编程,这也是面向XX编程系列术语之一哈,但它跟我们熟知的面向对象编程没什么关系。

什么是切片?

程序要完成一件事情,一定会有一些步骤,1,2,3,4这样。这里分解出来的每一个步骤我们可以认为是一个切片。

什么是面向切片编程?

你针对每一个切片的间隙,塞一些代码进去,在程序正常进行1,2,3,4步的间隙可以跑到你塞进去的代码,那么你写这些代码就是面向切片编程。

为什么会出现面向切片编程?

你要想做到在每一个步骤中间做你自己的事情,不用AOP也一样可以达到目的,直接往步骤之间塞代码就好了。但是事实情况往往很复杂,直接把代码塞进去,主要问题就在于:塞进去的代码很有可能是跟原业务无关的代码,在同一份代码文件里面掺杂多种业务,这会带来业务间耦合。为了降低这种耦合度,我们引入了AOP。

如何实现AOP?

AOP一般都是需要有一个拦截器,然后在每一个切片运行之前和运行之后(或者任何你希望的地方),通过调用拦截器的方法来把这个jointpoint扔到外面,在外面获得这个jointpoint的时候,执行相应的代码。

在iOS开发领域,objective-C的runtime有提供了一系列的方法,能够让我们拦截到某个方法的调用,来实现拦截器的功能,这种手段我们称为Method Swizzling。Aspects通过这个手段实现了针对某个类和某个实例中方法的拦截。

另外,也可以使用protocol的方式来实现拦截器的功能,具体实现方案就是这样:

@protocol RTAPIManagerInterceptor 

@optional
- (void)manager:(RTAPIBaseManager *)manager beforePerformSuccessWithResponse:(AIFURLResponse *)response;
- (void)manager:(RTAPIBaseManager *)manager afterPerformSuccessWithResponse:(AIFURLResponse *)response;

- (void)manager:(RTAPIBaseManager *)manager beforePerformFailWithResponse:(AIFURLResponse *)response;
- (void)manager:(RTAPIBaseManager *)manager afterPerformFailWithResponse:(AIFURLResponse *)response;

- (BOOL)manager:(RTAPIBaseManager *)manager shouldCallAPIWithParams:(NSDictionary *)params;
- (void)manager:(RTAPIBaseManager *)manager afterCallingAPIWithParams:(NSDictionary *)params;

@end

@interface RTAPIBaseManager : NSObject

@property (nonatomic, weak) id interceptor;

@end

这么做对比Method Swizzling有个额外好处就是,你可以通过拦截器来给拦截器的实现者提供更多的信息,便于外部实现更加了解当前切片的情况。另外,你还可以更精细地对切片进行划分。Method Swizzling的切片粒度是函数粒度的,自己实现的拦截器的切片粒度可以比函数更小,更加精细。

缺点就是,你得自己在每一个插入点把调用拦截器方法的代码写上,通过Aspects(本质上就是Mehtod Swizzling)来实现的AOP,就能轻松一些

我转载这篇文章用于学习。
文章来源 :CasaTaloyum

你可能感兴趣的:(代码规范&&架构&&设计)