面向对象永远是个可以吐槽的话题,从开始提出到推崇备至,到充满质疑,一路走来让人唏嘘不已。面向对象的思想可谓历史悠久,20 世纪 70 年代的 Smalltalk 可以说是面向对象语言的经典,直到今天我们依然将这门语言视为面向对象语言的基础。
面向对象是大部分编程语言的基本特性,像 C++、Java、Objective-C 这样的静态语言,Ruby、Python 这样的动态语言都是面向对象的语言。但是如何编写面向对象的程序却一直是困扰人们的话题,即使是 Smalltalk,也有人认为这是一个有缺陷的面向对象的语言实现。
我在 2010 年翻译过的一篇 InfoQ 的文章,《面向对象编程──走错了路》中提到,面向对象编程的三个原则是:基于消息传递机制,对象分离和多态。文章中还举了Erlang 例子,认为Erlang 具备了这些原则,“所以可能是唯一的面向对象语言”。除了之前提到的三个特征,单继承和动态类型也被引用为面向对象语言的“绝对需求”。基于这些考虑,文章指出,Smalltalk 在实现对象思想时的“错误”──例如,只关注状态和行为,在类和基于映像的语言里缺乏良好的并发模型和消息机制。
这篇文章中的核心就是,面向对象思想中除了对象的状态、行为,还应该关注其并发机制、消息机制,后者更为重要。这一点事实上是我在接触了Objective-C 之后才有了更深入的体会。
Ojbective-C 的语法设计主要基于 Smalltalk,除了提供传统的面向对象编程特性之外,还增加了很多类似动态语言 Ruby、Python 才具有的特性,例如动态类型、动态加载、动态绑定等等,同时强化了消息传递机制和表意(Intention Revealing Interface)接口的概念。
消息
消息传递模型(Message Passing)是 Objective-C 语言的核心机制。在 Objective-C 中,没有方法调用这种说法,只有消息传递。在 C++ 或 Java 中调用某个类的方法,在 Objective-C 中是给该类发送一个消息。在 C++ 或 Java 里,类与类的行为方法之间的关系非常紧密,一个方法必定属于一个类,且于编译时就已经绑定在一起,所以你不可能调用一个类里没有的方法。而在 Objective-C 中就比较简单了,类和消息之间是松耦合的,方法调用只是向某个类发送一个消息,该类可以在运行时再确定怎么处理接受到的消息。也就是说,一个类不保证一定会响应接收到的消息,如果收到了一个无法处理的消息,那么程序就是简单报一个错。甚至你可以向一个值为 nil 的空对象发送消息,系统都不会出错或宕掉。这种设计本身也比较符合软件的隐喻。
在表意接口(Intention Revealing Interface)方面,Objective-C 也是设计的比较出色的语言。面向对象语言的特性之一就是通过 API 把实现封装起来,为上层建筑提供服务。但是需要注意的一点就是,你封装的 API 最好能够让调用者看到接口描述就知道怎么使用。如果为了使用一个 API 必须要去研究它的实现,那么就失去了封装的意义。Objective-C 通过显式的 API 描述,让开发者不自觉的写出满足表意接口的 API,比如下图中的 API 描述。
图 1:传统实例方法
上图中描述了一个传统意义的实例方法,但和 Java 或 C++ 不同的是,其方法关键字由多个字符串组成,在这个例子是 insertObject 和 atIndex,(id)anObject 和 (NSUInterger)index 分别表示参数类型和参数名称。整个方法看上去就像一个英语句子,我们可以很容易的知道,这个方法就是在索引为 index 处插入一个对象。如果你是从其他语言转到 Objective-C,那么开始的时候会感觉这种写法有些繁复,但是一旦理解并习惯了你会感受到其巨大的好处,这种写法会强制你写出优美易读的代码和 API,而且有了 XCode 强大的提示功能,再长的方法也是一蹴而就。
下面我们来说说多态和继承。 与 Java 一样,Objective-C 一样不支持多重继承,但是通过类别(Category)和协议(Protocol)可以很好的实现代码复用和扩展。
Category
首先我们来谈谈 Category。
Objective-C 提供了一种与众不同的方式——Catagory,可以动态的为已经存在的类添加新的行为。这样可以保证类的原始设计规模较小,功能增加时再逐步扩展。使用 Category 对类进行扩展时,不需要访问其源代码,也不需要创建子类。Category 使用简单的方式,实现了类的相关方法的模块化,把不同的类方法分配到不同的分类文件中。
实现起来很简单,我们举例说明。
SomeClass.h @interface SomeClass : NSObject{ } -(void) print; @end
这是类 SomeClass 的声明文件,其中包含一个实例方法 print。如果我们想在不修改原始类、不增加子类的情况下,为该类增加一个 hello 的方法,只需要简单的定义两个文件 SomeClass+Hello.h 和 SomeClass+Hello.m,在声明文件和实现文件中用“()”把 Category 的名称括起来即可。声明文件代码如下:
#import "SomeClass.h" @interface SomeClass (Hello) -(void)hello; @end
实现文件代码如下
#import "SomeClass+Hello.h" @implementationSomeClass (Hello) -(void)hello{ NSLog (@"name:%@ ", @"Jacky"); } @end
其中 Hello 是 Category 的名称,如果你用 XCode 创建 Category,那么需要填写的内容包括名称和要扩展的类的名称。这里还有一个约定成俗的习惯,将声明文件和实现文件名称统一采用“原类名 +Category”的方式命名。
调用也非常简单,毫无压力,如下: 首先引入 Category 的声明文件,然后正常调用即可。
#import "SomeClass+Hello.h" SomeClass * sc =[[SomeClass alloc] init]; [sc hello]
执行结果是:
name:Jacky
Category 的使用场景:
- 当你在定义类的时候,在某些情况下(例如需求变更),你可能想要为其中的某个或几个类中添加方法。
- 一个类中包含了许多不同的方法需要实现,而这些方法需要不同团队的成员实现
- 当你在使用基础类库中的类时,你可能希望这些类实现一些你需要的方法。
遇到以上这些需求,Category 可以帮助你解决问题。当然,使用 Category 也有些问题需要注意,
- Category 可以访问原始类的实例变量,但不能添加变量,如果想添加变量,可以考虑通过继承创建子类。
- Category 可以重载原始类的方法,但不推荐这么做,这么做的后果是你再也不能访问原来的方法。如果确实要重载,正确的选择是创建子类。
- 和普通接口有所区别的是,在分类的实现文件中可以不必实现所有声明的方法,只要你不去调用它。
用好 Category 可以充分利用 Objective-C 的动态特性,编写出灵活简洁的代码。
Protocol
下面我们再来看 Protocol。
Protocol,简单来说就是一系列不属于任何类的方法列表,其中声明的方法可以被任何类实现。这种模式一般称为代理(delegation)模式。你通过 Protocol 定义各种行为,在不同的场景采用不同的实现方式。在 iOS 和 OS X 开发中,Apple 采用了大量的代理模式来实现 MVC 中 View 和 Controller 的解耦。
定义 Protocol 很简单,在声明文件(h 文件)中通过关键字 @protocol 定义,然后给出 Protocol 的名称,方法列表,然后用 @end 表示 Protocol 结束。在 @end 指令结束之前定义的方法,都属于这个 Protocol。例如:
@protocol ProcessDataDelegate <NSObject> @required - (void) processSuccessful: (BOOL)success; @optional - (id) submitOrder: (NSNumber *) orderid; @end
以上代码可以单独放在一个 h 文件中,也可以写在相关类的 h 文件中,可以视具体情况而定。该 Protocol 包含两个方法,processSuccessful 和 submitOrder。这里还有两个关键字,@required 和 @optional,表示如果要实现这个协议,那么 processSuccessful 方法是必须要实现的,submitOrder 则是可选的,这两个注解关键字是在 Objective-C 2.0 之后加入的语法特性。如果不注明,那么方法默认是 @required 的,必须实现。
那么如何实现这个 Protocol 呢,很简单,创建一个普通的 Objective-C 类,取名为 TestAppDelegate,这时会生成一个 h 文件和 m 文件。在 h 文件中引入包含 Protocol 的 h 文件,之后声明采用这个 Protocol 即可,如下:
@interface TestAppDelegate : NSObject<ProcessDataDelegate>; @end
用尖括号(<…>)括起来的 ProcessDataDelegate 就是我们创建的 Protocol。如果要采用多个 Protocol,可以在尖括号内引入多个 Protocol 名称,并用逗号隔开即可。例如 <ProcessDataDelegate,xxxDelegate>
m 文件如下:
@implementation TestAppDelegate - (void) processSuccessful: (BOOL)success{ if (success) { NSLog(@" 成功 "); }else { NSLog(@" 失败 "); } } @end
由于 submitOrder 方法是可选的,所以我们可以只实现 processSuccessful。
Protocol 一般使用在哪些场景呢?Objective-C 里的 Protocol 和 Java 语言中的接口很类似,如果一些类之间没有继承关系,但是又具备某些相同的行为,则可以使用 Protocol 来描述它们的关系。不同的类,可以遵守同一个 Protocol,在不同的场景下注入不同的实例,实现不同的功能。其中最常用的就是委托代理模式,Cocoa 框架中大量采用了这种模式实现数据和 UI 的分离。例如 UIView 产生的所有事件,都是通过委托的方式交给 Controller 完成。根据约定,框架中后缀为 Delegate 的都是 Protocol,例如 UIApplicationDelegate,UIWebViewDelegate 等,使用时大家可以留意一下,体会其用法。
使用 Protocol 时还需要注意的是:
1、Protocol 本身是可以继承的,比如:
@protocol A -(void)methodA; @end @protocol B <A> -(void)methodB; @end
如果你要实现 B,那么 methodA 和 methodB 都需要实现。
2、Protocol 是类无关的,任何类都可以实现定义好的 Protocol。如果我们想知道某个类是否实现了某个 Protocol,还可以使用 conformsToProtocol 进行判断,如下:
[obj conformsToProtocol:@protocol(ProcessDataDelegate)]
好吧,具体的语言特性这次就介绍这么多。从某种意义上来说,Objective-C 是一门古老的语言,发明于 1980 年。1988 年,乔布斯的 Next 公司获得了 Objective-C 语言的授权,并开发出了 Objective-C 的语言库和 NEXTSTEP 的开发环境。NextStep 是以 Mach 和 BSD 为基础,Objective-C 是其语言和运行库,后来的事大家都清楚,苹果买了 Next,乔布斯回归苹果,开始神奇的苹果振兴之路,NextStep 成了 Max OS X 的基础。以后发展越来越好,Objctive-C 成了 Apple 的当家语言,现在基本上是 Apple 在维护 Objctive-C 的发展。
在苹果的 AppStore 推出之前,Objective-C 一直相对小众,但是其优秀的语言特性似乎一直在为后面的爆发积蓄力量,当苹果平台级的应用出现之后,Objective-C 开始大放异彩,静态语言的效率和动态语言的特性得到众多程序员的喜爱,目前它已经以火箭般的速度蹿升 TIOBE 语言排行版第四位。
对于喜爱苹果技术的技术人员来说,Objective-C 是你必须深入了解和值得学习的一门语言,希望以后有机会多写一些相关的文章。
原文链接: Objective-C——消息、Category 和 Protocol
感谢崔康对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。
评论