这次要讨论在 Objective-C 可以用哪些方式,来进行物件导向的程式设计。包涵的主题有:
- 类目(category)
- 协定(protocol)
- 继承(inheritance)
- 复合(composite)
何时会你会选择用 class category 而不用 inheritance?当你发现,你只需要新增或是修改某些操作(method),你觉得用继承是杀鸡用牛刀。
另外一个情况,是当有不只一个以上的人,来实做 class 的介面,用 class category 可以用来划分分工的内容。
1: //
2: // Fraction.h
3: // OOP1
4: //
5: // Created by Chou Shunyuan on 5/15/10.
6: // Copyright 2010 __MyCompanyName__. All rights reserved.
7: //
8:9: #import10:11:12: @interface Fraction : NSObject13: {14: int numerator;
15: int denominator;
16: }17:18: @property int numerator;
19: @property int denominator;
20:21: -(void) setTo: (int)n over: (int) d;22: -(Fraction*) add: (Fraction*) f;23: -(void) reduce;
24: -(double) convertToNum;
25: -(void) print;
26:27: @end28:
1: //
2: // Fraction.m
3: // OOP1
4: //
5: // Created by Chou Shunyuan on 5/15/10.
6: // Copyright 2010 __MyCompanyName__. All rights reserved.
7: //
8:9: #import "Fraction.h"
10:11:12: @implementation Fraction13:14: @synthesize numerator, denominator;15:16: -(void) setTo: (int) n over: (int) d17: {18: numerator = n;19: denominator = d;20: }21:22: -(Fraction*) add: (Fraction*) f23: {24: //
25: // (a/b) + (c/d) = ((a*d) + (b*c)) / (b*d)
26: //
27:28: Fraction *result = [[Fraction alloc] init];29:30: result.numerator = (self.numerator * f.denominator) + (self.denominator * f.numerator);31: result.denominator = (self.denominator * f.denominator);32:33: [result reduce];34:35: return result;
36: }37:38: -(void) reduce
39: {40: int u = numerator;
41: int v = denominator;
42: int temp;
43:44: while (v != 0)
45: {46: temp = u % v;47: u = v;48: v = temp;49: }50:51: numerator /= u;52: denominator /= u;53: }54:55: -(double) convertToNum
56: {57: double result = (double) numerator / denominator;58:59: return result;
60: }61:62: -(void) print
63: {64: NSLog(@" %i/%i", numerator, denominator);
65: }66:67: @end68:
(4) 接着我们透过 class category 来扩充 Fraction,我们要为 Fraction class 新增 add,sub,mul,div,加减乘除四个操作。我们把这个 category 取名为 MathOps。跟新增 Fraction 类别的步骤相同,不过这次我们给的档名为 Fraction+MathOps ,这样的命名是惯例。
(5) 接着就定义及实做 Fraction (MathOps) 这个类目,请参阅下面的程式列表。这个作法,即使你没有 Fraction 类别的原始码的情况下,也适用。这个就是 Objective-C 所提供的弹性。
1: //
2: // Fraction+MathOps.h
3: // OOP1
4: //
5: // Created by Chou Shunyuan on 5/15/10.
6: // Copyright 2010 __MyCompanyName__. All rights reserved.
7: //
8:9: #import "Fraction.h"
10:11:12: @interface Fraction (MathOps)13:14: -(Fraction*) add: (Fraction*) f;15: -(Fraction*) sub: (Fraction*) f;16: -(Fraction*) mul: (Fraction*) f;17: -(Fraction*) div: (Fraction*) f;18:19: @end20:
1: //
2: // Fraction+MathOps.m
3: // OOP1
4: //
5: // Created by Chou Shunyuan on 5/15/10.
6: // Copyright 2010 __MyCompanyName__. All rights reserved.
7: //
8:9: #import "Fraction+MathOps.h"
10:11:12: @implementation Fraction (MathOps)13:14: -(Fraction*) add: (Fraction*) f15: {16: // (a/b) + (c/d) = ((a*d) + (b*c)) / (b*d)
17:18: Fraction *result = [[Fraction alloc] init];19:20: result.numerator = (self.numerator * f.denominator) + (self.denominator * f.numerator);21: result.denominator = self.denominator * f.denominator;22:23: [result reduce];24:25: return result;
26: }27:28: -(Fraction*) sub: (Fraction*) f29: {30: // (a/b) - (c/d) = ((a*d) - (b*c)) / (b*d)
31:32: Fraction *result = [[Fraction alloc] init];33:34: result.numerator = (self.numerator * f.denominator) - (self.denominator * f.numerator);35: result.denominator = self.denominator * f.denominator;36:37: [result reduce];38:39: return result;
40: }41:42: -(Fraction*) mul: (Fraction*) f43: {44: // (a/b) * (c/d) = (a*c) / (b*d)
45:46: Fraction *result = [[Fraction alloc] init];47:48: result.numerator = (self.numerator * f.numerator);49: result.denominator = self.denominator * f.denominator;50:51: [result reduce];52:53: return result;
54: }55:56: -(Fraction*) div: (Fraction*) f57: {58: // (a/b) / (c/d) = (a*d) / (b*c)
59:60: Fraction *result = [[Fraction alloc] init];61:62: result.numerator = (self.numerator * f.denominator);63: result.denominator = self.denominator * f.numerator;64:65: [result reduce];66:67: return result;
68: }69:70: @end71:
(6) 接着修改 OOP1 主程式,来测试我们的 Fraction (MathOps) 类目,是否按照我们的设计,进行运算。首先初始化两个 Fraction 变数,fraction1 跟 fraction2,然后一个设为 1/2,另一个设为 1/4,接着进行两个分数的加、减、乘、除。最后释放掉 fraction1 跟 fraction 两个变数。
1: #import "Fraction.h"
2: #import "Fraction+MathOps.h"
3:4: int main (int argc, const char * argv[])5: {6: NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];7:8: Fraction *fraction1 = [[Fraction alloc] init];9: Fraction *fraction2 = [[Fraction alloc] init];10: Fraction *result;11:12: [fraction1 setTo: 1 over: 2];13: [fraction2 setTo: 1 over: 4];14:15: // compute (1/2) + (1/4)
16: result = [fraction1 add: fraction2];17: [result print];18: [result release];19:20: // compute (1/2) - (1/4)
21: result = [fraction1 sub: fraction2];22: [result print];23: [result release];24:25: // compute (1/2) * (1/4)
26: result = [fraction1 mul: fraction2];27: [result print];28: [result release];29:30: // compute (1/2) / (1/4)
31: result = [fraction1 div: fraction2];32: [result print];33: [result release];34:35: [fraction1 release];36: [fraction2 release];37:38: [pool drain];39: return 0;
40: }41:
执行结果如下图:
(二)、 使用协定(protocol),来定义物件支间要如何来互动(interact)。
(1) Objective-C 的 protocol 实质上的意义,就像是 C++ 的 pure virtual class,或是 Java 的 interface。但是取 protocol 这个名称,却更传神。在物件导向的程式设计,有时候你根本不关心跟你互动的是何种物件,你关心的是这个物件,到底听不听得懂你说的话。打个比方,“黑猫白猫,能抓得到老鼠的,就是好猫”,甚至“只要能抓得到老鼠,就是小狗,也不错啊”。
(2) 还是有点搞不清楚 protocol 到底是怎么一回事,对不对?让我们用一个简单的例子,来示范如何使用 protocl。先假设你是一个老板(一个已经存在的物件),你要找一个助手(一个未知的物件),对助手当然会有要求(protocol),只要满足这个要求,你就录用,不然他就不能成为你的助手。
(3) 首先用 Xcode 新增一个专案 OOP2(Object Oriented Program #2)
(2) 接着我们为这个专案新增一个 protocol,名字就叫 AssistantProtocol,名字你可以取你喜欢的,只要你自己明白,这是一个 protocol 档。
(3) AssistantProtocol.h 里定义了合格的 Assistant 应该要会的事情。在这里,我们定义了,一个合格的 Assistant 应该要会 makePlan,updateSchedule,statusReport,然后 bootlick (拍马屁)是选项,并非绝对必要,最后还要 tellTheTruth。
1: //
2: // AssistantProtocol.h
3: // OOP2
4: //
5: // Created by Chou Shunyuan on 5/15/10.
6: // Copyright 2010 __MyCompanyName__. All rights reserved.
7: //
8:9: #import10:11:12: @protocol AssistantProtocol13:14: -(void) makePlan;
15: -(void) updateSchedule;
16: -(void) statusReport;
17:18: @optional19: -(void) bootlick;
20:21: @required22: -(void) tellTheTruth;
23:24: @end25:
(4) 接着我们新增一个类别 GoodAssistant,这是一个合格的 Assistant,符合 AssistantProtocol 的要求。注意,我们在 GoodAssistant 的介面宣告,用 ,让这个 class 满足 AssistantProtocol 协定。
(5) 然后我们就在 GoodAssist 这个 class 里,实做 AssistantProtocol 所需要的操作。
1: //
2: // GoodAssistant.h
3: // OOP2
4: //
5: // Created by Chou Shunyuan on 5/15/10.
6: // Copyright 2010 __MyCompanyName__. All rights reserved.
7: //
8:9: #import10: #import "AssistantProtocol.h"
11:12: @interface GoodAssistant : NSObject13: {14: // nothing special here
15: }16:17: @end18:
1: //
2: // GoodAssistant.m
3: // OOP2
4: //
5: // Created by Chou Shunyuan on 5/15/10.
6: // Copyright 2010 __MyCompanyName__. All rights reserved.
7: //
8:9: #import "GoodAssistant.h"
10:11:12: @implementation GoodAssistant13:14: -(void) makePlan
15: {16: NSLog(@"make plan");
17: }18:19: -(void) updateSchedule
20: {21: NSLog(@"update schedule");
22: }23:24: -(void) statusReport
25: {26: NSLog(@"status report");
27: }28:29: -(void) bootlick
30: {31: NSLog(@"bootlick");
32: }33:34: -(void) tellTheTruth
35: {36: NSLog(@"tell the truth");
37: }38:39: @end40:
(6) 接着我们新增一个 BadAssistant 类别,这个类别不满足 AssistantProtocol。
1: //
2: // BadAssistant.h
3: // OOP2
4: //
5: // Created by Chou Shunyuan on 5/15/10.
6: // Copyright 2010 __MyCompanyName__. All rights reserved.
7: //
8:9: #import10:11:12: @interface BadAssistant : NSObject13: {14: // nothing special here
15: }16:17: @end18:
1: //
2: // BadAssistant.m
3: // OOP2
4: //
5: // Created by Chou Shunyuan on 5/15/10.
6: // Copyright 2010 __MyCompanyName__. All rights reserved.
7: //
8:9: #import "BadAssistant.h"
10:11: @implementation BadAssistant12:13: @end14:
(7) 接着我们新增一个 Boss 类别,这个 Boss 就是要按照 AssistantProtocol 的协定,来伺候的大爷。通常 protocol 协定,就是为这些 Boss 大爷而定的(不是为了 Assistant 助手这些小咖),这点抓到了,你大概就知道 protocol 是怎么一回事。Boss 他根据 AssistantProtocol 可以判断来服务的物件,是否够格,如果够格,就给他们点事情做。
1: //
2: // Boss.h
3: // OOP2
4: //
5: // Created by Chou Shunyuan on 5/15/10.
6: // Copyright 2010 __MyCompanyName__. All rights reserved.
7: //
8:9: #import10: #import "AssistantProtocol.h"
11:12:13: @interface Boss : NSObject14: {15: // nothing special here
16: }17:18: -(BOOL) IsAssistantQualified: (id) assistant;19: -(void) AskAssistantToWork: (id) asssitant;
20:21: @end22:
1: //
2: // Boss.m
3: // OOP2
4: //
5: // Created by Chou Shunyuan on 5/15/10.
6: // Copyright 2010 __MyCompanyName__. All rights reserved.
7: //
8:9: #import "Boss.h"
10:11:12: @implementation Boss13:14: -(BOOL) IsAssistantQualified: (id) assistant15: {16: return [assistant conformsToProtocol: @protocol(AssistantProtocol)];
17: }18:19: -(void) AskAssistantToWork: (id) asssitant
20: {21: [asssitant makePlan];22: [asssitant updateSchedule];23: [asssitant statusReport];24: [asssitant bootlick];25: [asssitant tellTheTruth];26: }27:28: @end29:
(8) 最后来看主程式 OOP2 是如何用这些物件的,首先初始化一个 Boss 物件,一个 GoodAssistant 物件,及一个 BadAssistant 物件。接着让 boss 物件检查两个 assistant 物件,如果满足 AssistantProtocol 协定,就让物件做点事,如果不合格,就印出错误讯息。
1: #import2: #import "AssistantProtocol.h"
3: #import "GoodAssistant.h"
4: #import "BadAssistant.h"
5: #import "Boss.h"
6:7: int main (int argc, const char * argv[])8: {9: NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];10:11: Boss *boss = [[Boss alloc] init];12: GoodAssistant *assistant1 = [[GoodAssistant alloc] init];13: BadAssistant *assistant2 = [[BadAssistant alloc] init];14:15: NSLog(@"-- if assistant1 is qualified then ask it to work --");
16:17: if ([boss IsAssistantQualified: assistant1] == YES)
18: [boss AskAssistantToWork: assistant1];19: else
20: NSLog(@"assistant1 is not qualified.");
21:22: NSLog(@"-- if assistant2 is qualified then ask it to work --");
23:24: if ([boss IsAssistantQualified: assistant2] == YES)
25: [boss AskAssistantToWork: assistant2];26: else
27: NSLog(@"assistant2 is not qualified.");
28:29: [boss release];30: [assistant1 release];31: [assistant2 release];32:33: [pool drain];34: return 0;
35: }36:
(9) 最后看看执行的结果
(三)、 使用继承(inheritance),站在巨人的肩膀上,写程式。
(1) 继承(inheritance)是物件导向程式设计,常用的一种实做的方法。你可以这样想,有许多事先已经写好的类别,来处理各式各样的事情,有时候直接拿来用,就对了。但是有时候,这些现有的类别,就是少那么一点什么,或是有些地方,跟你要的结果不一样。这时候你可以继承那些类别,新增或是修改你要的部份。这样对比你全部自己写还快。另外,有时候有两个或是两个以上的类别,他们有许多的程式码,是重复而可以互相共用的,这时候把共用的部份,抽出来,变成一个父类别,不但可以让程式码变小,还可减少错误发生的机会。
(2) 用 Xcode 新增一个专案 OOP3 (Object Oriented Program #3),然后新增 Rectangle 类别,有两个成员变数 width,跟 height。另外有三个操作 setWidth:andHeight: 用来设四角形的宽跟高,area 用来求四边形的面积,perimeter 用来求四边形的周长。
1: //
2: // Rectangle.h
3: // OOP3
4: //
5: // Created by Chou Shunyuan on 5/15/10.
6: // Copyright 2010 __MyCompanyName__. All rights reserved.
7: //
8:9: #import10:11:12: @interface Rectangle : NSObject13: {14: double width;
15: double height;
16: }17:18: @property double width;
19: @property double height;
20:21: -(void) setWidth: (double) w andHeight: (double) h;22: -(double) area;
23: -(double) perimeter;
24:25: @end26:
1: //
2: // Rectangle.m
3: // OOP3
4: //
5: // Created by Chou Shunyuan on 5/15/10.
6: // Copyright 2010 __MyCompanyName__. All rights reserved.
7: //
8:9: #import "Rectangle.h"
10:11:12: @implementation Rectangle13:14: @synthesize width, height;15:16: -(void) setWidth: (double) w andHeight: (double) h17: {18: width = w;19: height = h;20: }21:22: -(double) area
23: {24: return (width * height);
25: }26:27: -(double) perimeter
28: {29: return (2 * (width + height));
30: }31:32: @end33:
(3) 接着新增一个类别 Square 继承 Rectangle,但是多了 setSide 来设正方形的边长,及 side 来取得正方形的边长。
1: //
2: // Square.h
3: // OOP3
4: //
5: // Created by Chou Shunyuan on 5/15/10.
6: // Copyright 2010 __MyCompanyName__. All rights reserved.
7: //
8:9: #import "Rectangle.h"
10:11:12: @interface Square : Rectangle13: {14:15: }16:17: -(void) setSide: (double) side;18: -(double) side;
19:20: @end21:
1: //
2: // Square.m
3: // OOP3
4: //
5: // Created by Chou Shunyuan on 5/15/10.
6: // Copyright 2010 __MyCompanyName__. All rights reserved.
7: //
8:9: #import "Square.h"
10:11:12: @implementation Square13:14: -(void) setSide: (double) side15: {16: [self setWidth: side andHeight: side];17: }18:19: -(double) side
20: {21: return width;
22: }23:24: @end25:
(4) 在主程式 OOP3 中,我们初始化了一个四边形 5 x 10,及一个正方形 10 x 10,然后列印出场方形,及正方形的面积及周长。
1: #import "Rectangle.h"
2: #import "Square.h"
3:4: int main (int argc, const char * argv[])5: {6: NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];7:8: Rectangle *rectangle = [[Rectangle alloc] init];9: Square *square = [[Square alloc] init];10:11: [rectangle setWidth: 5.0 andHeight: 10.0];12: NSLog(@"the area of %g x %g rectangle = %g",
13: rectangle.width,14: rectangle.height,15: [rectangle area]);16:17: NSLog(@"the perimeter of %g x %g rectangle = %g",
18: rectangle.width,19: rectangle.height,20: [rectangle perimeter]);21:22: [square setSide: 10.0];23: NSLog(@"the area of %g x %g square = %g",
24: [square side],25: [square side],26: [square area]);27:28: NSLog(@"the perimeter of %g x %g square = %g",
29: [square side],30: [square side],31: [square perimeter]);32:33: [rectangle release];34: [square release];35:36: [pool drain];37: return 0;
38: }39:
(5) 最后是执行的结果:
(四)、 使用复合(composite),就像组合乐高积木,让写程式更简单。
(1) 复合(composite)其实不是 Objective-C 的语言特色,而是物件导向程式设计,用来化繁为简的一个作法。想像有一天,你有一个类别,继承了超过 10 个以上的子类别,会发生什么事情?
首先,各个类别的成员变数(member variable)的名字有可能会重复?各个类别的操作(method)的名字会重复?
接着,因为继承了很多类别,你的成员变数,跟操作的数量,是不是就很多,光是找名字,就累人了。当这种情形发生,用 composite 可以把类别阶层打平,方便管理。
composite 的作法,还有一个优势,就是你可以在执行时在合成你的物件,而继承是你在定义你的类别时,就已经决定。composite 在实际应用上,用得很广泛。
(2) 实际的例子如下,首先用 Xcode 建立一个新的专案 OOP4 (Object Oriented Program #4),然后新增一个类别 Rectangle 如以下列表。
1: //
2: // Rectangle.h
3: // OOP4
4: //
5: // Created by Chou Shunyuan on 5/15/10.
6: // Copyright 2010 __MyCompanyName__. All rights reserved.
7: //
8:9: #import10:11:12: @interface Rectangle : NSObject13: {14: double width;
15: double height;
16: }17:18: @property double width;
19: @property double height;
20:21: -(void) setWidth: (double) w andHeight: (double) h;22: -(double) area;
23: -(double) perimeter;
24:25: @end26:
1: //
2: // Rectangle.m
3: // OOP4
4: //
5: // Created by Chou Shunyuan on 5/15/10.
6: // Copyright 2010 __MyCompanyName__. All rights reserved.
7: //
8:9: #import "Rectangle.h"
10:11:12: @implementation Rectangle13:14: @synthesize width, height;15:16: -(void) setWidth: (double) w andHeight: (double) h17: {18: width = w;19: height = h;20: }21:22: -(double) area
23: {24: return (width * height);
25: }26:27: -(double) perimeter
28: {29: return (2 * (width + height));
30: }31:32: @end33:
(3) 接着我们新增一个类别 Square,跟之前版本不一样,这次的类别 Square 并没有继承 Rectangle,而是改成用一个成员变数 rectangle 来保存一个 Rectangle 类别的 instance 指标。特别注意,我们覆写了 init,在 init 中,我们初始化了 rectangle 变数。同时,我们也覆写了 dealloc,当 square 被释放前,用来释放掉我们所创建的 rectangle 变数。
1: //
2: // Square.h
3: // OOP4
4: //
5: // Created by Chou Shunyuan on 5/15/10.
6: // Copyright 2010 __MyCompanyName__. All rights reserved.
7: //
8:9: #import10: #import "Rectangle.h"
11:12:13: @interface Square : NSObject14: {15: Rectangle *rectangle;16: }17:18: -(id) init;19: -(void) dealloc;
20:21: -(void) setSide: (double) side;22: -(double) side;
23:24: -(double) area;
25: -(double) perimeter;
26:27: @end28:
1: //
2: // Square.m
3: // OOP4
4: //
5: // Created by Chou Shunyuan on 5/15/10.
6: // Copyright 2010 __MyCompanyName__. All rights reserved.
7: //
8:9: #import "Square.h"
10:11:12: @implementation Square13:14: -(id)init15: {16: if (self = [super init])
17: {18: rectangle = [[Rectangle alloc] init];19: }20:21: return self;
22: }23:24: -(void) dealloc
25: {26: [rectangle release];27: [super dealloc];28: }29:30: -(void) setSide: (double) side31: {32: if (rectangle)
33: {34: [rectangle setWidth: side andHeight: side];35: }36: }37:38: -(double) side
39: {40: if (rectangle)
41: {42: return rectangle.width;
43: }44:45: return 0;
46: }47:48: -(double) area
49: {50: if (rectangle)
51: {52: return [rectangle area];
53: }54:55: return 0;
56: }57:58: -(double) perimeter
59: {60: if (rectangle)
61: {62: return [rectangle perimeter];
63: }64:65: return 0;
66: }67:68: @end69:
(4) 特别注意,Square 类别,在计算面积,及计算周长时,是透过内部的 rectangle 物件,来计算的。你或许会觉得这是多此一举,用继承的方式,实做上似乎比较单纯。但是这是因为我所举的例子,所用到的类别 Rectangle 太简单了。以 composite 的方式,无论多复杂的物件,大概都是这样,依样画葫芦,就算有 100 个类别也是一样。同时,你只需要提供有用到的操作,没用到的可以忽略。
(5) 最后来看执行的结果。
未完待续,下次来谈一谈 Foundation……