编写高质量iOS与OSX代码的52个有效方法-第四章:协议与封装
协议(protocol)与java的接口类似。CO不支持多重继承,因而吧某个类应该实现的一系列方法定义在协议里。协议最常见的用途是事先委托模式,也有其他用法。
分类(Category)是OC一项重要语言特性。利用分类机制,无需继承子类就可以直接为当前类添加方法。由于OC运行期系统是高度动态的,所以才能支持这一特性,也有一些坑。
23、通过委托与数据源协议进行对象间通信
委托模式(Delegate pattern):定义一套接口,某对象若想接受另一个对象的委托,则需遵从次接口,以便成为其委托对象(delegate)。而这“另一个对象”可以给其委托对象回传一些消息,也可以在发生相关事件时通知委托对象。
此模式可将数据与业务逻辑解耦。
视图对象中可以包含负责数据与事件处理的对象,这两种对象分别称为数据源(data source)与委托(delegate)。
代理方法:
#import <Foundation/Foundation.h>
@class ZYDNetworkFetcher;
@protocol ZYDNetrokFetcherDelegate <NSObject>
// 一般默认方法是必选的
- (void)networkFetcherRequestFinished:(ZYDNetworkFetcher *)fetcher;
// 委托协议中的方法一般是可选的(optional)
@optional
- (void)networkFetcher:(ZYDNetworkFetcher *)fetcher didReceivdData:(NSData *)data;
- (void)networdFetcher:(ZYDNetworkFetcher *)fetcher didFailWithError:(NSError *)error;
@end
发起委托
.h
#import <Foundation/Foundation.h>
#import "ZYDNetrokFetcherDelegate.h"
@interface ZYDNetworkFetcher : NSObject
//用weak而不是strong,两者之间必须为非拥有关系。
//想要使用ZYDNetworkFetcher对象的那个而对象会持有本对象,知道用完本对象之后,才会释放。
//如果声明属性的时候用strong将本对象与委托对象之间定为拥有关系,那么就会引入保留换(retain cycle)
//所以,本类中存放委托对象的这个属性要么定义weak 要么定义unsafe_unretained。
//在相关对象销毁时自动清空,使用weak,不需要自动清空,使用后者。
@property (nonatomic,weak) id <ZYDNetrokFetcherDelegate> delegate;
@end
.m
#import "ZYDNetworkFetcher.h"
@implementation ZYDNetworkFetcher
- (void)requestFinishedWithData:(NSData *)data {
if ([_delegate respondsToSelector:@selector(networkFetcher:didReceivdData:)]) {
[_delegate networkFetcher:self didReceivdData:data];
}
}
- (void)reqeustFailedWithError:(NSError *)error {
if ([_delegate respondsToSelector:@selector(networdFetcher:didFailWithError:)]) {
[_delegate networdFetcher:self didFailWithError:error];
}
}
@end
委托对象
#import "ZYDDataModel.h"
#import "ZYDNetrokFetcherDelegate.h"
// 实现委托对象的办法,是声明遵从委托协议
// 然后把协议中想实现的方法在类里实现出来
@interface ZYDDataModel () <ZYDNetrokFetcherDelegate>
@end
@implementation ZYDDataModel
#pragma mark -- 实现协议方法
- (void)networkFetcherRequestFinished:(ZYDNetworkFetcher *)fetcher {
}
- (void)networkFetcher:(ZYDNetworkFetcher *)fetcher didReceivdData:(NSData *)data {
}
- (void)networdFetcher:(ZYDNetworkFetcher *)fetcher didFailWithError:(NSError *)error {
}
@end
数据源模式:信息从数据源流向类。
常规的委托模式,信息则从类流向受委托者。
一个类中相关方法要调用很多次的时候,需要反复调用判断逻辑[_delegate respondsToSelector:@selector(networdFetcher:didFailWithError:)]
。可以使用结构体,将结果标记缓存下来,只判断标记,不在调用判断语句。
#import "ZYDNetworkFetcher.h"
@interface ZYDNetworkFetcher () {
struct {
unsigned int didReceiveData: 1;
unsigned int didFailWIthError : 1;
} _delegateFlags;
}
@end
@implementation ZYDNetworkFetcher
- (void)setDelegate:(id<ZYDNetrokFetcherDelegate>)delegate {
_delegate = delegate;
_delegateFlags.didReceiveData = [_delegate respondsToSelector:@selector(networkFetcher:didReceivdData:)];
_delegateFlags.didFailWIthError = [_delegate respondsToSelector:@selector(networdFetcher:didFailWithError:)];
}
- (void)requestFinishedWithData:(NSData *)data {
if (_delegateFlags.didReceiveData) {
[_delegate networkFetcher:self didReceivdData:data];
}
}
- (void)reqeustFailedWithError:(NSError *)error {
if (_delegateFlags.didFailWIthError) {
[_delegate networdFetcher:self didFailWithError:error];
}
}
@end
- 委托模式为对象提供了一套接口,使其可由此将相关事件告知其他对象。
- 将委托对象应该支持的接口定义成协议,在协议中把可能需要处理的时间定义成方法。
- 当某对象需要从另外一个对象中获取数据时,可以使用委托模式,这种情况下亦称为数据源协议。
- 如果有必要,可实现含有位段的结构体,将委托对象是否能响应相关协议方法这一信息缓存至其中。
24、将类的实现代码分散到便于管理的数个分类之中
OC分类(类别/Category)机制,把类代码按逻辑划入几个分区中,这对开发与调试都有好处。
将代码按照方法分成好几个部分,类的基本要输都声明在主实现文件中,执行不同类型的操作作用的另外几套方法归入到各个分类中。
- 使用分类机制以后,依然可以把整个类都定义在一个接口文件中,并将其代码写在一个实现文件里。
.h
#import <Foundation/Foundation.h>
@interface ZYDPersonModel : NSObject
@property (nonatomic,copy,readonly) NSString *firstName;
@property (nonatomic,copy,readonly) NSString *lastName;
@property (nonatomic,strong,readonly) NSArray *friends;
- (instancetype)initWithFristName:(NSString *)firstName lastName:(NSString *)lastName;
@end
@interface ZYDPersonModel (FriendShip)
- (void)addFried:(ZYDPersonModel *)person;
- (void)removeFriend:(ZYDPersonModel *)person;
- (BOOL)isFriendWith:(ZYDPersonModel *)person;
@end
@interface ZYDPersonModel (Wrok)
- (void)performDaysWork;
@end
.m
#import "ZYDPersonModel.h"
@interface ZYDPersonModel ()
@property (nonatomic,readwrite) NSMutableArray *internalFriends;
@end
@implementation ZYDPersonModel
- (instancetype)initWithFristName:(NSString *)firstName lastName:(NSString *)lastName {
if (self = [super init]) {
_firstName = [firstName copy];
_lastName = [lastName copy];
_internalFriends = [NSMutableArray array];
}
return self;
}
- (NSString *)description {
return [NSString stringWithFormat:@"name :%@ ~ %@ \n frends: %@",_firstName,_lastName,_internalFriends];
}
- (NSArray *)friends {
return [_internalFriends copy];
}
@end
@implementation ZYDPersonModel(FriendShip)
- (void)addFried:(ZYDPersonModel *)person {
[_internalFriends addObject:person];
}
- (void)removeFriend:(ZYDPersonModel *)person {
if ([_internalFriends indexOfObject:person] != NSNotFound) {
[_internalFriends removeObject:person];
}
}
- (BOOL)isFriendWith:(ZYDPersonModel *)person {
if ([_internalFriends indexOfObject:person] != NSNotFound) {
return YES;
}
return NO;
}
@end
@implementation ZYDPersonModel (Wrok)
- (void)performDaysWork {
NSLog(@"%@ %@ work on Monday",_firstName,_lastName);
}
@end
- 随着分类数量增加,可以各个分类提取到各自的文件中。
#import "ZYDPersonModel.h"
@interface ZYDPersonModel (Play)
- (void)goToThePark;
@end
#import "ZYDPersonModel+Play.h"
@implementation ZYDPersonModel (Play)
- (void)goToThePark {
NSLog(@"%@ %@ go to the Park",self.firstName,self.lastName);
}
@end
两种方式的区别在于,分类在一个接口文件中,只需要引入主头文件即可#import "ZYDPersonModel.h"
。如果单独提取到各自的文件中,使用时需要单独引入头文件#import "ZYDPersonModel+Play.h"
#import "ViewController.h"
#import "ZYDPersonModel.h"
#import "ZYDPersonModel+Play.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
ZYDPersonModel *personOne = [[ZYDPersonModel alloc] initWithFristName:@"Smith" lastName:@"Json"];
ZYDPersonModel *personTwo = [[ZYDPersonModel alloc] initWithFristName:@"Wellienm" lastName:@"Jone"];
[personOne addFried:personTwo];
NSLog(@"%@",personOne);
[personOne performDaysWork];
[personOne goToThePark];
}
@end
通过分类机制,可以把类代码分成很多个易于管理的小块。还有一个好处,将代码分割成几块,把相应代码归入不同的功能区(functional area)。
再者,将类代码打散到分类,便于调试。
另外,可以创建名为Private的分离,把一些应该视为私有方法,只会在类或框架内部使用,无需对外公开的方法收入其中。
在编写准备分享给其他开发者使用的程序库时,可以考虑创建Private分类。如果程序库需要用到这些方法,就引入此分类的头文件,而分类的头文件并不随程序库一并公开。
- 使用分类机制把类的实现代码划分成易于管理的小块。
- 将应该视为私有的方法归入名叫Private的分类中,以隐藏实现细节。
25、为第三方类的分类名称加前缀
分类机制通常用于向无源码的既有类中新增功能。
分类的方法是直接加到类中的,就好比是类中固有的方法,将分类方法加入类中这一操作时在运行期系统加载分类时完成的。运行期系统会把分类中所实现的每个方法都加入类的方法列表中。
如果类中本来就有这个方法,分类中又实现了一次,那么分类中方法会覆盖原来的实现代码。有可能会发生多次覆盖。
如果多个分类名称相同,在运行期,是不会报错的。但是加载的分类有可能不是你所期望的。
如果有相同的方法,那么运行时调用的不一定是你想要的方法。
运行期不会报错,但是在实现结果的时候,就会出现未知错误。
比如实现了NSString的两个分类,同时都有一个分类方法”- (NSString *)urlEncordedString;”那么在调用过程中,就不知道是调用哪个分类中实现的方法。同样能够正常编译通过。
NSString *urlString = @"http://www.baidu.com";
NSLog(@"%@",[urlString urlEncordedString]);
因为不会报错,这种问题比较难发现,所以在写之前就避免这种情况就显得非常重要。
当然如果分类名相同,但是方法名不同时,有可能出现的问题是:No visible @interface for 'NSString' declares the selector 'seconString'
,你无法调用自己实现的方法。系统只是提供最后加载到的分类,如此而已。
- 向第三方库添加分类时,给分类名称加上专用前缀。
- 向第三方库添加分类时,给分类方法名加上专用前缀。
26、 勿在分类中声明属性
属性是封装数据的形式。尽管从技术上说,分类里也可以声明属性,但这种做法还是要尽量避免。原因在于,处理实现文件之外,其他分类无法向类中新增实例变量。因此,他们无法将实现属性所需的实例变量合成出来。
分类,应将其理解为一种手段,目标在于扩展类的功能,而非封装数据。
- 把封装数据所用的全部属性都定义在主接口里。
- 在实现文件之外的其他分类中,可以定义存取方法,但尽量不要定义属性。
27、使用class-continuation分类隐藏实现细节
class-continuation分类,和普通分类不同,他没有名字,它在其所续接的那个类的实现文件里。重要之处在于,这是唯一能声明实例变量的分类,而且此分类没有特定的实现文件,其中的实现方法都应该定义在类的实现文件里。
@interface ZYDPersonModel ()
@end
在class-continuation分类中给类新增实例变量,或者在实现块增加,可以将其隐藏起来,只供本类使用。
引入C++文件,一般用class-continuation分类解决。
class-continuation还有一种合理用法,将public接口中声明为只读的属性扩展为可读写,以便在类的内部设置其值。通常不直接访问实例变量,而是通过设置访问方法来做,应为这样能够触发键值观察。
出现在class-continuation分类或其他分类中的属性必须同类接口里的属性剧痛相同的特质(attribute),不过只读状态可以扩充为可读写。
只会在累的实现代码中用到的私有方法也可以声明在class-continuation分类中。
若对象所遵从的协议只应视为私有,则可杂class-continuation中声明。
@interface ZYDDataModel () <ZYDNetrokFetcherDelegate>
@end
- 通过class-continuation分类向类中新增实例变量。
- 如果某属性在主接口中声明为只读,而类的内部又要用设置方法修改此属性,那么就在class-continuation分类中将其扩展为可读写。
- 把私有方法的原型声明在class-continuation分类里面。
- 若想使类所遵循的协议不为人所知,则可于class-continuation分类中声明。
28、通过协议提供匿名对象
匿名对象(anonymous object),用协议把自己所写的API之中的实心细节隐藏起来,将返回的对象设计为遵从此协议的纯id类型。这样,想要隐藏的类名就不会出现在API之中。
若是北邮有多个不同的实现类,不想指明具体使用哪个类,可以考虑用这个方法,这些类有可能会变,有时候他们又无法容纳于标准的类集成体系中,因而不能以某一个公共基类来统一表示。
例如,在定义受委托者属性时:@property (nonatomic,weak) id <ZYDNetrokFetcherDelegate> delegate;
任何类的对象都能充当代理属性,即便不继承自NSObject也可以,只要遵循ZYDNetrokFetcherDelegate
协议就行。对于具备此属性的类来说,delegate就是匿名的(anonymous)。
- 例如,实现数据库链接
// 通过代理实现方法
#import <Foundation/Foundation.h>
@protocol ZYDDatabaseConnection <NSObject>
- (void)connect;
- (void)disConnect;
- (BOOL)isConnected;
- (NSSarray *)perforQuery:(NSString *)query;
@end
通过管理器提供数据库链接。
#import <Foundation/Foundation.h>
@protocol ZYDDatabaseConnection;
@interface ZYDDatabaseManager : NSObject
+ (id)sharedInstance;
//通过 此返回对象,实现数据库链接、断开、查询方法,
// 对象类型并不重要,重要的是有没有实现方法。--这种情况下也可以用这些匿名类型(anonymous type)
- (id <ZYDDatabaseConnection>)connectionWithIdentifier:(NSString *)identifier;
@end
可以创建匿名对象将不同的三方类库包裹一下,是匿名对象成为其子类,并遵从ZYDDatabaseConnection
协议。然后用connectionWithIdentifier:
返回这些类对象。后续版本,无须改变公共API,即可切换后端的实现类。
(待完善)
- 协议可以在某种程度上提供匿名类型,具体的对象类型可以淡化成遵从某协议的id类型,协议里规定了对象所应实现的方法。
- 使用匿名对象来隐藏类型名称(或类名)
- 如果具体类型不重要,重要的是对象能够相应(定义在协议里的)特定方法,那么可以使用匿名对象来表示。