面向对象程序设计简介(全)

Ellen Shapiro

在使用 Cocoa 和 Objective-C 进行编程的时候,最让人感到困惑的就是面向对象编程。几乎所有的现代编程语言都是面向对象的,学习面向对象的概念和模式对你读写代码都会带来很大的帮助。
UITableViewUIScrollView 或者 NSString NSMutableString 之间的关系体现的是面向对象设计的基本理念。通过对这些理念的理解,您将更好的领会到Cocoa 和 Cocoa Touch内部为什么要像它现在这样组织,并且在您以后编写您自己的应用或者框架的时候,将会更有想法。
在本系列教程中,您将学习面向对象程序设计,内容涵盖以下几个概念:

 

  • 对象基础
  • 继承
  • MVC模型
  • 多态
  • 常见的面向对象模式

总的来说本系列课程是为那些编程初学者而设计的-就像我刚开始学习编程时那样。你很可能还没有接触过很多编程语言,并且你也不明白为什么所有事都要以一种特定的方式去完成。
本教程将会侧重介绍面向对象设计原则,而不会介绍具体语法,所以在继续阅读之前您应该对Objective-C 和 Xcode 的基本概念有所理解。 如果您需要补充这方面的基础知识,请借鉴这篇教程 Beginning Objective-C

准备开始

为了以更具体的方式去理解一些概念, 你将创建一个叫做Vehicles的程序。 它将用到一个能将现实世界的物件转换成虚拟对象的最常见的隐喻词“车”, 它可以是自行车,汽车或者任何其它带轮子的的东西。
比如, 这是一辆车:
Kleinwagen_16
这也是一辆车:
Motorcycle on a white background
这也是一辆车:
B
这还是一辆车:
European 18-wheeler with canvas trailer
在这部分教程中, 你将用面向对象的技术建立一个数据模型以代表所有的这些“车”,还将创建一个简单的应用以实现这些数据模型并将这些“车”的数据显示给用户。
下载 初始工程, 它将包含一个你将用于学习面向对象编程的程序的基础框架。

对象基础

在面向对象编程中,主要的目的是分解一个“东西”的特点,并将其用于创建一个或多个对象,这个对象可以描述出这个“东西”是什么以及它能做哪些事。
有时候,就像车一样,你的“东西”在现实世界会有一个等同物。但有时候也并不一定会有这种等同物, 就像很多不同类型的 UIViewController 对象一样。 为了简单, 你先创建一些具有现实世界等同物的对象吧。
为了回答某个“东西”是什么,你首先要弄清楚这个“东西”有哪些特点。
有些语言会把这些特点作为一个“字段”,一个“成员”,甚至只是一个“变量”。然而在Objective-C 中,一个对象的特点是由它的特性(properties)所体现的.

想一想“车”的这个概念——一个能够描述涵盖所有以上图片的东西。你的脑中会浮现出一些关于“车”的什么特点呢?

  • 它的轮子数量总是大于零
  • 它总有某种能量来源,以使它能动起来,可以是人力,汽油,电能或者是混合动力
  • 它是有品牌的,像福特,雪佛兰,哈利—戴维森,施文
  • 它有类型名称,像越野车,跑车或者小汽车
  • 它有出厂日期

*- 针对汽车或者卡车我们有时候也说“制造商”,但是为了清晰我们这里统一都说“品牌”。
现在你已经知道了一些车的基本特点,你已经能根据这些特点构建一个对象了。
初始工程里有两个文件:Vehicle.hVehicle.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对象的方法。

小小的题外话:类方法与实例方法

你很可能已经注意到了,在你书写代码的时候,有些方法的前面是+号,而有的方法前面是-号。这两个不同的符号恰恰区分了这个方法是一个类方法还是一个实例方法。
最简单的区分法是将它们想象成是现实世界中的原理图:原理图永远只有一张,但是有了原理图,你就能复制任意多的拷贝。
类方法用+号来表示,它代表了这张原理图不需要进行复制就能做的操作。比如NSStringstringWithFormat:就是一个类方法,它能创建一个新的字符串对象。
实例方法用-号来表示,它是需要这张原理图先进行拷贝以后,它的拷贝所能执行的方法。比如, 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),就像下面这样:
Log output
你将使用继承来为这些方法提供更特定的实现。

继承

继承这个概念和遗传非常像:孩子总是继承他们父母的特点。
然而,在像Objective-C这样的单一继承编程语言中,继承的概念要远比现实世界中遗传的概念要严格的多。”子“类总是继承自一个”父“类,或者说超类,而不是像现实中,你的特点实际上是你父母的特点的混合。
Vehicle 类继承自NSObject类,而NSObject类位于最底层,它几乎是Objective-C中所有类的父类。

注意: 有一些 C 语言结构体,像 CGRectCGSize 它们并不是 NSObject 的子类,因为结构体并不遵循面向对象编程的原则。然而,大部分以NS 或 UI 开头的类都是 NSObject 的子类。 有关NSObject的更详细介绍,请看 Apple’s documentation

为了更实际的看到继承,创建一个Vehicle的子类 “Car”。点击 FileNewFile…Cocoa TouchObjective-C Class。像图示一样创建一个名为CarVehicle子类:
Add Car
打开 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。
当你继承一个方法时,你可以做两件事:

  1. 调用 [super method] 方法以执行父类中的所有内容,或者
  2. 从零开始,为子类提供新的实现

你会发现在所有的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的标题,以显示它的内容。
编译并运行你的程序,你会看到的将会和下图显示的一样:
Three Cars
你看到的这些数字可能会不一样,因为它们代表着内存地址,但是除了这以外,其它内容都应该是一样的。
好消息是这些对象已经被识别为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 对象。
现在,更新VehicleListTableViewControllertableView: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;
}

编译并运行你的应用程序,现在它应该看上去更漂亮了:
Three Vehicles With Readable Titles
然而,当你从列表中选择了一个 Vehicle ,你看到的将是和storyboard中所显示的一模一样的内容,而不是和你所选择的Vehicle 对象所对应的内容:
Before Data Hookup Detail为什么会这样呢?
打开 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 中单击一个对象,你将看到如下的详细视图:
Basic vehicle details

模型-视图-控制器(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];

但是你要注意,这样会有一个小问题:
Error with Car properties
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];
    }
}

编译并运行你的程序;选择一辆车,除了看到一般信息以外,你还应该能看到带有汽车特点的信息,就像下面这样:
Basic and car-specific details
你的 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];

上面的代码简单的初始化了两个摩托车对象,并将它们添加到车的数组中。
编译并运行你的应用程序;你将会在列表中看到你刚刚添加的 摩托车 对象 :
Added Motorcycles
点击其中的一个,你将会被带到这个摩托车 的详情页面,就像下面这样:
Motorcycle Details
无论是汽车还是摩托车(甚至是一个普通的老爷车),你都可以调用 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 对象。
编译并运行程序;点击卡车其中的一个,确保你能够看到带有卡车特点的详情,就像下面显示的这样:
Truck-specific Details
看起来很棒!这些卡车信息的得来要归功于 vehicleDetailsString 方法,继承以及重写的实现。

接下来做什么呢?

你可以下载到目前为止的项目工程

你已经创建了一个卡车基类,还有汽车,摩托车,卡车子类,并且全部列在一个table view中。然而你却没有办法确认对于不同大小类型的卡车,你的处理是否正确。

教程的第二部分 将会完成这个应用的剩余部分,以显示更多的有关车的信息。同时,你还将学习多态,以及其它一些主要的有关于面向对象编程的设计模式。

到那时,何不试试实现一个自行车类,或者为其它车的子类添加更多相关属性?或者你可以试着读读苹果有关面向对象编程的官方参考资料 Object-Oriented Programming with Objective-C


第二部分

本教程的第一部分中,你学会了面向对象设计的基本概念:对象,继承以及模型-视图-控制器(MVC)模式。你初步完成了一个叫做 Vehicles 的程序,它帮助你更好的理解所学的这些概念。
在这第二部分中,你将学习多态性以及其它一些面向对象编程的关键概念:类工厂方法和单例。
如果你已经完成了本教程的前半部分,那就太好了!你可以在本教程中继续使用之前您所使用的工程。然而,如果你刚刚开始阅读本篇教程,你也可以从这里下载我们为你准备的第一部分完整工程

 

多态性(Polymorphism)

关于多态的普通定义来源于它的希腊词根 –  “Poly” 表示很多, “Morph” 表示形式。
在计算机科学中,这个词有特别的定义,依据 Free Online Dictionary of Computing网站的解释:
一个变量,它在可能指向一个对象,这个对象的类在编译时还未知,但是会在运行时根据实际指向的类执行相应的反馈。
这些定义最终可以归结为“一个对象同时可以成为不同的事物”。
Objective-C 中的多态性有一些子类型,但是其中最主要的两种类型,也是最常见的两种就是修饰模式和适配器模式。

修饰(Decorator)模式

苹果公司的基础文档 Cocoa 设计模式中有这样的解释:
修饰设计模式将额外的职责动态的附加给一个对象。修饰模式为用于拓展功能性的子类化提供了灵活的选择。就像子类化一样,修饰模式能让你不用修改原来的代码就能合并添加新的功能。 修饰类包含了一个被拓展行为类的对象。 
在Objective-C中,一个修饰模式的最典型例子就是类别的使用。
类别是iOS中一种特别的类,它能让你在不继承一个类,也不需要修改这个类的源代码的情况下为这个类添加额外你所需要的方法。它主要被用来拓展iOS自带的UIKit组件。
类别与子类之间的区别非常的简单:类别能让你为一个存在的类添加新的方法,但是你不能修改已经存在的方法。你不能为一个类别添加新的特性或者实例变量 – 你只能使用那些本来就存在的。如果你想添加新的特性或者实例变量,那你就要考虑使用继承来创建一个子类,并添加你想要添加的特性和方法了。
但是如果你不需要这样做呢?假如你只是需要将你经常使用的一些方法封装进UIKit对象呢?在这种情况下,类别就是你的最佳解决方案。
在你的练习应用中,你将为UIAlertView添加一个简便方法,以避免为一个简单的警告界面重复的书写 分配-初始化-显示 这些步骤。

实现修饰模式

打开 FileNewFileCocoa Touch, 并选择 Objective-C Category:
Objective-C Category
Convenience 作为类别名填入第一栏,在第二栏选择添加一个UIAlertView的类别:
Adding Convenience Category to UIAlertView
一旦你创建完成这些文件,你就会发现 Xcode 给类别不同的文件名以区分这是一个类别文件,就像下面显示的这样:
UIAlertView+Convenience
这种 [原类名]+[类别名] 的形式同时指明了被修饰的类名和类别本身的名字。你甚至可以在同一个应用中为同一个类添加各种不同的类别;这样还能使这个类别在其它的应用中更容易被使用。
在类别中创建一个方法,就像为一个普通的类创建方法一样。因为你将要创建一个UIAlertView 的新的实例,而不是使用已经存在的实例,打开 UIAlertView+Convenience.h 文件,  @interface 行之后添加如下方法声明:

// Shows a UIAlertView with the given title and message, and an OK button to dismiss it.
+ (void)showSimpleAlertWithTitle:(NSString *)title andMessage:(NSString *)message;

接着,打开 UIAlertView+Convenience.m 文件,添加如下方法实现:

+ (void)showSimpleAlertWithTitle:(NSString *)title andMessage:(NSString *)message
{
    UIAlertView *simpleAlert = [[UIAlertView alloc] initWithTitle:title
                                                          message:message
                                                         delegate:nil
                                                cancelButtonTitle:@"OK"
                                                otherButtonTitles:nil];
    [simpleAlert show];
}

这里所做事非常简单 — 你只是集成了一些你要重复使用的代码,它产生一个简单的警告窗口,上面带一个可以让窗口消失的取消按钮。
接着,打开 VehicleDetailViewController.m 文件,并添加如下导入语句:

#import "UIAlertView+Convenience.h"

在这个文件的底部,你会发现一些 IBAction 方法,方法体中仅仅有  TODO 注释。像下面这样更新goForward, goBackward, stopMoving, 和 makeNoise 方法以使用你的新类别:

-(IBAction)goForward
{
    [UIAlertView showSimpleAlertWithTitle:@"Go Forward" andMessage:[self.detailVehicle goForward]];
}
-(IBAction)goBackward
{
    [UIAlertView showSimpleAlertWithTitle:@"Go Backward" andMessage:[self.detailVehicle goBackward]];
}
-(IBAction)stopMoving
{
    [UIAlertView showSimpleAlertWithTitle:@"Stop Moving" andMessage:[self.detailVehicle stopMoving]];
}
-(IBAction)makeNoise
{
    [UIAlertView showSimpleAlertWithTitle:@"Make Some Noise!" andMessage:[self.detailVehicle makeNoise]];
}

编译并运行你的应用;选择一辆车以后,随意点击一个除了 “Turn…” 之外的按钮,你会看到根据不同车所显示的对应的消息。比如,如果你在不同车中都点击“Make Some Noise!” 按钮,你看到的将会是下面这样:
Make some noise!
但是如果你要做一些更复杂的事情呢 – 比如你需要从你显示的 UIAlertView 中获取一些信息呢?这个时候,适配器模式和委托就要派上用场了。

适配器(Adapter)模式

再看苹果文档中的解释 Cocoa Fundamentals Guide:
适配器设计模式将一个类的接口转变为另外一种用户期望的接口。适配器让那些因为接口不适配而冲突的类能够一起工作。对目标对象的类实现了解耦。
协议是 Objective-C 中适配器的最主要的例子。它可以指定一些能被任何类所实现的方法。它们通常被用于  DataSourceDelegate 方法,但是也可以用于帮助两个完全不相关类之间的通信。
这种模式的优势在于只需要某个类声明它遵从这个协议,无论这个类是个模型或者视图又或是控制器都没关系。它只想知道在另外一个类里所发生的事,并为此实现所有需要的方法。
为了知道用户希望车所应转弯的角度,你就需要利用 UIAlertViewDelegate 协议以获取用户输入UIAlertView的数据。

实现适配器模式

打开 VehicleDetailViewController.h 并将它声明为遵从 UIAlertViewDelegate 协议,像下面这样将协议名加入一对括号中:

@interface VehicleDetailViewController : UIViewController <UIAlertViewDelegate>

如果要将上面这行语句翻译为中文,将会是:“这是一个VehicleDetailViewController 它是UIViewController 的子类,并遵从于 UIAlertViewDelegate 协议”。如果一个类遵从于多个协议,你只需要将它们一起列在括号中并用逗号分开就行了。

注意: 在一个类中实现某个特定的协议通常还被称为“遵从”这个协议。

你将用这些来实现一个获取用户期望转弯度数的机制。
打开 VehicleDetailViewController.mturn 方法替换为如下的实现:

-(IBAction)turn
{
    //Create an alert view with a single text input to capture the number of degrees
    //to turn your vehicle. Set this class as the delegate so one of the delegate methods
    //can retrieve what the user entered.
    UIAlertView *turnEntryAlertView = [[UIAlertView alloc] initWithTitle:@"Turn" message:@"Enter number of degrees to turn:" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Go!", nil];
    turnEntryAlertView.alertViewStyle = UIAlertViewStylePlainTextInput;
    [[turnEntryAlertView textFieldAtIndex:0] setKeyboardType:UIKeyboardTypeNumberPad];
    [turnEntryAlertView show];
}

该方法创建了一个带输入框的 UIAlertView ,用来提示用户输入一个数字。
接下来,你需要为 UIAlertView 实例添加一个委托方法作为用户输入一个数字以后的回调函数。添加如下的方法:

#pragma mark - UIAlertViewDelegate method
-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
    //Note: Only one alert view will actually declare this class its delegate, so we can
    //      proceed without double-checking the alert view instance. If you have more than
    //      one alert view using the same class as its delegate, make sure you check which
    //      UIAlertView object is calling this delegate method.
    if (buttonIndex != alertView.cancelButtonIndex) {
        //Get the text the user input in the text field
        NSString *degrees = [[alertView textFieldAtIndex:0] text];
 
        //Convert it from a string to an integer
        NSInteger degreesInt = [degrees integerValue];
 
        //Use the simple alert view to display the information for turning. 
        [UIAlertView showSimpleAlertWithTitle:@"Turn" andMessage:[self.detailVehicle turn:degreesInt]];
    } //else the user has cancelled, and we don't need to do anything.
}

以上的代码实现了一个选定的 UIAlertViewDelegate 中的一个方法,它用于监听UIAlertView中某个按钮被按下的事件。编译并运行你的程序;从列表里选择一辆车,点击 Turn 按钮,并输入一个需要转弯的角度,就像这样:
Turn 90 degrees
如果你点击了Cancel,将不会发生任何事,因为你在委托的实现中忽略了它。然而,如果你点击 Go!,前面那个 UIAlertView 将会消失,下面这个 UIAlertView 将会出现:
Turn complete!
你的应用现在在功能上已经完整了。然而,你总会希望将你的代码写的更加优雅,更易于管理和扩展。是时候介绍另外两个面向对象的设计模式了,它们将大大简化你的编程工作。

额外的面向对象模式

尽管在面向对象编程中你可以运用大量的模式(事实上, Eli Ganem 已经发表了一篇相关的 教程),其中有两种对你的‘车’应用来说显得特别的有用:类工厂方法单例。
它们都被广泛的运用于 iOS 开发,理解它们将有效的帮助你理解未来你作为一个iOS开发者所要接触到的大部分代码。

类工厂方法

类工厂的主要理念在于产生并返回一个特定类的实例对象,并在产生对象时尽可能的预填充数据。相比调用 alloc/init 再设置特性,使用类工厂的一个显而易见的好处就是,你的代码将会缩短很多。
这样,你就能使用一个方法来创建一个对象并初始化它的所有特性,而不需要用一个方法来创建对象,再用很多行代码来设置各种特性。与此同时,这种技术还有两个不太明显的好处。
其一,它强制了每个使用你的类的人必须提供你所需要的每一项数据,以创建一个功能完整的实例对象。鉴于你在本教程的前面部分所创建的那些对象,你可能也发现了,往往很容易就忘记了一个或两个特性的初始化。有了类工厂方法, 你将被强制关注你创建对象所需要的每一项特性。
其二,公认的减少了使用了 ARC 的代码将产生的问题,在 iOS 5之后类工厂方法将返回自动释放池对象,释放调用者而不必在以后再释放。你可能不需要担心这个问题,除非你需要兼容老的代码,但是这依旧是一个值得注意的问题。

实现车的类工厂方法

打开Vehicle.h 文件并声明如下类工厂方法,该方法的参数代表了一个车的所有基本参数:

//Factory Method
+ (instancetype)vehicleWithBrandName:(NSString *)brandName modelName:(NSString *)modelName modelYear:(NSInteger)modelYear powerSource:(NSString *)powerSource wheels:(NSInteger)numberOfWheels;

instancetype 类型是一个稍微安全版本的 id。一个 id 类型的参数或者返回值,将会接受任何 NSObject 的子类,instancetype 作为一个方法签名,它告诉你在这个实例被初始化后,你将收到的一定是这个类或者它的子类的实例。

注意: 更多关于instancetype 的介绍,请阅读 NSHipster

另外一件需要关注的事是有关于类工厂方法和继承:因为类工厂方法返回一个完全初始化的对象,所以在父类中你要小心的使用它们,它返回一个特殊类的对象。
打开 Vehicle.m 文件,添加如下类工厂方法的实现:

#pragma mark - Factory method
+ (instancetype)vehicleWithBrandName:(NSString *)brandName modelName:(NSString *)modelName modelYear:(NSInteger)modelYear powerSource:(NSString *)powerSource wheels:(NSInteger)numberOfWheels;
{
    //Use self in the superclass to ensure you're getting the proper return type for each of the subclasses. 
    Vehicle *newVehicle = [[self alloc] init];
 
    //Set the provided values to the appropriate instance variables.
    newVehicle.brandName = brandName;
    newVehicle.modelName = modelName;
    newVehicle.modelYear = modelYear;
    newVehicle.powerSource = powerSource;
    newVehicle.numberOfWheels = numberOfWheels;
    //Return the newly created instance.
    return newVehicle;
}

这里的类工厂方法初始化了对象并设置了特性。因为车将会有子类,所以请确保你使用的是 [[self alloc] init] 而不是 [[Vehicle alloc] init]。这样, 像 Car 这样的子类也可以使用这个继承的类工厂方法来获取一个Car对象,而不是 Vehicle 对象。

注意: Quality Coding 网站有一篇深入分析此话题的文章 How to Botch Your Objective-C Factory Method

 

实现汽车的类工厂方法

打开 Car.h 文件并声明如下的类工厂方法:

//Factory Method
+(Car *)carWithBrandName:(NSString *)brandName modelName:(NSString *)modelName modelYear:(NSInteger)modelYear powerSource:(NSString *)powerSource numberOfDoors:(NSInteger)numberOfDoors convertible:(BOOL)isConvertible hatchback:(BOOL)isHatchback sunroof:(BOOL)hasSunroof;

因为你需要的是有关于 Vehicle 类的所有信息,而不仅仅是轮子的个数,你将所有有关Vehicle 类的特性和带有Car-特点的特性作为方法的参数。
打开 Car.m 文件,将 init 方法替换为如下的类工厂方法实现:

#pragma mark - Factory Method
+(Car *)carWithBrandName:(NSString *)brandName modelName:(NSString *)modelName modelYear:(NSInteger)modelYear powerSource:(NSString *)powerSource numberOfDoors:(NSInteger)numberOfDoors convertible:(BOOL)isConvertible hatchback:(BOOL)isHatchback sunroof:(BOOL)hasSunroof
{
    //Create the car object using the superclass factory method.
    Car *newCar = [Car vehicleWithBrandName:brandName modelName:modelName modelYear:modelYear powerSource:powerSource wheels:4];
    //Set the car-specific properties using the passed-in variables.
    newCar.numberOfDoors = numberOfDoors;
    newCar.isConvertible = isConvertible;
    newCar.isHatchback = isHatchback;
    newCar.hasSunroof = hasSunroof;
 
    //Return the fully instantiated Car object.
    return newCar;
}

注意依据以往的经验法则来说,你并不一定必须从 init 方法和类工厂方法中二选一;然而在本例中你将不会再直接使用 init 方法,取而代之的类工厂方法将负责所有init 方法曾经所做的事。这种管理旧代码的方法是合理的,因为你将不会再需要它们。
接下来,打开 VehicleListTableViewController.m 并更新 setupVehicleArray 方法以对你创建的每个汽车类使用新的类工厂方法,就像下面这样:

    //Create a car.
    Car *mustang = [Car carWithBrandName:@"Ford" modelName:@"Mustang" modelYear:1968
      powerSource:@"gas engine" numberOfDoors:2 convertible:YES hatchback:NO sunroof:NO];
 
    //Add it to the array
    [self.vehicles addObject:mustang];
 
    //Create another car.
    Car *outback = [Car carWithBrandName:@"Subaru" modelName:@"Outback" modelYear:1999
      powerSource:@"gas engine" numberOfDoors:5 convertible:NO hatchback:YES sunroof:NO];
 
    //Add it to the array.
    [self.vehicles addObject:outback];
 
    //Create another car
    Car *prius = [Car carWithBrandName:@"Toyota" modelName:@"Prius" modelYear:2007
      powerSource:@"hybrid engine" numberOfDoors:5 convertible:YES hatchback:YES sunroof:YES];
 
    //Add it to the array.
    [self.vehicles addObject:prius];

编译并运行你的应用;一切看起来就像往常一样,但是你知道实际上在创建你的 Vehicle 数组的时候,你的代码量大大减少了。现在你可以将这种模式应用到 Motorcycle 和 Truck 类上了。

实现摩托车的类工厂方法

在 Motorcycle.h 文件中,添加如下新的类工厂方法声明:

//Factory Method
+(Motorcycle *)motorcycleWithBrandName:(NSString *)brandName modelName:(NSString *)modelName modelYear:(NSInteger)modelYear engineNoise:(NSString *)engineNoise;

在此情况下,你将添加一些特定的参数以创建 Motorcycles的实例。
现在打开 Motorcycle.m 并将 init 方法替换为你的类工厂方法的实现,就像下面这样:

#pragma mark - Factory Method
+(Motorcycle *)motorcycleWithBrandName:(NSString *)brandName modelName:(NSString *)modelName modelYear:(NSInteger)modelYear engineNoise:(NSString *)engineNoise
{
    //Create a new instance of the motorcycle with the basic properties by calling the Factory
    //method on the superclass.
    Motorcycle *newMotorcycle = [Motorcycle vehicleWithBrandName:brandName modelName:modelName modelYear:modelYear powerSource:@"gas engine" wheels:2];
 
    //Set the Motorcycle-specific properties.
    newMotorcycle.engineNoise = engineNoise;
 
    return newMotorcycle;
}

 

实现卡车的类工厂方法

打开 Truck.h 并添加如下类工厂方法声明:

//Factory Method
+(Truck *)truckWithBrandName:(NSString *)brandName modelName:(NSString *)modelName modelYear:(NSInteger)modelYear powerSource:(NSString *)powerSource wheels:(NSInteger)numberOfWheels cargoCapacityCubicFeet:(NSInteger)cargoCapacityCubicFeet;

就像之前一样,你将带入一些特定于你新的 Vehicle 实例的参数 — 在此情况下, 卡车
打开 Truck.m, 添加如下类工厂方法的实现 (本例中没有可以替代的 init 方法):

#pragma mark - Factory Method
+(Truck *)truckWithBrandName:(NSString *)brandName modelName:(NSString *)modelName modelYear:(NSInteger)modelYear powerSource:(NSString *)powerSource wheels:(NSInteger)numberOfWheels cargoCapacityCubicFeet:(NSInteger)cargoCapacityCubicFeet
{
    //Create a new instance using the superclass's factory method. 
    Truck *newTruck = [Truck vehicleWithBrandName:brandName modelName:modelName modelYear:modelYear powerSource:powerSource wheels:numberOfWheels];
 
    newTruck.cargoCapacityCubicFeet = cargoCapacityCubicFeet;
 
    //Return the newly created truck instance.
    return newTruck;
}

现在你已经为 MotorcycleTruck 创建了你的类工厂方法,回到 VehicleDetailsViewController.m 并更新你的代码以利用新的类工厂方法,就像下面这样:

    //Add a motorcycle
    Motorcycle *harley = [Motorcycle motorcycleWithBrandName:@"Harley-Davidson"
      modelName:@"Softail" modelYear:1979 engineNoise:@"Vrrrrrrrroooooooooom!"];
 
    //Add it to the array.
    [self.vehicles addObject:harley];
 
    //Add another motorcycle
    Motorcycle *kawasaki = [Motorcycle motorcycleWithBrandName:@"Kawasaki"
      modelName:@"Ninja" modelYear:2005 engineNoise:@"Neeeeeeeeeeeeeeeeow!"];
 
    //Add it to the array
    [self.vehicles addObject:kawasaki];
 
    //Create a truck
    Truck *silverado = [Truck truckWithBrandName:@"Chevrolet" modelName:@"Silverado"
      modelYear:2011 powerSource:@"gas engine" wheels:4 cargoCapacityCubicFeet:53];
 
    [self.vehicles addObject:silverado];
 
    //Create another truck
    Truck *eighteenWheeler = [Truck truckWithBrandName:@"Peterbilt" modelName:@"579"
      modelYear:2013 powerSource:@"diesel engine" wheels:18 cargoCapacityCubicFeet:408];
 
    [self.vehicles addObject:eighteenWheeler];

编译并运行你的应用;一切还是像往常一样工作。然而,你缩短并简化了你的代码,将你经常需要重复使用的代码转移到可以重用的类工厂方法中去。
类工厂方法不仅使用方便,还大大降低了再不经意间漏掉一个特性的可能性。它们可能很常见,像 NSString 的 stringWithFormat: 或 UIButton 的buttonWithType: – 现在你已经将它们添加到你自己的车类及其它的子类中!

单例模式

类工厂方法中一种很有用也很特别的方法就是 单例。它确保了一个类的一个特定实例永远只被初始化一次。
这对于那些只需要一个实例的东西来说是很棒的  — 比如,  UIApplication 单例 sharedApplication — 或者那些初始化开销很大的类,又或者虽然所存的数据很小但是你的应用从始至终都需要获取并更新它。
在 Vehicles 应用中,你会发现有一个数据可能我们从始至终都需要接触或者更新的:车的列表。这个列表也违反了 MVC 原则,因为它让  VehicleListTableViewController 来管理它的创建和生存。通过将车的列表移交到它自己的单例类中去,你的代码在将来会更具可拓展性。
打开 FileNewFileObjective-C Class 并创建一个名为VehicleList 的 NSObject 的子类。打开VehicleList.h 并添加如下类方法声明和一个用于存储车的数组特性:

//The list of vehicles.
@property (nonatomic, strong) NSArray *vehicles;
//Singleton Instance
+ (VehicleList *)sharedInstance;

接着,打开 VehicleList.m 并添加如下单例类工厂方法的实现:

+ (VehicleList *)sharedInstance
{
    //Declare a static instance variable
    static VehicleList *_vehicleList = nil;
 
    //Create a token that facilitates only creating this item once.
    static dispatch_once_t onceToken;
 
    //Use Grand Central Dispatch to create a single instance and do any initial setup only once.
    dispatch_once(&onceToken, ^{
        //These are only invoked the onceToken has never been used before.
        _vehicleList = [[VehicleList alloc] init];
        _vehicleList.vehicles = [VehicleList initialVehicleList];
    });
 
    //Returns the shared instance variable.
    return _vehicleList;
}

注意你将  _vehicleList 实例变量和 onceToken GCD 标记声明为 static 变量。这意味着这个变量将存在于这个应用的整个声明周期中。这将从两个方面有助于单例的创建:

  1. 相比检查 _vehicleList 实例变量是否为空, GCD 能更快的检测 onceToken 是否被执行过了,以相应的决定是否需要创建  _vehicleList 实例。使用 GCD 来执行这个检测操作同时也是线程安全的,因为 dispatch_once 确保了当它被从多线程调用的时候,下一个对象只有在当前线程结束之后才会被允许创建实例。
  2. _vehicleList 实例不会被意外的覆盖掉,因为静态变量只能被初始化一次。如果任何人在你的_vehicleList变量被初始化后,不经意间另外调用了一次[[VehicleList alloc] init] ,将不会对你现有的 VehicleList 对象有任何效果。

接下来,你需要将车的创建工作从 VehicleListTableViewController 转移到 VehicleList 类中。
首先,在VehicleList.m文件的头部导入 Car, Motorcycle, 和 Truck 类:

#import "Car.h"
#import "Motorcycle.h"
#import "Truck.h"

接着,在 VehicleList.m文件中添加如下方法:

+ (NSArray *)initialVehicleList
{
    //Initialize mutable array.
    NSMutableArray *vehicles = [NSMutableArray array];
 
    //Create a car.
    Car *mustang = [Car carWithBrandName:@"Ford" modelName:@"Mustang" modelYear:1968
      powerSource:@"gas engine" numberOfDoors:2 convertible:YES hatchback:NO sunroof:NO];
 
    //Add it to the array
    [vehicles addObject:mustang];
 
    //Create another car.
    Car *outback = [Car carWithBrandName:@"Subaru" modelName:@"Outback" modelYear:1999
      powerSource:@"gas engine" numberOfDoors:5 convertible:NO hatchback:YES sunroof:NO];
 
    //Add it to the array.
    [vehicles addObject:outback];
 
    //Create another car
    Car *prius = [Car carWithBrandName:@"Toyota" modelName:@"Prius" modelYear:2007
      powerSource:@"hybrid engine" numberOfDoors:5 convertible:YES hatchback:YES sunroof:YES];
 
    //Add it to the array.
    [vehicles addObject:prius];
 
    //Add a motorcycle
    Motorcycle *harley = [Motorcycle motorcycleWithBrandName:@"Harley-Davidson" modelName:@"Softail"
      modelYear:1979 engineNoise:@"Vrrrrrrrroooooooooom!"];
 
    //Add it to the array.
    [vehicles addObject:harley];
 
    //Add another motorcycle
    Motorcycle *kawasaki = [Motorcycle motorcycleWithBrandName:@"Kawasaki" modelName:@"Ninja"
      modelYear:2005 engineNoise:@"Neeeeeeeeeeeeeeeeow!"];
 
    //Add it to the array
    [vehicles addObject:kawasaki];
 
    //Create a truck
    Truck *silverado = [Truck truckWithBrandName:@"Chevrolet" modelName:@"Silverado" modelYear:2011
      powerSource:@"gas engine" wheels:4 cargoCapacityCubicFeet:53];
 
    [vehicles addObject:silverado];
 
    //Create another truck
    Truck *eighteenWheeler = [Truck truckWithBrandName:@"Peterbilt" modelName:@"579" modelYear:2013
      powerSource:@"diesel engine" wheels:18 cargoCapacityCubicFeet:408];
 
    [vehicles addObject:eighteenWheeler];
 
    //Sort the array by the model year
    NSSortDescriptor *modelYear = [NSSortDescriptor sortDescriptorWithKey:@"modelYear" ascending:YES];
    [vehicles sortUsingDescriptors:@[modelYear]];
 
    return vehicles;
}

以上方法创建或者重置车的列表,并能在任何时候被调用。
你会发现大部分代码都是从VehicleListTableViewController转移过来的,但是现在所有的车都被添加到新创建的一个局部变量 vehicles 数组,而不是VehicleListTableViewController的 self.vehicles。
现在你可以回到 VehicleListTableViewController.m 并移除三样我们不再需要的内容:

  1. 删除整个 setupVehiclesArray 方法,并删除其在awakeFromNib中的调用。
  2. 删除 vehicles 实例变量并删除其在 awakeFromNib中的初始化调用。
  3. 删除 Car.h, Motorcycle.h, 和 Truck.h#imports语句。

你的VehicleListTableViewController的私有接口以及 awakeFromNib 的实现现在看起来应该是这样的:

@interface VehicleListTableViewController ()
@end
@implementation VehicleListTableViewController
#pragma mark - View Lifecycle
- (void)awakeFromNib
{
    [super awakeFromNib];
    //Set the title of the View Controller, which will display in the Navigation bar.
    self.title = @"Vehicles";
}

你将注意到Xcode 提示你有三个错误,因为你有三个地方使用了 vehicles 特性以填充 UITableViewDataSource 和 segue 处理方法。你需要用你新的单例取代它们。
首先,在 VehicleListTableViewController.m 文件头部导入 VehicleList 类,以便你可以使用单例:

#import "VehicleList.h"

接着,找到三个 Xcode 提示出错的地方,并将代码更新为使用 VehicleList 单例的vehicles 数组,就像下面这样:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [[VehicleList sharedInstance] vehicles].count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
    Vehicle *vehicle = [[VehicleList sharedInstance] vehicles][indexPath.row];
    cell.textLabel.text = [vehicle vehicleTitleString];
    return cell;
}
#pragma mark - Segue handling
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    if ([[segue identifier] isEqualToString:@"showDetail"]) {
        NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
        Vehicle *selectedVehicle = [[VehicleList sharedInstance] vehicles][indexPath.row];
        [[segue destinationViewController] setDetailVehicle:selectedVehicle];
    }
}

编译并运行你的应用;你会看到和之前一样的列表,但是你会因此而睡得更安稳,因为你知道应用背后的代码更加干净,简洁也更易于扩展了。
基于以上这些改变,你将能够轻易的为这个列表添加新的Vehicles。比如,你想添加一个新的 UIViewController 来允许用户添加他们自己的 Vehicle, 你就只需要将它添加到单例的 Vehicles 数组。
又或者,你想让用户能够编辑Vehicle对象,你能确保所有的数据都被存储并返回,而不需要为VehicleListViewController实现一个 delegate。
有关于单例有一件事特别需要注意:它们将在你的应用的整个生命周期中生存着,因此你不应该给它们加载太多数据。它们在轻量级的数据存储以及使对象们对整个应用范围内可见是很棒的。
如果你需要存储大量的数据,那么你可能需要寻找一些更加健壮的工具来帮助存储和检索数据,就像 Core Data 。

接下来做什么呢?

在一个单独的应用中,你运用基本对象,继承,MVC模式,多态性还有单例和类工厂方法创建了一个干净的,面向对象的应用程序。你可以通过 这里回顾本工程的完成版代码。
有关于单例的更多介绍,你可以阅读 Mike Ash的一篇极棒的文章 the Care and Feeding of Singletons
如果你需要了解更多有关于面向对象模式的信息,Eli Ganem 写了一篇很好的教程,叫做 iOS Design Patterns , 它回顾了一些本教程中提到的内容,接着它介绍了一些更加高级的设计模式,你可以用来构建更加优雅的代码。
如果你有任何问题,欢迎在下面的评论中留言


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值