基于Objective-C的iOS架构初探

面向对象思想

S.O.L.I.D原则

作为面向对象编程和面向对象设计的五个基本原则,S.O.L.I.D的恰当运用,会使软件的维护和扩展变得更加容易。

  1. Single Responsibility Principle (SRP) – 单一职责原则
    单一职责原则的核心思想是:一个类,只做一件事情,且只有一个引起它变化的原因
    单一职责原则可以看作是低耦合、高内聚在面向对象原则上的引申,将职责定义为引起变化的原因,以提高内聚性来减少引起变化的原因。职责过多,可能引起它变化的原因就越多,这将导致职责依赖,相互之间就产生影响,从而极大的损伤其内聚性和耦合度。单一职责,通常意味着单一的功能,因此不要为一个模块实现过多的功能点,以保证实体只有一个引起它变化的原因。

  2. Open/Closed Principle (OCP) – 开闭原则
    开闭原则的核心思想是:模块是可扩展的,但是不可修改。即,对扩展是开放的,而对修改是封闭的。面向对象编程思想中,需要依赖抽象,而不是实现。

  • 对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。
  • 对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对类进行任何修改。
  1. Liskov Substitution Principle (LSP) – 里氏代换原则
    里氏代换原则要求,子类必须能够替换成他们的基类。即:子类应该可以替换任何基类能够出现的地方,并且经过替换以后,代码还能正常工作。
    里氏代换原则目的就是要保证继承关系的正确性。违反里氏代换原则意味着违反了开闭原则,反之未必。里氏代换原则是使代码符合开闭原则的一个重要保证。

经典例子:正方形不是长方形,鸵鸟不是鸟

  1. Interface Segregation Principle (ISP) – 接口隔离原则
    接口隔离原则意思是把功能实现在接口中,而不是类中,使用多个专门的接口比使用单一的总接口要好。
    采用接口隔离原则对接口进行约束时,要注意以下几点:
  • 接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不争的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。
  • 为依赖 的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。
  • 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。
  1. Dependency Inversion Principle (DIP) – 依赖倒置原则
    高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。(也体现了多态的思想)

小结:所有原则的制定,都源于对面向对象的三大特性(封装、继承和多态)的理解和使用。

封装

具备封装性的面向对象程序设计隐藏了某一方法的具体运行步骤,取而代之的是通过消息传递机制发送消息给它。封装是通过限制只有特定类的对象可以访问这一特定类的成员,而它们通常利用接口实现消息的传入传出。

  • 迪米特法则
    迪米特法则(Law of Demeter),又称“最少知识原则”(Principle of Least Knowledge。一个对象应该对其他对象保持最少的了解,以达到低耦合、高内聚的目的。

对于对象 ‘O’ 中一个方法’M’,M应该只能够访问以下对象中的方法:

  1. 对象O;
  2. 与O直接相关的Component Object;
  3. 由方法M创建或者实例化的对象;
  4. 作为方法M的参数的对象。

迪米特法则的初衷是降低类之间的耦合,由于每个类都减少了不必要的依赖,因此的确可以降低耦合关系。但是凡事都有度,虽然可以避免与非直接的类通信,但是要通信,必然会通过一个“中介”来发生联系。过分的使用迪米特原则,会产生大量这样的中介和传递类,导致系统复杂度变大。所以在采用迪米特法则时要反复权衡,既做到结构清晰,又要高内聚低耦合。

继承

继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码,能够大大的提高开发的效率。

我们在使用继承的时候,应该尽量遵循以下原则:

  1. 父类只是给子类提供服务,不涉及子类的业务逻辑
  2. 父类的所有实现,都是子类的需求
  3. 最好不要有超过三层的继承

由于继承是强耦合关系,如果过度使用,会导致代码之间的高耦合,牵一发而动全身,使用时应特别注意。举个栗子:
在项目中对于继承的过度使用,会导致代码之间的高耦合,牵一发而动全身。
比如我们在工程中有个DefaultThemeViewController的基类,用于设置通用的viewController样式,在viewWillAppear中对导航栏的样式做了基本设置:

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    [self setNavigationControllerTintColor];
    [self.navigationController setNavigationBarHidden:NO animated:YES];
    [[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleLightContent];
 }

对于大部分页面来说,可以通过继承该基类方便地获得默认的导航栏主题,而不用再做设置,但对于部分需要隐藏导航栏的页面来说,继承则会导致页面显示不符合需求。
对于这种情况,我们不应该在基类中实现诸如导航栏样式之类的定制需求。而应该提供接口供子类调用。

多态的使用

多态最重要的优点在于简化了编程接口,比如在进行网络交互时,通过对基础网络接口模型的方法重写,实现不同网络接口的解析。

XX网接口模型基类

/**
 *  XX网接口模型基类
 */
@interface InterfaceModalXX
@property (strong, nonatomic) NSString* isSuccess;
@property (strong, nonatomic) NSString* responseCode;
@property (strong, nonatomic) NSString* resultMessage;

//内容解析
-(void)PaserChild:(NSDictionary*)resultDic;
@end

具体接口模型类

/**
 *  手机验证码
 */
@interface InterfaceCellPhoneVerifyCode : InterfaceModalXX

@end

@implementation InterfaceCellPhoneVerifyCode

//子类重写
-(void)PaserChild:(NSDictionary*)resultDic{
    self.resultMessage = [resultDic objectForKey:@"msg"];
    self.responseCode = [[resultDic objectForKey:@"code"] stringValue];
    self.isSuccess = [resultDic objectForKey:@"status"];
    
}
@end

在进行网络请求时,通过网络请求助手类,封装接口方法与服务器进行通信

/**
 获取手机验证码
 */
-(void)checkVerifyCode:(NSString*)phoneNum
              withCode:(NSString*)code
           withSuccess:(void(^)(InterfaceModal*))onSuccess//不同接口均统一返回基类类型
              withFail:(void(^)(NSString*))onFail;
-(void)checkVerifyCode:(NSString *)phoneNum
              withCode:(NSString *)code
           withSuccess:(void (^)(InterfaceModal *))onSuccess
              withFail:(void (^)(NSString *))onFail{
    NSString *URL = "XXX";
    [self asynRequestServerAndParser:URL withPostData:nil
                     withParserModal:(InterfaceModal*)[[InterfaceCellPhoneVerifyCode alloc]init]
                         withSuccess:onSuccess withFail:onFail];//底层根据调用方传入的实际接口类型,进行解析处理
}

恰当利用多态的特性,可以减少很多重复性的工作,让代码变得更为优雅。

架构模式

对于iOS应用的架构模式,一般可分为MVC,MVP,MVVM等,名称虽多,但都是在MVC的基础上演化而来。
    MVVM可以解决Controller臃肿的问题,通过实现数据的双向绑定,使Model和View解耦。但MVVM需要一定的学习成本,而且数据绑定也加大了框架的复杂度,以及带来调试的困难。在项目初期可以尝试选择,后期工程维护和扩展时慎重使用。
    MVC作为传统的MVC架构,View是可以直接访问Model的!从而,View里会包含Model信息,不可避免的还要包括一些业务逻辑。 在MVC模型里,更关注的Model的不变,而同时有多个对Model的不同显示,即View。所以,在MVC模型里,Model不依赖于View,但是View是依赖于Model的。不仅如此,因为有一些业务逻辑在View里实现了,导致要更改View也是比较困难的,至少那些业务逻辑是无法重用的。
    而苹果采用的iOS下的MVC模式相比传统的MVC有所改进,见下图比较:

Traditional MVC
Cocoa MVC

控制器Controller作为视图View和数据Model之间的中介,使它们之间不需要有关联。这样处理确实使得View和Model分离了,但也造成View和Controller紧紧地耦合在一起,很多复杂的业务逻辑、代理或者数据源等,都放在了Controller里,使得Controller层负担过重。所以在业务模式比较复杂的情况下,Cocoa MVC也越来越不能满足需求了。
    这里着重说一下MVP模式。

MVP

MVP模式看起来有些类似于Cocoa MVC,如下图:

MVP

在MVP模式下,视图控制器被归类为View层,只负责视图的布局,而业务逻辑都放在了Presenter,它与视图控制器的生命周期没有任何关联。这样就真正实现了业务逻辑与视图的解耦,可以更好地复用业务代码。
    对于项目工程结构,我们可以采用如下方式进行组织:

MVP目录结构
  • View
    对于View的构建,可以是纯代码方式,也可以是Nib方式,具体采用哪种,可参考唐巧的一篇文章,个人觉得在进行复杂布局,特别是要在不同设备上呈现相同效果时,使用nib布局比较好(比如复杂的登录界面:包括三方登录,注册,找回密码等功能),因为可以实时调试布局效果,熟悉之后修改也很方便。在进行一些基础页面对控件的添加和布局时,个人还是喜欢手写代码。

在使用Nib时注意控件/事件和Controller的绑定,特别是Controller中删除和修改之后,在Nib中也要对应修改。

View同ViewController一起组成Passive View,对视图进行展示和更新操作。我们在实现功能简单的ViewController时(很明确没有后期功能新增的情况下),可以直接在里面添加subview,做到代码清晰即可。对于复杂Controller,则将View单独分离。

//FontViewController.h
-(void)viewDidLoad {
    [super viewDidLoad];
    [self.view addSubview:self.fontTableView];
}
/
/
/
-(UITableView *)fontTableView{
    if (!_fontTableView) {
        _fontTableView = [[UITableView alloc]initWithFrame:CGRectMake(0, 0, DEVICE_WIDTH, DEVICE_HEIGHT)];
        [_fontTableView registerNib:[UINib nibWithNibName:@"FontTableViewCell" bundle:nil] forCellReuseIdentifier:fontCellReuseId];
        _fontTableView.delegate = self;
        _fontTableView.dataSource = self;
    }
    return _fontTableView;
}
  • Presenter
    Presenter作为逻辑控制层,从Model处取数据,运算和转化,最后用View来展示;Presenter完全把Model和View进行了分离,主要的程序逻辑在Presenter里实现。而且,Presenter与具体的View是没有直接关联的,而是通过定义好的接口进行交互,从而使得在变更View时候可以保持Presenter的不变,即重用。这也是MVP模式的精髓所在。
  • Model
    Model层进行本地数据的存储与读取,还可发送网络请求,处理返回结果。Model层的主要作用是完成对数据的解析,并供UI层使用。实际项目中可引入一些优秀的三方框架,如YYKitMJExtension等。

分层和模块化

无论是MVC、MVP还是MVVM,其实都是在讨论软件的分层方式,在实际运用中,并不一定非要限制自己非要用哪种模式,开发者需要自己抽象出实现某个功能所需要分离出的层次结构,同一项目中的功能可能根据不同的复杂程度,采取不同的模式。
复杂功能的实现如果只根据架构模式来生硬地套用,无论使用哪一种模式都会使代码变得臃肿和混乱,所以开发者应懂得对软件功能的细分,使代码变得更有组织和条理,也就是所谓的模块化编程。模块化编程具有以下优点:

  • 易设计:较大的复杂问题分解为若干较小的简单问题,使我们可以从抽象的模块功能角度而非具体的实现角度去理解软件系统,从而整个系统的结构非常清晰、容易理解,设计人员在设计之初可以更加关注系统的顶层逻辑而非底层细节。
  • 易实现:模块化设计适合团队开发,因为每个团队成员不需要了解系统全貌,只需 关注所分配的小任务。另外团队可以灵活地增加人手,新人只需直接接手某个模块,不会影响系统其他模块的开发。
  • 易测试:每个模块不但可以独立开发,也可以独立测试,最后组装时再进行联合测试。
  • 易维护:如果需要修改系统或者扩展系统功能,只需针对特定模块进行修改或者添加新模块。
  • 可重用:很多模块的代码都可以不加修改地用于其他程序的开发。
分层与模块化

跳出面向对象

如果简单的总结编程,就是表现在两个方面,数据和对数据的操作。面向对象编程将关注点放在对象上,即数据。让操作围绕着数据进行,这更符合人类的自然思维习惯,能更好的抽象出拟人化的思维过程,从而使代码结构变得清晰。面向对象的特性也让程序变得容易扩展,代码重用率高。

但实际软件开发过程中,纯粹的面向对象思想也带来了很多缺陷,比如过度封装导致的工作量增加,继承使用不当导致代码高度耦合,对象间的多重引用导致性能降低,并发操作引起死锁等。

为此,我们有必要了解一些其它的编程思想,并合理运用于实际工程中。

函数式编程

函数式编程是一种编程范式,也就是讨论如何编写程序的方法论。
函数式编程遵循一个准则:不依赖于外部的数据,而且也不改变外部数据的值,而是返回一个新的值给你。举个简单例子感受下:

//非函数式方式
int a;
-(void)increase{
    a+=1;
}
//函数式方式
-(int)increase:(int) a{
    return a+=1;
}

函数式编程可以总结出以下几个特点:

  1. 函数是一等公民
    函数可以同其他数据类型一样,赋值给其它变量,也可以作为参数,传入另一个函数,或者作为函数的返回值。
    在Objective-C中,通过block的引入,将函数当做变量,可以更好地运用函数式编程思想。
    int (^addBlock)(int,int) = ^(int first, int second){
        return first+second;
    };
    int (^subBlock)(int,int) = ^(int first, int second){
        return first-second;
    };
    int result = addBlock(subBlock(2,1),3);
  1. 只用表达式,不用语句
    表达式(expression)是一个单纯的运算过程,总有返回值;语句(statement)是执行某种操作,没有返回值。函数式编程要求,只使用表达式,不使用语句。也就是说,每一步都是单纯的运算,而且都有返回值。
  2. 不修改状态
    函数式编程只返回新的值,并不修改系统变量,所以没有副作用。
  3. 引用透明
    函数的运行不依赖于外部变量或"状态",只依赖于输入的参数,任何时候只要参数相同,引用函数所得到的返回值总是相同的。

函数式编程具有代码简洁,接近自然语言,易于理解,易于并发编程等优点,Objective-C项目中可用于View动画展示,通知事件处理,事件完成处理,错误处理,网络回调等场景(block方式)。

PS:函数式编程可以和链式编程相结合,通过声明一个block属性,而这个block的返回值是一个对象,然后链式地调用下一个block。这种方式可以参考开源项目Masonry

UIEdgeInsets padding = UIEdgeInsetsMake(10, 10, 10, 10);

[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
    make.top.equalTo(superview.mas_top).with.offset(padding.top); //with is an optional semantic filler
    make.left.equalTo(superview.mas_left).with.offset(padding.left);
    make.bottom.equalTo(superview.mas_bottom).with.offset(-padding.bottom);
    make.right.equalTo(superview.mas_right).with.offset(-padding.right);
}];

面向协议/接口编程

接口/协议是一系列没有主体代码的方法定义组成的集合体,它可以被具体的类所实现。面向接口编程要求高层类不依赖底层模块,高层模块只依赖于抽象,即接口/协议。这样做的好处是高层模块具有很高的复用性,多用于复杂业务的抽象提取,低层模块只要实现了接口/协议,就可以在不修改高层模块的情况下,使用高层模块提供的服务。在Objective-C中,一般应用场景为代理模式,一方(高层模块)使用protocol(协议)划定一个或一组规则,成为其代理的角色(低层模块)必须遵守这一系列规则,最后根据规则去办事。
当然实现面向接口编程,不只使用代理一种方式,还可以通过观察者模式、依赖注入等方式实现。

//协议
@protocol FontDownloaderProtocal <NSObject>
@optional
- (void)willStartDownload:(Font *)fInfo;//队列中处于等待状态
- (void)startDownload:(Font *)fInfo;//开始下载
- (void)downloadProgress:(Font *)fInfo progress:(float)progress;//下载过程中
- (void)didFinishDownload:(Font *)fInfo;//完成下载
- (void)downloadFailed:(Font *)fInfo;//下载失败
- (void)downloadExceed:(Font *)fInfo;//下载数超过队列限制
@end

//Presenter
@interface FontPresenter : NSObject
- (void)getFontList:(void (^)())onSuccess onFail:(void (^)())onFail;
- (void)downLoadFont:(Font *)font handler:(id<FontDownloaderProtocal>)handler;//依赖注入

@end

总结:本文尝试通过架构模式和编程思想等方面的梳理,力图总结出iOS开发(基于Objective-C)的项目优化方向。对理论知识的全面掌握不是因为都要运用于项目之中,而是在掌握各种方法之后,在不同场景选择最合适的去使用。

参考资料:

  1. http://coolshell.cn/articles/4535.html
  2. http://www.jianshu.com/p/b849c6972216
  3. http://kaedea.com/2015/10/11/android-mvp-pattern/
  4. http://toughcoder.net/blog/2015/11/29/understanding-android-mvp-pattern/
  5. http://www.ruanyifeng.com/blog/2012/04/functional_programming.html
  6. https://onevcat.com/2016/11/pop-cocoa-1/
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容