![Objective-C:面向对象程序设计(I) Object-oriented design](http://cdn3.raywenderlich.com/wp-content/uploads/2013/11/oop.jpg)
UIScrollView
NSString
NSMutableString
在本系列教程中,您将学习面向对象程序设计,内容涵盖以下几个概念:
- 对象基础
- 继承
- MVC模型
- 多态
- 常见的面向对象模式
总的来说本系列课程是为那些编程初学者而设计的-就像我刚开始学习编程时那样。你很可能还没有接触过很多编程语言,并且你也不明白为什么所有事都要以一种特定的方式去完成。
本教程将会侧重介绍面向对象设计原则,而不会介绍具体语法,所以在继续阅读之前您应该对Objective-C 和 Xcode 的基本概念有所理解。
准备开始
为了以更具体的方式去理解一些概念, 你将创建一个叫做Vehicles的程序。 它将用到一个能将现实世界的物件转换成虚拟对象的最常见的隐喻词“车”, 它可以是自行车,汽车或者任何其它带轮子的的东西。
比如, 这是一辆车:
这也是一辆车:
这也是一辆车:
这还是一辆车:
在这部分教程中, 你将用面向对象的技术建立一个数据模型以代表所有的这些“车”,还将创建一个简单的应用以实现这些数据模型并将这些“车”的数据显示给用户。
对象基础
在面向对象编程中,主要的目的是分解一个“东西”的特点,并将其用于创建一个或多个对象,这个对象可以描述出这个“东西”是什么以及它能做哪些事。
有时候,就像车一样,你的“东西”在现实世界会有一个等同物。但有时候也并不一定会有这种等同物, 就像很多不同类型的 UIViewController
为了回答某个“东西”是什么,你首先要弄清楚这个“东西”有哪些特点。
有些语言会把这些特点作为一个“字段”,一个“成员”,甚至只是一个“变量”。然而在Objective-C 中,一个对象的特点是由它的特性(properties)所体现的.
想一想“车”的这个概念——一个能够描述涵盖所有以上图片的东西。你的脑中会浮现出一些关于“车”的什么特点呢?
- 它的轮子数量总是大于零
- 它总有某种能量来源,以使它能动起来,可以是人力,汽油,电能或者是混合动力
- 它是有品牌的,像福特,雪佛兰,哈利—戴维森,施文
- 它有类型名称,像越野车,跑车或者小汽车
- 它有出厂日期
*- 针对汽车或者卡车我们有时候也说“制造商”,但是为了清晰我们这里统一都说“品牌”。
现在你已经知道了一些车的基本特点,你已经能根据这些特点构建一个对象了。
初始工程里有两个文件: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,
@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作为类方法是没有意义的,因为一个类根本没有用于转为为小写的字符串实例!
为你的类添加基本的方法
将以下方法添加到 @end
之上:
//Basic operation methods -(NSString *)goForward; -(NSString *)goBackward; -(NSString *)stopMoving; -(NSString *)changeGears:(NSString *)newGearName; -(NSString *)turn:(NSInteger)degrees; -(NSString *)makeNoise; |
头文件中声明
-(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; |
这些代码大部分都只是框架而已,待会儿你将实现这些方法的细节。 turn:
changeGears:
打开
#import "Vehicle.h"
|
这样你就能在你的代码中引用Vehicle
类了。
接下来,将application:didFinishLaunchingWithOp
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOp |
当vehicle实例被初始化以后,你可以调用它的每一个实例方法,看看它的日志输出。
编译并运行你的程序,你就能看到所有我们填充了数据的字符串都正常的返回了日志。但是对于那些没有设置过的特性(property),或者返回值为nil
的方法,你会看到返回的日志是(null),就像下面这样:
你将使用继承来为这些方法提供更特定的实现。
继承
继承这个概念和遗传非常像:孩子总是继承他们父母的特点。
然而,在像Objective-C这样的单一继承编程语言中,继承的概念要远比现实世界中遗传的概念要严格的多。”子“类总是继承自一个”父“类,或者说超类,而不是像现实中,你的特点实际上是你父母的特点的混合。
Vehicle
注意:
为了更实际的看到继承,创建一个Vehicle的子类
打开 @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
如果你需要更多的变量来描述汽车(car)该怎么办呢?除了轮子的数量以外, 汽车还有很多特殊的特点,它有几扇门呢?它的顶棚可以开关吗?它有遮阳棚吗?
当然,你可以很容易的添加这些新的特性!打开 @interface
@property (nonatomic, assign) BOOL isConvertible; @property (nonatomic, assign) BOOL isHatchback; @property (nonatomic, assign) BOOL hasSunroof; @property (nonatomic, assign) NSInteger numberOfDoors; |
重写方法
在你添加了这些新的特性之后,你还可以添加新的方法或者从父类中继承一些方法,并在子类中实现它们。
继承的意思是“拿一个父类中已经声明的方法,并为它创建你自己的实现”。 比如,当你创建一个 initWithNibName:bundle:
, viewDidLoad
, 和didReceiveMemoryWarning。
当你继承一个方法时,你可以做两件事:
- 调用
[super method]
方法以执行父类中的所有内容,或者 - 从零开始,为子类提供新的实现
你会发现在所有的UIViewController [super method]
然而,因为那些你将要继承的Car
打开
#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了。
构建用户界面
在
#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"; |
这个函数的作用就是简单的将数据初始化的工作分离出来以构建你的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
编译并运行你的程序,你会看到的将会和下图显示的一样:
你看到的这些数字可能会不一样,因为它们代表着内存地址,但是除了这以外,其它内容都应该是一样的。
好消息是这些对象已经被识别为Car tableView:cellForRowAtIndexPath:
中都做了什么事:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithI |
这里,你获取了一个 self.vehicles
description
textLabel
变量。
description
回到 @end
之上:
//Convenience method for UITableViewCells and UINavigationBar titles. -(NSString *)vehicleTitleString; |
接着, 在
#pragma mark - Convenience Methods -(NSString *)vehicleTitleString { return [NSString stringWithFormat:@"%d %@ %@", self.modelYear, self.brandName, self.modelName]; } |
上面这个方法用每个Vehicle
现在,更新VehicleListTableViewConttableView:cellForRowAtIndexPath:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithI |
编译并运行你的应用程序,现在它应该看上去更漂亮了:
然而,当你从列表中选择了一个
为什么会这样呢?
打开 IBOutlets
连接你的数据和视图
为了连接数据,更新 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]; |
编译并运行你的程序;从TableView 中单击一个对象,你将看到如下的详细视图:
模型-视图-控制器(MVC)封装逻辑
iOS 和很多其它现代编程语言都有一个设计模式叫做
MVC 背后的理念主要是,视图永远只关心如何呈现,模型永远只关心数据,控制器应该能在不需要了解二者太多的内部结构的前提下,很好的将二者嫁接起来。
使用MVC最大的好处就是,当你的数据模型变了,你只需要修改一次就够了。
新人最容易犯的错误就是在
为什么要在你的应用中实现MVC模型呢?设想如果你想往
//Car-specific details [basicDetailsString appendString:@"nnCar-Specific Details:nn"]; [basicDetailsString appendFormat:@"Number of doors: %d", self.detailVehicle.numberOfDoors]; |
但是你要注意,这样会有一个小问题:
VehicleDetailsViewContro
有很多方法可以解决这个问题。
一种最直观的方法就是导入Car.h文件, 那么
每次你发现你自己在做这种事的时候,你都应该问问你自己:“我的视图控制器是不是做了太多了呢?”
这种情况下,你的答案是肯定的。你可以利用继承的特性,用同一个方法来为不同的子类提供对应的字符串来显示相应的内容。
通过继承创建子类
首先,将下面的新方法添加到Vehicle.h:
//Convenience method to get the vehicle's details. -(NSString *)vehicleDetailsString; |
这是公开声明的方法,它可以被像 VehicleDetailsViewContro
的其它类调用。它们不需要知道每一个参数,相反,它们仅仅通过调用vehicleDetailsString
一个方法就可以获取完全格式化的字符串,然后使用它。
打开
-(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]; |
这个方法和你添加到 VehicleDetailViewControl
现在你可以继承父类vehicle 的基础字符串并为 vehicleDetailsString
:方法的实现:
- (NSString *)vehicleDetailsString { //Get basic details from superclass NSString *basicDetails = [super vehicleDetailsString]; |
汽车版本的这个函数首先调用了父类的相应方法以获取有关车的详细内容。接着它将和带有汽车特点的详细内容存入 carDetailsBuilder
现在将VehicleDetailViewControlconfigureView
- (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]; } } |
编译并运行你的程序;选择一辆车,除了看到一般信息以外,你还应该能看到带有汽车特点的信息,就像下面这样:
你的
这种方法的优势在你继续为
打开
因为有的摩托车会发出深沉的引擎噪音,而有的摩托车的引擎声音是高亮的,所以你创建的每一个
在 @interface
@property (nonatomic, strong) NSString *engineNoise; |
接着,打开 init
#pragma mark - Initialization - (id)init { if (self = [super init]) { self.numberOfWheels = 2; self.powerSource = @"gas engine"; } |
因为所有的摩托车都有两个轮子,并且都是汽油驱动的(在这个例子中,所有用电驱动的都被看作电动车,而不叫摩托车),你可以在初始化对象的时候设置它轮子的个数已经动力源。
接下来,添加下面的方法以覆盖父类中那些返回是 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
- (NSString *)vehicleDetailsString { //Get basic details from superclass NSString *basicDetails = [super vehicleDetailsString]; |
现在,是时候创建一些
打开
#import "Motorcycle.h"
|
接下来,找到 setupVehicleArray
//Create a motorcycle Motorcycle *harley = [[Motorcycle alloc] init]; harley.brandName = @"Harley-Davidson"; harley.modelName = @"Softail"; harley.modelYear = 1979; harley.engineNoise = @"Vrrrrrrrroooooooooom!"; |
上面的代码简单的初始化了两个摩托车对象,并将它们添加到车的数组中。
编译并运行你的应用程序;你将会在列表中看到你刚刚添加的
点击其中的一个,你将会被带到这个摩托车
无论是汽车还是摩托车(甚至是一个普通的老爷车),你都可以调用 vehicleDetailsString
适当的分离模型,视图和控制器,并运用继承,你就能够为一个父类的不同子类显示数据,而避免了为不同的子类撰写大量额外的代码。代码越少==程序员越开心:]
提供模型类中的逻辑
运用这种方法,你还可以将更多的更复杂的逻辑包装在模型类里面。想想
进入
添加如下整型变量到Truck.h
@property (nonatomic, assign) NSInteger cargoCapacityCubicFeet; |
因为卡车的类型太多了,所以你也不需要创建初始化方法以自动提供所有的详情。你可以只是简单的重写父类中那些对于任何类型的卡车都适用的方法。
打开
#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"]]; } |
接着,你需要重写一些方法,以便它能根据货车拉货量的多少返回不同的字符串。大的卡车在倒车的时候需要发出警报声,所以你可以为此创建一个私有函数(一个不声明在
将如下帮助代码添加到
#pragma mark - Private methods - (NSString *)soundBackupAlarm { return @"Beep! Beep! Beep! Beep!"; } |
然后回到刚刚重写的那个方法中,现在你可以在 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"]]; } |
不同的卡车喇叭也不同;比如小型的卡车喇叭声和汽车的喇叭声很像,然而越大的卡车就会拥有更大的喇叭声。为了解决这种情况,你只需要在 makeNoise
像下面这样添加
- (NSString *)makeNoise { if (self.numberOfWheels <= 4) { return @"Beep beep!"; } else if (self.numberOfWheels > 4 && self.numberOfWheels <= 8) { return @"Honk!"; } else { return @"HOOOOOOOOONK!"; } } |
最后,你可以重写 vehicleDetailsString
-(NSString *)vehicleDetailsString { //Get basic details from superclass NSString *basicDetails = [super vehicleDetailsString]; |
现在你的
#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"; |
这将会在汽车和摩托车所在的数组中添加一些带有卡车特点的
编译并运行程序;点击卡车其中的一个,确保你能够看到带有卡车特点的详情,就像下面显示的这样:
看起来很棒!这些卡车信息的得来要归功于 vehicleDetailsString
刘一道《编写高质量代码:改善Objective-C程序的156个建议》
iOS 技术交流群:526307135
iOS 技术BLOG: Blog.sina.com.cn/Beijingwolf