编写高质量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类型,协议里规定了对象所应实现的方法。
  • 使用匿名对象来隐藏类型名称(或类名)
  • 如果具体类型不重要,重要的是对象能够相应(定义在协议里的)特定方法,那么可以使用匿名对象来表示。

推荐阅读更多精彩内容