- 第5章 复合
使用复合可以组合多个对象,让它们分工合作。在实际的程序中,你会同时用到继承和复合来创建自己的类,所以掌握这两个概念非常重要。
- 什么是复合
复合就是一个类持有另一个类的引用(默然说话:大多数面向对象理论书籍里的术语叫关联)。在Objective-C中,复合是通过包含作为实例变量的对象指针实现的。例如我们可以用一个Pedal(脚踏板)对象和一个Tire(轮胎)对象组合出虚拟的Unicycle(独轮车)。
@interface Unicycle:NSObject
{
Pedal *pedal;
Tire *tire;
}
@end //Unicycle
Car程序
现在准备搭建一个汽车模型。好吧,我们不会去费力地研究真正的汽车的物理模型,我们象这样考虑:1辆汽车只包含1台发动机和4个轮胎。轮胎和发动机都是仅包含一个方法的类,这个方法仅输出它们各自所代表的含义:轮胎对象会说它们是轮胎,发动机对象会说它是一台发动机。
CarParts程序的所有代码也是包含在main.m中。完整代码如下:
//
// main.m
// CarParts
// 类与类的关系:复合
// Created by mouyong on 13-6-23.
// Copyright (c) 2013年 mouyong. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface Tire : NSObject
@end //Tire
@implementation Tire
- (NSString *)description{
return (@"我是一个轮胎");
}//description
@end//Tire
@interface Engine : NSObject
@end//Engine
@implementation Engine
- (NSString *)description{
return @"我是一个发动机";
}//description
@end//Engine
@interface Car : NSObject
{
Engine *engine;
Tire *tires[4];
}
-(void)print;
@end//Car
@implementation Car
- (id) init{
if (self=[super init]) {
engine=[Engine new];
tires[0]=[Tire new];
tires[1]=[Tire new];
tires[2]=[Tire new];
tires[3]=[Tire new];
}
return self;
}//init
- (void) print{
NSLog(@"%@",engine);
NSLog(@"%@",tires[0]);
NSLog(@"%@",tires[1]);
NSLog(@"%@",tires[2]);
NSLog(@"%@",tires[3]);
@end//Car
int main(int argc, const char * argv[])
{
Car *car;
car=[Car new];
[car print];
return 0;
}//main
在Tire类中只有一个description方法,比较奇怪的是,它并没有在接口中声明。它是从哪儿来的?如果接口中并没有包含它,计算机又怎么能知道可以在Tire类里调用description方法呢?(默然说话:感觉description就象Java中的toString())
- 为SNLog定义输出字符串
NSLog()可以使用%@格式说明符来输出对象。NSLog()处理%@说明符时,会调用对象的description方法,然后对象的description方法生成一个NSString并将其返回。NSLog()就会在输出结果中包含这个字符串。在类中提供description方法就可以自定义NSLog()会如何输出对象。
在自定义的description方法中,既可以写一个很简单的字符串,也可以使用各种形式连接一个复杂的字符串。
Engine类也只有一个description方法。
最后一部分是Car本身,它拥有一个engine对象和一个由4个tire对象组成的数组。它通过复合的方式来组装自己。Car同时还有一个print()方法,该方法输出轮胎和发动机的描述。
因为engine和tires是Car类的实例变量,所以它们是复合的。你可以说汽车有4个轮胎和1个发动机。
@interface Car : NSObject
{
Engine *engine;
Tire *tires[4];
}
-(void)print;
@end//Car
每一个Car对象都会为指向engine和tires的指针分配内存,但是真正包含在Car中的并不是engine和tires变量,只是内存中存在的其他对象的引用指针。为新建的Car对象分配内存时,这些指针将被初始化为nil(默然说话:相当于Java中的null值。),也就是说这辆汽车现在既没有发动机也没有轮胎,但预留的安装发动机和轮胎的位置,只是还没装上去。
下面让我们在看看Car类的实现。首先是一个初始化实例变量的init方法。(默然说话:注意到声明中并没有声明init吧?直觉告诉我,这个方法应该是Java中的构造方法吧)该方法为我们的汽车创建了用来装配的1个engine和4个tire变量。使用new创建新对象时,系统其实在后台执行了两个步骤:第一步,为对象分配内存,即对象获得一个用来存放实例变量的内存块;第二步,自动调用init方法,使该对象进入可用状态。
Car类的init方法创建了4个新轮胎并赋值给tires数组,还创建了一台发动机并赋值给engine实例变量。
在init方法中比较奇怪的是那个if语句,我们解释一下这行代码的意思。为了让父类将所有需要的初始化工作一次性完成,你需要调用[super init]。意思是让父类初始化过程中返回的对象与一开始创建的保持一致。而将初始化返回的结果赋给self是Objective-C的惯例。
@implementation Car
- (id) init{
if (self=[super init]) {
engine=[Engine new];
tires[0]=[Tire new];
tires[1]=[Tire new];
tires[2]=[Tire new];
tires[3]=[Tire new];
}
return self;
}//init
- (void) print{
NSLog(@"%@",engine);
NSLog(@"%@",tires[0]);
NSLog(@"%@",tires[1]);
NSLog(@"%@",tires[2]);
NSLog(@"%@",tires[3]);
@end//Car
最后一部分是main()函数,也是程序的驱动力。main()函数创建了一辆新车,并告诉它输出自身的信息,然后退出程序。
生成并运行CarParts程序,你应该会看到与下面内容类似的输出:
-------------------------------------------------------------------------------------------------------------------
2013-06-27 10:01:18.312 CarParts[16183:303] 我是一个发动机
2013-06-27 10:01:18.314 CarParts[16183:303] 我是一个轮胎
2013-06-27 10:01:18.315 CarParts[16183:303] 我是一个轮胎
2013-06-27 10:01:18.315 CarParts[16183:303] 我是一个轮胎
2013-06-27 10:01:18.316 CarParts[16183:303] 我是一个轮胎
-------------------------------------------------------------------------------------------------------------------
- 存取方法
我们可以使用存取方法来改进CarParts。
经验丰富的编程人员看到Car类的init方法可能会问:“为什么汽车要自己创建轮胎和发动机呢?”如果用户能为汽车定做不同类型的轮胎和发动机,那么这个程序就会更完善了。
我们可以添加存取方法来实现上述想法。存取(accessor)方法是用来读取或改变某个对象属性的方法。例如前面Shapes-Object中的setFillColor:就是一个存取方法。如果添加一个新方法去改变Car对象中的engine对象变量,那它就是一个存取方法。因为它为对象中的变量赋值,所以这类存取方法被称为setter方法。你也许听说过mutator方法,它是用来更改对象状态的。
另一种存取方法当然是getter方法。getter方法为代码提供了通过对象自身访问对象属性的方式。在赛车游戏中,物理逻辑引擎可能会想要读取汽车轮胎的属性,以此来判断赛车以当前的速度行驶是否会在湿滑的道路上打滑。
说明:如果要对其他对象中的属性进行操作,应该尽量使用对象提供的存取方法,绝对不能直接改变对象里面的值。例如,main()函数不应直接访问Car类的engine实例变量(通过car->engine的方法)来改变engine的属性,而应使用setter方法进行更改。
存储方法是程序间接工作的另一个例子。使用存取方法间接地访问car对象中engine,可以让car的实现更为灵活。
下面为Car添加一些setter和getter方法,这样它就有选用轮胎和发动机的自主权了。下面是Car类的新接口。
@interface Car : NSObject
{
Engine *engine;
Tire *tires[4];
}
-(Engine *)engine;
-(void)setEngine:(Engine *)newEngine;
-(Tire *)tireAtIndex:(int) index;
-(void)setTire:(Tire *) tire atIndex:(int)index;
-(void)print;
@end//Car
代码中的实例变量并没有变化,但是新增了两对方法:engine和setEngine:用来处理发动机的属性,tireAtIndex和setTire:atIndex:用来处理轮胎的属性。存取方法总是成对出现的,一个用来设置属性的值,另现代战争用来读取属性的值。有时只有一个getter方法(用于只读属性)或者只有一个setter方法(只写属性)也是合理的。但通常情况下,我们都会同时编写setter和getter方法。
对于存取方法的全名,Cocoa有自己的惯例。
setter方法根据它所更改的属性的名称来命名,并加上前缀set。
getter方法则是以其返回的属性名称命名,不要将get用作getter方法的前缀。
说明 get这个词在Cocoa中有着特殊的含义。如果get出现在Cocoa的方法名称中,就意味着这个方法会将你传递的参数作为指针来返回数值。
如果你在存取方法的名称中使用了get,那么有经验的Cocoa编程人员就会习惯性地将指针当做参数传入这个方法,当他们发现这不过是一个简单的存取方法时就会感到困惑。最好不要让其他编程人员被你的代码搞得一头雾水。(默然说话:作为一个有经验的Java编程人员,在看到getter方法不写get前缀时我感到非常困惑,我现在已经一头雾水啦~~)
- 设置engine属性的存取方法
第一对存取方法用来访问发动机的属性:
-(Engine *)engine;
-(void)setEngine:(Engine *)newEngine;
在代码中调用Car对象的engine方法可以访问engine变量,调用setEngine:方法可以更改发动机的属性。下面是实现代码:
-(Engine *)engine{
return engine;
}//engine
-(void) setEngine:(Engine *)newEngine{
engine=newEngine;
}//setEngine
getter方法engine返回实例变量engine的当前值。记住,在Objective-C中所有对象间的交互都是通过指针实现的,所以方法engine返回的是一个指针,指向Car中的发动机对象。
同样,setter方法setEngine:将实例变量engine的值赋为参数所指向的值。实际上被复制的并不是engine变量,而是指向engine的指针值。换一种方式说,就是在调用了对象Car中的setEngine:方法后,依然只存在一个发动机,而不是两个。
说明 为了信息的完整性,我们需要说明,在内存管理和对象所有权方面,Engine的getter方法和setter方法还存在着问题。但是现在就把内存管理和对象生命周期管理的问题摆出来,肯定会让你困惑和沮丧,所以我们把如何准确无误的编写存取方法放到第8章再讲。
- 设置tires属性的存取方法
tires的存取方法稍微复杂一点:
-(Tire *)tireAtIndex:(int) index;
-(void)setTire:(Tire *) tire atIndex:(int)index;
由于汽车的4个轮胎都有自己不同的位置(汽车车体的4个角落各有一个轮胎),所以Car对象中包含一个轮胎的数组。在这里我们需要用索引存取器而不能直接访问tires数组。所以在为汽车配置轮胎时,不仅需要知道是哪个轮胎,还要清楚每个轮胎在汽车上的位置。同样,当访问汽车上的某个轮胎时,访问的也是这个轮胎的具体位置。
下面是相关存取方法的实现代码:
-(Tire *)tireAtIndex:(int)index{
if (index <0 || index>3) {
NSLog(@"错啦 索引值%d不对!",index);
exit(1);
}
return (tires[index]);
}//tireAtIndex
-(void)setTire:(Tire *)tire atIndex:(int)index{
if (index <0 || index>3) {
NSLog(@"错啦 索引值%d不对!",index);
exit(1);
}
tires[index]=tire;
}//setTire:atIndex
tire存取方法检查了tires实例变量的数组索引,以保证它是有效的。如果传入的索引值不在0到3的范围,程序将会输出错误信息并退出。这就是所谓防御式编程(defensive programming),这是种很好的编程思想。防御式编程能够在开发早期发现错误,比如tires数组的索引错误。
- Car类代码的其他变化
首先Car类的init方法。由于Car现在已经有访问engine和tires变量的方法,所以init方法就不需要再创建这两个变量了,这样一来,init方法都可以去除(默然说话:当然,你要保留它也行,这样程序就多一种选择,既可以弄到一辆已经装配好的车,又可以随时更换轮胎和发动机),因为已经不需要在Car中做这些工作了。新车的车主会得到一辆没有轮胎和发动机的汽车,可以自己装配它。
接着我们可以更新一下main()函数,如下:
int main(int argc, const char * argv[])
{
Car *car;
car=[Car new];
Engine *engine=[Engine new];
[car setEngine:engine];
for (int i=0; i<4; i++) {
Tire *tire=[Tire new];
[car setTire:tire atIndex:i];
}
[car print];
return 0;
}//main
从用户的角度来看,程序的运行结果并没有任何改变。
- 扩展CarParts程序
既然Car类已经有了存取方法,就应该充分利用它。我们不会照搬当前的发动机和轮胎,而是对这些部件做一些改变。用集成方式来创建新的发动机和轮胎,然后使用Car类的存取方法(复合方式)给汽车配置新的部件。
首先创建一个新型的发动机Slant6。
@interface Slant6 : Engine
@end//Slant6
@implementation Slant6
-(NSString *)description{
return (@"我是一个Slant6发动机!");
}//description
@end//Slant6
Slant6是发动机,所以它是Engine的子类。要记住,继承可以让我们在需要父类的的地方使用子类。在Car类中,setEngine:方法需要的是Engine型的参数,所以我们可以放心的传递Slant6型的参数。
在Slant6类中,description方法被重写,用来输出新信息。由于Slant6并没有调用父类中的description方法,所以新信息完全替代了继承自Engine的描述信息。
轮胎的新类AllWeatherRadial的实现步骤与创建Slant6的步骤非常相似。将其定义为现有类Tire的子类,然后重写description方法。
@interface AllWeatherRadial : Tire
@end//AllWeatherRadial
@implementation AllWeatherRadial
-(NSString *)description{
return @"我是一个全天候轮胎";
}//description
最后调整main()函数,使用痛苦的型的发动机和轮胎
int main(int argc, const char * argv[])
{
Car *car;
car=[Car new];
Engine *engine=[Slant6 new];
[car setEngine:engine];
for (int i=0; i<4; i++) {
Tire *tire=[AllWeatherRadial new];
[car setTire:tire atIndex:i];
}
[car print];
return 0;
}//main
main()仅仅是重新new了两个新类,其他部分并无改动,然后我们就发现输出已经完全变了。
----------------------------------------------------------------------------------------------------------------------
2013-07-06 10:16:38.476 CarParts[576:303] 我是一个Slant6发动机!
2013-07-06 10:16:38.478 CarParts[576:303] 我是一个全天候轮胎
2013-07-06 10:16:38.478 CarParts[576:303] 我是一个全天候轮胎
2013-07-06 10:16:38.479 CarParts[576:303] 我是一个全天候轮胎
2013-07-06 10:16:38.479 CarParts[576:303] 我是一个全天候轮胎
----------------------------------------------------------------------------------------------------------------------
- 复合还是继承
CarParts同时用到了继承和复合,也就是我们在前一章和本章中所介绍的两个“万能”工具。那么,什么时候用继承,什么时候用复合?
继承的类之间建立的关系为“是”的关系,三角形是形状,Slant6是发动机,AllWeatherRadial是轮胎。所以,如果可以说“X是Y”,那么就可以使用继承。
复合的类之间建立的关系为“有”。形状有颜色,汽车有发动机和轮胎。与继承的区别:汽车不是发动机,也不是轮胎。所以,如果可以说“X有Y”,那就可以使用复合。
新手在进行面向对象编程时通常会犯这样的错误:对任何东西都想使用继承,例如让Car类继承Engine类。这样的确可以让代码正常运行,但是其他人阅读这段代码时会不理解:汽车是发动机?!当然不是。所以只应在适当的时机使用继承。
- 小结
复合是OOP的基础概念,我们通过这种技巧来创建引用其他对象的对象。例如,汽车对象引用了1个发动机和4个轮胎对象。在本章关于复合的讨论中,我们介绍了存取方法,它既为外部对象提供了改变其属性的途径,同时又能保护实例变量本身。
存取方法和复合是密不可分的,因为我们通常都会为复合的对象编写存取方法。我们还学习了两种类型的存取方法:setter方法和getter方法,前者告诉对象将属性改为什么,后者要求对象提供属性的值。
本章还介绍了Cocoa存取方法的命名规则。需要特别指出的是,对于返回属性值的存取方法,名称中不能使用get这个词。
下一章我们不会谈论任何OOP理论,而是介绍如何分割不同的类,并将它们放入多个源文件中,而不是把所有代码都写到一个大文件里。
本人接受个人捐助,如果你觉得文章对你有用,并愿意对默然说话进行捐助,数额随意, 默然说话将不胜感激。