面向对象程序设计简介(1/2)
在使用 Cocoa 和 Objective-C 进行编程的时候,最让人感到困惑的就是面向对象编程。几乎所有的现代编程语言都是面向对象的,学习面向对象的概念和模式对你读写代码都会带来很大的帮助。UITableView
和 UIScrollView
或者 NSString
和NSMutableString
之间的关系体现的是面向对象设计的基本理念。通过对这些理念的理解,您将更好的领会到Cocoa 和 Cocoa Touch内部为什么要像它现在这样组织,并且在您以后编写您自己的应用或者框架的时候,将会更有想法。
在本系列教程中,您将学习面向对象程序设计,内容涵盖以下几个概念:
- 对象基础
- 继承
- MVC模型
- 多态
- 常见的面向对象模式
总的来说本系列课程是为那些编程初学者而设计的-就像我刚开始学习编程时那样。你很可能还没有接触过很多编程语言,并且你也不明白为什么所有事都要以一种特定的方式去完成。
本教程将会侧重介绍面向对象设计原则,而不会介绍具体语法,所以在继续阅读之前您应该对Objective-C 和 Xcode 的基本概念有所理解。 如果您需要补充这方面的基础知识,请借鉴这篇教程 Beginning Objective-C。
准备开始
为了以更具体的方式去理解一些概念, 你将创建一个叫做Vehicles的程序。 它将用到一个能将现实世界的物件转换成虚拟对象的最常见的隐喻词“车”, 它可以是自行车,汽车或者任何其它带轮子的的东西。
比如, 这是一辆车:
这也是一辆车:
这也是一辆车:
这还是一辆车:
在这部分教程中, 你将用面向对象的技术建立一个数据模型以代表所有的这些“车”,还将创建一个简单的应用以实现这些数据模型并将这些“车”的数据显示给用户。
下载 初始工程, 它将包含一个你将用于学习面向对象编程的程序的基础框架。
对象基础
在面向对象编程中,主要的目的是分解一个“东西”的特点,并将其用于创建一个或多个对象,这个对象可以描述出这个“东西”是什么以及它能做哪些事。
有时候,就像车一样,你的“东西”在现实世界会有一个等同物。但有时候也并不一定会有这种等同物, 就像很多不同类型的 UIViewController
对象一样。 为了简单, 你先创建一些具有现实世界等同物的对象吧。
为了回答某个“东西”是什么,你首先要弄清楚这个“东西”有哪些特点。
有些语言会把这些特点作为一个“字段”,一个“成员”,甚至只是一个“变量”。然而在Objective-C 中,一个对象的特点是由它的特性(properties)所体现的.
想一想“车”的这个概念——一个能够描述涵盖所有以上图片的东西。你的脑中会浮现出一些关于“车”的什么特点呢?
- 它的轮子数量总是大于零
- 它总有某种能量来源,以使它能动起来,可以是人力,汽油,电能或者是混合动力
- 它是有品牌的,像福特,雪佛兰,哈利—戴维森,施文
- 它有类型名称,像越野车,跑车或者小汽车
- 它有出厂日期
*- 针对汽车或者卡车我们有时候也说“制造商”,但是为了清晰我们这里统一都说“品牌”。
现在你已经知道了一些车的基本特点,你已经能根据这些特点构建一个对象了。
初始工程里有两个文件:Vehicle.h 和 Vehicle.m, 它们一起组成了一个NSObject的子类 。过会儿你将会进一步了解什么子类。
将下面这部分代码加入到 Vehicle.h 文件中,位于 @interface
的下一行:
@property (nonatomic, assign) NSInteger numberOfWheels; @property (nonatomic, copy) NSString *powerSource; @property (nonatomic, copy) NSString *brandName; @property (nonatomic, copy) NSString *modelName; @property (nonatomic, assign) NSInteger modelYear; |
这这些特性(property)的声明描述了所有你想要记录的有关于这个对象的特点。
小小的题外话:特性(property)的背后
当你在Xcode 4.4或者以上的环境下声明一个@property,
Xcode 将自动为这个特性(property)合成一个后台实例变量,一个getter方法, 一个setter 方法。 这为我们省去了大量不必要的代码。 如果没有这种自动合成功能,你就需要为每一个特性(property)写以下代码:
@interface Vehicle() { NSString *_brandName; } @end @implementation Vehicle //Setter method -(void)setBrandName:(NSString *)brandName { _brandName = [brandName copy]; } //Getter method -(NSString *)brandName { return _brandName; } @end |
为每一个特性(property)省去这些代码以后,你的代码看起来会更加干净,可读性更高。并且你还可以以几种不同的方式存取一个@property
:
someVariableName = self.brandName;
这句话在背后实际上调用了[self brandName];这个事先为你合成好了的
getter 方法,它将返回_brandName
实例变量中的数据, 并将它赋值给someVariableName
.self.brandName = @"Some Brand Name";
这句话在背后实际上调用了[self setBrandName:@"Some Brand Name"];
setter 方法,它将实例变量_brandName
的值设置成@"Some Brand Name"
.
描述对象
另一个有关与对象的很重要的问题— 这个对象究竟能做什么?
从程序层面来讲,一个对象能做的事都通常被称为方法. 想一想上面图片中所有的车普遍都能做的事:
- 都能前进
- 都能后退
- 都能停下
- 都能转弯
- 都能换挡
- 都能发出某种噪音(比如喇叭或者铃铛)
大多情况下,你会使用返回值类型为void
的方法:比如, -(void)nameOfMethod
. 这在当你仅仅想要执行某个函数体而不需要从该函数体获取任何返回信息时特别有用。然而,为了能更容易的显示你的程序正在发生着什么,你将用到一些返回值类型为NSString
对象的方法。
小小的题外话:类方法与实例方法
你很可能已经注意到了,在你书写代码的时候,有些方法的前面是+号,而有的方法前面是-号。这两个不同的符号恰恰区分了这个方法是一个类方法还是一个实例方法。
最简单的区分法是将它们想象成是现实世界中的原理图:原理图永远只有一张,但是有了原理图,你就能复制任意多的拷贝。
类方法用+号来表示,它代表了这张原理图不需要进行复制就能做的操作。比如NSString
的stringWithFormat:
就是一个类方法,它能创建一个新的字符串对象。
实例方法用-号来表示,它是需要这张原理图先进行拷贝以后,它的拷贝所能执行的方法。比如, NSString
的一个实例 @"Hello There"
就有一个实例方法 lowercaseString
它将所有字符转换为小写,返回 @"hello there"。如果将
lowercaseString作为类方法是没有意义的,因为一个类根本没有用于转为为小写的字符串实例!
为你的类添加基本的方法
将以下方法添加到 Vehicle.h 头文件中,位于你早些时候添加的特性(property)以下,但位于 @end
之上:
//Basic operation methods -(NSString *)goForward; -(NSString *)goBackward; -(NSString *)stopMoving; -(NSString *)changeGears:(NSString *)newGearName; -(NSString *)turn:(NSInteger)degrees; -(NSString *)makeNoise; |
头文件中声明 的方法是公开的 – 就像告诉别的对象,“这些是我能做的事” ,但是别的对象并不知道,这些事是如何被完成的。为了完成这些事,我们将以下方法的实现添加到Vehicle.m文件中:
-(NSString *)goForward { return nil; } -(NSString *)goBackward { return nil; } -(NSString *)stopMoving { return nil; } -(NSString *)turn:(NSInteger)degrees { //Since there are only 360 degrees in a circle, calculate what a single turn would be. NSInteger degreesInACircle = 360; if (degrees > degreesInACircle || degrees < -degreesInACircle) { //The % operator returns the remainder after dividing. degrees = degrees % degreesInACircle; } return [NSString stringWithFormat:@"Turn %d degrees.", degrees]; } -(NSString *)changeGears:(NSString *)newGearName { return [NSString stringWithFormat:@"Put %@ into %@ gear.", self.modelName, newGearName]; } -(NSString *)makeNoise { return nil; } |
这些代码大部分都只是框架而已,待会儿你将实现这些方法的细节。 turn:
和 changeGears:
有一些日志输出,这些输出将帮助你理解你的函数是否正常工作。
打开 AppDelegate.m文件, 将这行导入语句添加到文件顶部:
#import "Vehicle.h" |
这样你就能在你的代码中引用Vehicle
类了。
接下来,将application:didFinishLaunchingWithOptions:
这个函数的实现替换成如下代码:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { Vehicle *vehicle = [[Vehicle alloc] init]; //Test methods with implementations NSLog(@"Vehicle turn: %@", [vehicle turn:700]); NSLog(@"Vehicle change gears: %@", [vehicle changeGears:@"Test"]); //Test methods without implementations NSLog(@"Vehicle make noise: %@", [vehicle makeNoise]); NSLog(@"Vehicle go forward: %@", [vehicle goForward]); NSLog(@"Vehicle go backward: %@", [vehicle goBackward]); NSLog(@"Vehicle stop moving: %@", [vehicle stopMoving]); return YES; } |
当vehicle实例被初始化以后,你可以调用它的每一个实例方法,看看它的日志输出。
编译并运行你的程序,你就能看到所有我们填充了数据的字符串都正常的返回了日志。但是对于那些没有设置过的特性(property),或者返回值为nil
的方法,你会看到返回的日志是(null),就像下面这样:
你将使用继承来为这些方法提供更特定的实现。
继承
继承这个概念和遗传非常像:孩子总是继承他们父母的特点。
然而,在像Objective-C这样的单一继承编程语言中,继承的概念要远比现实世界中遗传的概念要严格的多。”子“类总是继承自一个”父“类,或者说超类,而不是像现实中,你的特点实际上是你父母的特点的混合。
Vehicle 类继承自NSObject类,而NSObject类位于最底层,它几乎是Objective-C中所有类的父类。
注意: 有一些 C 语言结构体,像 CGRect 和 CGSize 它们并不是 NSObject 的子类,因为结构体并不遵循面向对象编程的原则。然而,大部分以NS 或 UI 开头的类都是 NSObject 的子类。 有关NSObject的更详细介绍,请看 Apple’s documentation 。
为了更实际的看到继承,创建一个Vehicle的子类 “Car”。点击 FileNewFile…Cocoa TouchObjective-C Class。像图示一样创建一个名为Car的 Vehicle子类:
打开 Car.m 文件,将下面的初始化函数添加到 @implementation
行以下:
- (id)init { if (self = [super init]) { // Since all cars have four wheels, we can safely set this for every initialized instance // of a car. self.numberOfWheels = 4; } return self; } |
这个init
初始化方法仅仅是把轮子的数量设置为 4。
你有没有发现,你不需要做任何额外的操作来调用 numberOfWheels
这个Vehicle 变量 。那是因为继承了 Vehicle 类以后,Car 已经能够调用所有Vehicle的公共变量和方法.
如果你需要更多的变量来描述汽车(car)该怎么办呢?除了轮子的数量以外, 汽车还有很多特殊的特点,它有几扇门呢?它的顶棚可以开关吗?它有遮阳棚吗?
当然,你可以很容易的添加这些新的特性!打开 Car.h , 在 @interface
行下添加如下代码:
@property (nonatomic, assign) BOOL isConvertible; @property (nonatomic, assign) BOOL isHatchback; @property (nonatomic, assign) BOOL hasSunroof; @property (nonatomic, assign) NSInteger numberOfDoors; |
重写方法
在你添加了这些新的特性之后,你还可以添加新的方法或者从父类中继承一些方法,并在子类中实现它们。
继承的意思是“拿一个父类中已经声明的方法,并为它创建你自己的实现”。 比如,当你创建一个 UIViewController 对象时,系统已经自动为你继承了这些方法 initWithNibName:bundle:
,viewDidLoad
, 和 didReceiveMemoryWarning。
当你继承一个方法时,你可以做两件事:
- 调用
[super method]
方法以执行父类中的所有内容,或者 - 从零开始,为子类提供新的实现
你会发现在所有的UIViewController 方法中,苹果都要求你调用 [super method]
方法 – 因为在UIViewController 类中有一些很重要的方法,以至于你的子类在执行它自己的任务前必须先执行父类中的方法。
然而,因为那些你将要继承的Car 类中的方法都返回nil,所以你仅仅只需要提供你的实现就行了,因为父类中的实现是空的,所以也就没有必要必要再调用父类方法了。
打开 Car.m 文件并添加如下私有方法以简化你的父类继承:
#pragma mark - Private method implementations - (NSString *)start { return [NSString stringWithFormat:@"Start power source %@.", self.powerSource]; } |
有些车比如自行车是不需要启动的,但是汽车是需要启动的!在这种情况下,你就不需要将start
定义为公开方法,因为它只需要本类的实现中被调用。
接着,将剩下的继承方法添加到文件中:
#pragma mark - Superclass Overrides - (NSString *)goForward { return [NSString stringWithFormat:@"%@ %@ Then depress gas pedal.", [self start], [self changeGears:@"Forward"]]; } - (NSString *)goBackward { return [NSString stringWithFormat:@"%@ %@ Check your rear view mirror. Then depress gas pedal.", [self start], [self changeGears:@"Reverse"]]; } - (NSString *)stopMoving { return [NSString stringWithFormat:@"Depress brake pedal. %@", [self changeGears:@"Park"]]; } - (NSString *)makeNoise { return @"Beep beep!"; } |
现在你有了具体的,实现完全的车的子类,你可以开始构建你的 Table View controller了。
构建用户界面
在 VehicleListTableViewController.m文件中,将如下导入语句添加到文件头部,位于Vehicle导入语句之下:
#import "Car.h" |
接下来,将下面的方法添加到didReceiveMemoryWarning
和 #pragma mark - Table View
之间:
#pragma mark - Data setup -(void)setupVehicleArray { //Create a car. Car *mustang = [[Car alloc] init]; mustang.brandName = @"Ford"; mustang.modelName = @"Mustang"; mustang.modelYear = 1968; mustang.isConvertible = YES; mustang.isHatchback = NO; mustang.hasSunroof = NO; mustang.numberOfDoors = 2; mustang.powerSource = @"gas engine"; //Add it to the array [self.vehicles addObject:mustang]; //Create another car. Car *outback = [[Car alloc] init]; outback.brandName = @"Subaru"; outback.modelName = @"Outback"; outback.modelYear = 1999; outback.isConvertible = NO; outback.isHatchback = YES; outback.hasSunroof = NO; outback.numberOfDoors = 5; outback.powerSource = @"gas engine"; //Add it to the array. [self.vehicles addObject:outback]; //Create another car Car *prius = [[Car alloc] init]; prius.brandName = @"Toyota"; prius.modelName = @"Prius"; prius.modelYear = 2002; prius.hasSunroof = YES; prius.isConvertible = NO; prius.isHatchback = YES; prius.numberOfDoors = 4; prius.powerSource = @"hybrid engine"; //Add it to the array. [self.vehicles addObject:prius]; //Sort the array by the model year NSSortDescriptor *modelYear = [NSSortDescriptor sortDescriptorWithKey:@"modelYear" ascending:YES]; [self.vehicles sortUsingDescriptors:@[modelYear]]; } |
这个函数的作用就是简单的将数据初始化的工作分离出来以构建你的vehicle数组。
找到 awakeFromNib
并将以下代码添加到这个函数的末尾处:
// Initialize the vehicle array self.vehicles = [NSMutableArray array]; // Call the setup method [self setupVehicleArray]; // Set the title of the View Controller, which will display in the Navigation bar. self.title = @"Vehicles"; |
上面这个方法会在你的Storyboard 完成构建一个UIViewController 后被执行。它调用了 你刚刚创建的 setupVehicleArray 方法,并设置了 VehicleListTableViewController的标题,以显示它的内容。
编译并运行你的程序,你会看到的将会和下图显示的一样:
你看到的这些数字可能会不一样,因为它们代表着内存地址,但是除了这以外,其它内容都应该是一样的。
好消息是这些对象已经被识别为Car 对象了。坏消息是当前显示的内容并不是非常的有用。看一看UITableViewDataSource 的代理方法 tableView:cellForRowAtIndexPath:
中都做了什么事:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath]; Vehicle *rowVehicle = self.vehicles[indexPath.row]; cell.textLabel.text = [rowVehicle description]; return cell; } |
这里,你获取了一个 UITableViewCell 对象,并以当前cell所在的行数作为 self.vehicles
数组的索引获取了一个Vehicle 对象。 紧接着你将这个Vehicle 对象的description
字符串赋值给当前cell的 textLabel
变量。description
方法(继承自 NSObject 对象)输出的字符并不是非常的友好。你会希望在Vehicle 中定义一个能够以友好方式描述 Vehicle 对象完整内容的方法。
回到 Vehicle.h 文件中并添加如下方法声明,位于所有其它方法的声明之下,但位于 @end
之上:
//Convenience method for UITableViewCells and UINavigationBar titles. -(NSString *)vehicleTitleString; |
接着, 在 Vehicle.m 文件中添加如下实现,还是位于其它方法的实现之下:
#pragma mark - Convenience Methods -(NSString *)vehicleTitleString { return [NSString stringWithFormat:@"%d %@ %@", self.modelYear, self.brandName, self.modelName]; } |
上面这个方法用每个Vehicle 对象中都会有的三个参数来完整的描述了vehicle 对象。
现在,更新VehicleListTableViewController的tableView:cellForRowAtIndexPath:
方法以使用新的描述方法,就像下面这样:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath]; Vehicle *rowVehicle = self.vehicles[indexPath.row]; cell.textLabel.text = [rowVehicle vehicleTitleString]; return cell; } |
编译并运行你的应用程序,现在它应该看上去更漂亮了:
然而,当你从列表中选择了一个 Vehicle ,你看到的将是和storyboard中所显示的一模一样的内容,而不是和你所选择的Vehicle 对象所对应的内容:
为什么会这样呢?
打开 VehicleDetailViewController.m文件, 你会看到当所有的UI在Storyboard 中被创建完成的时候,所有的IBOutlets
也都为你连接好了以节省你手动操作UI的时间,但是所有的数据都还没有连接好。
注意: 你会发现有些 IBOutlets
是在 VehicleDetailViewController.m 文件中设置的, 而不是像正常一样位于 .h 文件中。
如果你有一些参数不希望向其它的类公开,你总是可以将它们作为似有的实现写在.m 文件中。这就是以 @interface
声明的位于 .m 文件头部,紧接着类名和一对括号。比如,UIViewController()
就是 UIViewController的私有实现。
任何在此接口中声明的 @property
都仍将可以像正常 IBOutlet
(如果合理的被标注过的话) 一样从Storyboard 中和 你当前 .m 实现文件中使用,但是任何不相关的类,或者它的子类都无法使用它。
连接你的数据和视图
为了连接数据,更新 VehicleDetailViewController.m 中的 configureView
方法,利用事先设置好的 vehicle 对象,就像下面这样:
- (void)configureView { // Update the user interface for the detail vehicle, if it exists. if (self.detailVehicle) { //Set the View Controller title, which will display in the Navigation bar. self.title = [self.detailVehicle vehicleTitleString]; //Setup the basic details string based on the properties in the base Vehicle class. NSMutableString *basicDetailsString = [NSMutableString string]; [basicDetailsString appendString:@"Basic vehicle details:nn"]; [basicDetailsString appendFormat:@"Brand name: %@n", self.detailVehicle.brandName]; [basicDetailsString appendFormat:@"Model name: %@n", self.detailVehicle.modelName]; [basicDetailsString appendFormat:@"Model year: %dn", self.detailVehicle.modelYear]; [basicDetailsString appendFormat:@"Power source: %@n", self.detailVehicle.powerSource]; [basicDetailsString appendFormat:@"# of wheels: %d", self.detailVehicle.numberOfWheels]; self.vehicleDetailsLabel.text = basicDetailsString; } } |
编译并运行你的程序;从TableView 中单击一个对象,你将看到如下的详细视图:
模型-视图-控制器(MVC)封装逻辑
iOS 和很多其它现代编程语言都有一个设计模式叫做 模型-视图-控制器 ,简称 MVC 。
MVC 背后的理念主要是,视图永远只关心如何呈现,模型永远只关心数据,控制器应该能在不需要了解二者太多的内部结构的前提下,很好的将二者嫁接起来。
使用MVC最大的好处就是,当你的数据模型变了,你只需要修改一次就够了。
新人最容易犯的错误就是在 UIViewController 类里密密麻麻的的写了过多的逻辑。这就使得视图和 UIViewControllers 的连接太过于紧密,以至于这个视图很难再被重用于显示其它不同的内容。
为什么要在你的应用中实现MVC模型呢?设想如果你想往 VehicleDetailViewController 中添加更多有关汽车的详细内容 。你可以回到 configureView 方法中,并添加更多有关于汽车的具体内容,就像这样:
//Car-specific details [basicDetailsString appendString:@"nnCar-Specific Details:nn"]; [basicDetailsString appendFormat:@"Number of doors: %d", self.detailVehicle.numberOfDoors]; |
但是你要注意,这样会有一个小问题:
VehicleDetailsViewController 只知道 在 Vehicle 父类中定义的参数;它并不知道任何关于Car 子类的内容。
有很多方法可以解决这个问题。
一种最直观的方法就是导入Car.h文件, 那么 VehicleDetailViewController 就知道Car子类的所有参数了。但是那就意味着要为每一个子类添加大量的逻辑来处理这些参数。
每次你发现你自己在做这种事的时候,你都应该问问你自己:“我的视图控制器是不是做了太多了呢?”
这种情况下,你的答案是肯定的。你可以利用继承的特性,用同一个方法来为不同的子类提供对应的字符串来显示相应的内容。
通过继承创建子类
首先,将下面的新方法添加到Vehicle.h:
//Convenience method to get the vehicle's details. -(NSString *)vehicleDetailsString; |
这是公开声明的方法,它可以被像 VehicleDetailsViewController
的其它类调用。它们不需要知道每一个参数,相反,它们仅仅通过调用vehicleDetailsString
一个方法就可以获取完全格式化的字符串,然后使用它。
打开 Vehicle.m 文件并添加如下实现:
-(NSString *)vehicleDetailsString { //Setup the basic details string based on the properties in the base Vehicle class. NSMutableString *basicDetailsString = [NSMutableString string]; [basicDetailsString appendString:@"Basic vehicle details:nn"]; [basicDetailsString appendFormat:@"Brand name: %@n", self.brandName]; [basicDetailsString appendFormat:@"Model name: %@n", self.modelName]; [basicDetailsString appendFormat:@"Model year: %dn", self.modelYear]; [basicDetailsString appendFormat:@"Power source: %@n", self.powerSource]; [basicDetailsString appendFormat:@"# of wheels: %d", self.numberOfWheels]; return [basicDetailsString copy]; } |
这个方法和你添加到 VehicleDetailViewController.m中的方法非常类似,只是它返回的是一个字符串,而不是直接将它在某个地方显示出来。
现在你可以继承父类vehicle 的基础字符串并为 Car 类添加特殊的内容。打开 Car.m 并覆盖vehicleDetailsString
:方法的实现:
- (NSString *)vehicleDetailsString { //Get basic details from superclass NSString *basicDetails = [super vehicleDetailsString]; //Initialize mutable string NSMutableString *carDetailsBuilder = [NSMutableString string]; [carDetailsBuilder appendString:@"nnCar-Specific Details:nn"]; //String helpers for booleans NSString *yes = @"Yesn"; NSString *no = @"Non"; //Add info about car-specific features. [carDetailsBuilder appendString:@"Has sunroof: "]; if (self.hasSunroof) { [carDetailsBuilder appendString:yes]; } else { [carDetailsBuilder appendString:no]; } [carDetailsBuilder appendString:@"Is Hatchback: "]; if (self.isHatchback) { [carDetailsBuilder appendString:yes]; } else { [carDetailsBuilder appendString:no]; } [carDetailsBuilder appendString:@"Is Convertible: "]; if (self.isConvertible) { [carDetailsBuilder appendString:yes]; } else { [carDetailsBuilder appendString:no]; } [carDetailsBuilder appendFormat:@"Number of doors: %d", self.numberOfDoors]; //Create the final string by combining basic and car-specific details. NSString *carDetails = [basicDetails stringByAppendingString:carDetailsBuilder]; return carDetails; } |
汽车版本的这个函数首先调用了父类的相应方法以获取有关车的详细内容。接着它将和带有汽车特点的详细内容存入 carDetailsBuilder
字符串,最后再将它们二者结合起来。
现在将VehicleDetailViewController.m 文件中的 configureView
函数替换为如下的实现,以显示我们刚刚创建完成的字符串:
- (void)configureView { // Update the user interface for the detail vehicle, if it exists. if (self.detailVehicle) { //Set the View Controller title, which will display in the Navigation bar. self.title = [self.detailVehicle vehicleTitleString]; self.vehicleDetailsLabel.text = [self.detailVehicle vehicleDetailsString]; } } |
编译并运行你的程序;选择一辆车,除了看到一般信息以外,你还应该能看到带有汽车特点的信息,就像下面这样:
你的 VehicleDetailViewController 类现在已经能让 Vehicle 和 Car 类来判断所要显示的数据了。 ViewController 所做的唯一的事情就是将信息和视图连接起来。
这种方法的优势在你继续为 Vehicle 创建其它子类的时候被显现出来。就拿一个最简单的摩托车来说。
打开 FileNewFileCocoaTouchObjective-C Class, 创建一个 Vehicle 的新的子类 Motorcycle。
因为有的摩托车会发出深沉的引擎噪音,而有的摩托车的引擎声音是高亮的,所以你创建的每一个 Motorcycle 对象,你都应该为它指定它能发出的噪音种类。
在 Motorcycle.h 中,添加一个代表噪音种类的参数,位于 @interface
行后面:
@property (nonatomic, strong) NSString *engineNoise; |
接着,打开 Motorcycle.m. 添加如下 init
方法:
#pragma mark - Initialization - (id)init { if (self = [super init]) { self.numberOfWheels = 2; self.powerSource = @"gas engine"; } return self; } |
因为所有的摩托车都有两个轮子,并且都是汽油驱动的(在这个例子中,所有用电驱动的都被看作电动车,而不叫摩托车),你可以在初始化对象的时候设置它轮子的个数已经动力源。
接下来,添加下面的方法以覆盖父类中那些返回是 nil
的方法:
#pragma mark - Superclass Overrides -(NSString *)goForward { return [NSString stringWithFormat:@"%@ Open throttle.", [self changeGears:@"Forward"]]; } -(NSString *)goBackward { return [NSString stringWithFormat:@"%@ Walk %@ backwards using feet.", [self changeGears:@"Neutral"], self.modelName]; } -(NSString *)stopMoving { return @"Squeeze brakes."; } -(NSString *)makeNoise { return self.engineNoise; } |
最后,覆盖 vehicleDetailsString
方法以添加有 Motorcycle-特点的内容,就像下面这样:
- (NSString *)vehicleDetailsString { //Get basic details from superclass NSString *basicDetails = [super vehicleDetailsString]; //Initialize mutable string NSMutableString *motorcycleDetailsBuilder = [NSMutableString string]; [motorcycleDetailsBuilder appendString:@"nnMotorcycle-Specific Details:nn"]; //Add info about motorcycle-specific features. [motorcycleDetailsBuilder appendFormat:@"Engine Noise: %@", self.engineNoise]; //Create the final string by combining basic and motorcycle-specific details. NSString *motorcycleDetails = [basicDetails stringByAppendingString:motorcycleDetailsBuilder]; return motorcycleDetails; } |
现在,是时候创建一些 Motorcycle 的实例了。
打开 VehicleListTableViewController.m 确保它导入了 Motorcycle 类,否则加入下面这句话:
#import "Motorcycle.h" |
接下来,找到 setupVehicleArray
方法,并添加如下代码,位于你之前添加的 Car 对象的下面,但是位于数组排序代码的上面:
//Create a motorcycle Motorcycle *harley = [[Motorcycle alloc] init]; harley.brandName = @"Harley-Davidson"; harley.modelName = @"Softail"; harley.modelYear = 1979; harley.engineNoise = @"Vrrrrrrrroooooooooom!"; //Add it to the array. [self.vehicles addObject:harley]; //Create another motorcycle Motorcycle *kawasaki = [[Motorcycle alloc] init]; kawasaki.brandName = @"Kawasaki"; kawasaki.modelName = @"Ninja"; kawasaki.modelYear = 2005; kawasaki.engineNoise = @"Neeeeeeeeeeeeeeeeow!"; //Add it to the array [self.vehicles addObject:kawasaki]; |
上面的代码简单的初始化了两个摩托车对象,并将它们添加到车的数组中。
编译并运行你的应用程序;你将会在列表中看到你刚刚添加的 摩托车 对象 :
点击其中的一个,你将会被带到这个摩托车 的详情页面,就像下面这样:
无论是汽车还是摩托车(甚至是一个普通的老爷车),你都可以调用 vehicleDetailsString
并获得响应的详情。
适当的分离模型,视图和控制器,并运用继承,你就能够为一个父类的不同子类显示数据,而避免了为不同的子类撰写大量额外的代码。代码越少==程序员越开心:]
提供模型类中的逻辑
运用这种方法,你还可以将更多的更复杂的逻辑包装在模型类里面。想想 卡车 对象:很多不同类型的车都被称为“卡车”,从小货车到半挂车。你的卡车类需要一些逻辑,以基于这辆开车能拉多少货物而改变它的行为。
进入 FileNewFileCocoaTouchObjective-C Class, 创建一个名为Truck 的Vehicle 的子类。
添加如下整型变量到Truck.h 文件中,用于存储卡车的载重数据:
@property (nonatomic, assign) NSInteger cargoCapacityCubicFeet; |
因为卡车的类型太多了,所以你也不需要创建初始化方法以自动提供所有的详情。你可以只是简单的重写父类中那些对于任何类型的卡车都适用的方法。
打开 Truck.m 文件并添加如下方法:
#pragma mark - Superclass overrides - (NSString *)goForward { return [NSString stringWithFormat:@"%@ Depress gas pedal.", [self changeGears:@"Drive"]]; } - (NSString *)stopMoving { return [NSString stringWithFormat:@"Depress brake pedal. %@", [self changeGears:@"Park"]]; } |
接着,你需要重写一些方法,以便它能根据货车拉货量的多少返回不同的字符串。大的卡车在倒车的时候需要发出警报声,所以你可以为此创建一个私有函数(一个不声明在 .h 文件中的函数,因此对于其它的类是不可见的)。
将如下帮助代码添加到 Truck.m 文件中:
#pragma mark - Private methods - (NSString *)soundBackupAlarm { return @"Beep! Beep! Beep! Beep!"; } |
然后回到刚刚重写的那个方法中,现在你可以在 goBackward 方法中调用 soundBackupAlarm
方法,这样大卡车在后退的时候就可以发出警报声了:
- (NSString *)goBackward { NSMutableString *backwardString = [NSMutableString string]; if (self.cargoCapacityCubicFeet > 100) { //Sound a backup alarm first [backwardString appendFormat:@"Wait for "%@", then %@", [self soundBackupAlarm], [self changeGears:@"Reverse"]]; } else { [backwardString appendFormat:@"%@ Depress gas pedal.", [self changeGears:@"Reverse"]]; } return backwardString; } |
不同的卡车喇叭也不同;比如小型的卡车喇叭声和汽车的喇叭声很像,然而越大的卡车就会拥有更大的喇叭声。为了解决这种情况,你只需要在 makeNoise
方法中添加一些简单的 if/else 语句就行了。
像下面这样添加 makeNoise 方法:
- (NSString *)makeNoise { if (self.numberOfWheels <= 4) { return @"Beep beep!"; } else if (self.numberOfWheels > 4 && self.numberOfWheels <= 8) { return @"Honk!"; } else { return @"HOOOOOOOOONK!"; } } |
最后,你可以重写 vehicleDetailsString
方法以从你的 Truck 对象中获取对应的信息。就像下面这样:
-(NSString *)vehicleDetailsString { //Get basic details from superclass NSString *basicDetails = [super vehicleDetailsString]; //Initialize mutable string NSMutableString *truckDetailsBuilder = [NSMutableString string]; [truckDetailsBuilder appendString:@"nnTruck-Specific Details:nn"]; //Add info about truck-specific features. [truckDetailsBuilder appendFormat:@"Cargo Capacity: %d cubic feet", self.cargoCapacityCubicFeet]; //Create the final string by combining basic and truck-specific details. NSString *truckDetails = [basicDetails stringByAppendingString:truckDetailsBuilder]; return truckDetails; } |
现在你的 Truck 对象已经写好了,你可以试着创建一些实例。回到VehicleListTableViewController.m 中,添加如下的导入语句到文件头部以便它能使用 Truck 类:
#import "Truck.h" |
找到 setupVehicleArray
方法,在数组排序之前添加如下代码:
//Create a truck Truck *silverado = [[Truck alloc] init]; silverado.brandName = @"Chevrolet"; silverado.modelName = @"Silverado"; silverado.modelYear = 2011; silverado.numberOfWheels = 4; silverado.cargoCapacityCubicFeet = 53; silverado.powerSource = @"gas engine"; //Add it to the array [self.vehicles addObject:silverado]; //Create another truck Truck *eighteenWheeler = [[Truck alloc] init]; eighteenWheeler.brandName = @"Peterbilt"; eighteenWheeler.modelName = @"579"; eighteenWheeler.modelYear = 2013; eighteenWheeler.numberOfWheels = 18; eighteenWheeler.cargoCapacityCubicFeet = 408; eighteenWheeler.powerSource = @"diesel engine"; //Add it to the array [self.vehicles addObject:eighteenWheeler]; |
这将会在汽车和摩托车所在的数组中添加一些带有卡车特点的 Truck 对象。
编译并运行程序;点击卡车其中的一个,确保你能够看到带有卡车特点的详情,就像下面显示的这样:
看起来很棒!这些卡车信息的得来要归功于 vehicleDetailsString
方法,继承以及重写的实现。
接下来做什么呢?
你可以下载到目前为止的项目工程。
你已经创建了一个卡车基类,还有汽车,摩托车,卡车子类,并且全部列在一个table view中。然而你却没有办法确认对于不同大小类型的卡车,你的处理是否正确。
教程的第二部分 将会完成这个应用的剩余部分,以显示更多的有关车的信息。同时,你还将学习多态,以及其它一些主要的有关于面向对象编程的设计模式。
到那时,何不试试实现一个自行车类,或者为其它车的子类添加更多相关属性?或者你可以试着读读苹果有关面向对象编程的官方参考资料 Object-Oriented Programming with Objective-C。
如果有任何问题,欢迎在评论中提出!
http://www.raywenderlich.com/zh-hans/57314/面向对象程序设计简介(12)