PPPan's 平凡之路

做一个互联网内容的贡献者

PPPan's 平凡之路 一个技术博客覆盖范围包括 iOS Objecticve-C Swift Xcode 等


iOS项目架构 - 模块化

###当我们谈论iOS架构的时候我们在谈论什么

  1. 模块化
  2. 规范化
  3. 统一行为

本文主要就以上模块化,以网络请求模块的抽取为例稍作讨论。

###模块化
模块化的目的是实现解耦,提升模块、组件的复用性。一个很简单的例子,App需要与服务器交换数据,最差的做法,是把网络请求、回调、业务处理、界面操作全部写在ViewController里。如果把网络请求抽取出来,建一个Webservice类,仅负责最基本的RESTful请求,并在回调中处理好类似404,无网络等各种网络状况。那么,这个类就变成了通用的网络服务处理模块,便于维护和复用。

###遇到的问题
在划分模块的一个前提是,模块不要对其他层有依赖性。最好的情况是,这个模块拿到任意一个项目都可以直接使用。在这一点上,我遇到了一个值得思考的问题。

在iOS客户端里,网络请求可以分为两种,一种是静默请求,也就是在用户不知情的情况下默默地进行网络请求处理。典型的例子有微信朋友圈点赞。用户在点赞时,UI直接进行点赞成功的反馈,同时网络请求在静默进行,不阻碍用户的正常操作。另一种是显示请求,用户需等待操作完成后才能进行下一步操作。典型的例子一种是下拉刷新,另一种是支付宝付款。付款的请求中,UI暂时显示一个Loading图标,用以提示用户程序正在处理,而非卡死(设计思想:反馈 ——《iOS Human Interface Guidelines》)。

遇到的选择,是在第二种显示请求的情况下如何更好地封装。我所负责的这个项目,除网络请求外,并没有耗时很长的I/O等情况需要处理。因此,有两个方案。

将UI的Loading封装在WebService中,仅暴露isShowIndicatortoView两个参数在网络请求的接口中

- (void)requestWithPath:(NSString *)path
             parameters:(id)parameters
        isShowIndicator:(BOOL) isShowIndicator
                  toView:(UIView *)toView
               labelText:(NSString *)labelText
                 success:(SuccessBlock)success
                 failure:(FailureBlock)failure;

或者,将UI的Loading封装在BaseViewController中,作为一个ViewController的基础行为,相应的业务模块需要显示请求时,调用显示指示器方法。

@interface BaseViewController : UIViewController

- (void)showTips:(NSString *)tips;
- (void)showIndicatorWithTitle:(NSString *)title;
- (void)hideIndicator;

@end

前一种方式在写具体的业务逻辑时非常方便。只需要在调用前考虑一下这个请求是否为静默请求,然后将BOOLView参数传入此方法中即可。这样封装的==好处==是,可以在网络层的回调中,处理好指示器的隐藏,以免出现忘记调用Hide方法的情况,并且ViewController不必因为需要这两个方法,就继承BaseViewController 。==缺点==是不够灵活,如果有不同的业务需要处理不同的显示,隐藏方式,还是需要在ViewController中重新写一下。

后一种方式在写具体的业务时==缺点==是略微麻烦一些,需要考虑好相对应业务具体情况,具体调用显示隐藏指示器,优点是灵活,并且做到了网络层的去UI依赖

就MVC模式来说,UI响应数据的处理,应当由Controller负责调度。苹果自己在设计的时候,也都遵循了这一设计模式。WebService层,可以算作Model层的一个工具。而Model层与View层,在iOS的设计上是绝对不允许直接交互的。引用一张斯坦福大学iOS7公开课上的图来形象地展示一下。

Model和View之间是双黄线,就像开车一样,绝对不能压过双黄线 :)至于为什么,我们以后讨论到MVC设计模式再详细讨论。

因此,在WebService中封装了View,实际上是在Model层操作View。多了这么一个View,多出来的事可就多了。我们不仅需要Import进UIKit,还需要Import通用的Indicator。这一依赖就产生了模块之间的依赖,原本为了解耦合而提取的WebService层反而耦合了Indicator。同时,也造成Webservice的粒度增大。需要处理的事情变得更多。

反观第二种方法,需要所有ViewController继承BaseViewController。继承本身是紧耦合的。然而纵向的紧耦合有时候减少了重复工作,将一些相同的行为,做统一化的处理,并且没有造成横向跨层的依赖。类似于无网络时的处理,网络请求出错时的处理,Token过期的处理,都应该放在BaseViewController里面。因为这是所有Controller都需要面对的问题。因此,继承BaseViewController是必要的。在统一行为之余,把这几个方法封装在BaseViewController中,又让WebService层实现了细颗粒的模块化组件化。同时,Controller本身就需要控制View,不会因此Import进许多不相关的模块。所需要付出的仅仅是在网络请求结束的时候别忘记调用Hide方法,不然测试小妹的“界面失去响应”BUG就会指派到我们头上 :(

最终,我们的Webservice是封装成这样的:

/**
 *  post a request to server
 *
 *  @param path       the request path append at BASEURL
 *  @param parameters parameters
 *  @param token      Authorization token
 *  @param success    when success goto this block
 *  @param failure    when failure goto this block
 */    
- (void)postWithPath:(NSString *)path
     parameters:(NSDictionary *)parameters
          token:(NSString *)token
        success:(void (^)(id JSON))success
        failure:(void (^)(NSError *error, id JSON))failure;

综上所述,模块化所需要遵循的思想就是:轻,少,专。其实思想和我们写方法的时候考虑的“一个方法只做一件事”有类似的地方。

That’s all,hope you enjoy it :)

Newer Post

iOS项目架构 - 规范

记得刚工作的时候,我的Mentor带我参加某银行业务系统研发成果交流分享会。会议让我印象很深的一点,是关于规范。发言人在说规范的时候,举了他见过的这么一个例子: <div class="div1" ...> <div class="div2&quo …

继续阅读
Older Post

iOS的多Target应用

最近刚完成了一个多Target的项目,做了许多思考与选择,现写下心得总结,权当抛砖引玉。 ####项目背景本项目是一个适用于正在学车人群的预约学车平台。分为学生端和教练端。学生端主功能为预约学车,教练端主功能为排课。 什么时候选用多Target多target的选用,意在实现最大化重用代码。其经典情景 …

继续阅读