协议与分类
文章目录
通过委托与数据源协议进行对象间通信
对象之间经常需要互相通信,实现通信的方式有很多种,OC中广泛使用的是委托模式这种开发方式。
该模式的主旨是:定义一套接口,某对象如果想接受另一个对象的委托,则需要遵循这个接口,以便成为委托对象,这里主要有两个部分的内容,一个是处理数据的内容被称作数据源,另一个事件处理对象则叫做委托。
这里我们看一个例子:
我们呢获取网络数据的类有一个委托对象,在获取完数据之后,他会回调这个委托对象
上图就展示了这一个流程。
这里给出一段代码展示下相关的流程
@protocol FetchDataDelegate <NSObject>
- (void)FetchData:(NetWorkFetch*)fetcher didRecevice:(NSData*)data;
@end
@interface NetWorkFetcher : NSObject
@property (nonatomic, weak) id<FetchDataDelegate> delegate;
@end
这里要注意,这个属性要定义成weak,而不是strong,因为两者之间不能互相持有,否则就会出现一个循环引用的问题。
实现委托对象的方法是声明某个类遵从委托协议,而如果这个协议是一个委托协议的话,一般情况下只用在类的内部实现,所以都在exstion中声明的。
委托协议中的方法一般都设置成可选。可选通过@optional
来实现
@protocol FetchDataDelegate <NSObject>
@optional
- (void)FetchData:(NetWorkFetcher*)fetcher didRecevice:(NSData*)data;
@end
这里注意如果设置成可选注意要调用respondsToSelector
这个方法来查询类型信息方法看他能不能响应这个方法。
在调用这个fetchData
的方法的时候,可以根据fetch的不同来给他提供不同的解决方案,我们可以通过不同的实例来处理不同的一个情况。这就是委托模式的意义,把一个行为的责任推给另一个类。
也可以用协议定义一个接口,令某类由该接口获得对应的一个数据。这叫做数据源模式
比方说UITableVIew就是这么设计的,我们一般通过同一个对象来处理信息源和委托模式。
如果我们大量的采用这种模式,就会经常性的出现这种代码:``respondsToSelector`来判断是否符合对应的一个条件,但是实际上并不需要这么多的一个判断,我们可以采用一个bool值作为某一个方法是否存在的判断,这样就可以节约很多时间。
这里通过一个位段来实现这部分内容:
@interface Person () <FetchDataDelegate> {
struct data {
unsigned int fieldA : 8;
unsigned int fieldB : 4;
unsigned int fieldC : 2;
unsigned int fieldD : 1;
} _delegateFlags;
}//设置一个结构体来处理不同方法的bool值
如果这个方法在可以被响应,那就修改对应的位段的bool值为1,这样就可以实现一个优化。
小结
- 委托模式为对象提供了一个接口,可以吧这个事件告知其他对象
- 委托对象应该支持的接口定义成协议,把处理事件定义成方法
- 一个对象需要从另一个对象中获取数据的时候,可以使用委托模式
- 若有必要可以是实现一个尾段的结构体,来缓存能否响应的内容,
将类的实现代码分散到便于管理的多个分类中
我们可以根据类的不同功能将不同的代码分散到便于管理的多个类中
就像这下面的代码:
#import <Foundation/Foundation.h›
@interface EOCPerson : NSObject
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSArray *friends;
- (id) initWithFirstName: (NSString*)firstName
andLastName: (NSString*) lastName;
/* Friendship methods */
- (void) addFriend: (EOCPerson*) person;
- (void) removeFriend: (EOCPerson*) person;
- (BOOL) isFriendsWith: (EOCPerson*) person;
/* Work methods * /
- (void) performDaysWork;
- (void) takeVacationFromWork;
/* Play methods */
- (void) goToTheCinema;
- (void) goToSportsGame;
@end
我们可以把他分配到不同的分类中:
#import <Foundation/Foundation.h>
@interface EOCPerson : NSObject
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSArray *friends;
- (id) initWithFirstName: (NSString*) firstName
andLastName: (NSString*) lastName;
@end
@interface EOCPerson (Friendship)
- (void) addFriend: (EOCPerson*) person;
- (void) removeFriend: (EOCPerson*) person;
- (BOOL) isFriendsWith: (EOCPerson*) person;
@end
@interface EOCPerson (Work)
- (void) performDaysWork;
- (void) takeVacationFromWork;
@end
@interface EOCPerson (Play)
- (void) goToTheCinema;
- (void) goToSportsGame;
@end
这样把他放在不同的分类很大程度上的将它分成了便于管理的各个分类,同时这种方式还利于调试,因为这里的调试可以显示他所处的分类,可以方便我们去查找方法。
小结
- 使用分类机制把类的实现代码划分成易于管理的小块
- 将应该视作私有的方法归入Private的分类中,隐藏实现细节
为第三方类的分类名称提供前缀
分类机制通常用于向无源码的既有类中新增功能,这就相当于给一个类提供一个固有方法,但是这种东西可能会造成一个覆盖主实现的问题,最好解决这个问题的方法就是给类添加一个前缀,防止出现一个命名空间的重复。
- 向第三方类添加分类的时候,总应该给名称加上专属的前缀
- 给分类中的名称也加上自己的一个前缀
不在分类中声明属性
我们之前就接触过分类中不可以声明属性,如果他在分类中声明属性会出现警告:
这里其实要求我们自己在运行期提供一个属性的一个存取方法,这里只能通过关联对象来实现一个合成实例变量的操作,但是这实际上并不能实现一些简单的代码。
主要产生问题的原因,还是尽管修改了属性的内存管理语义,还需要在设置方法中修改关联对象的内存管理语义,这样实际上很容易产生一些问题。
但是其实是可以声明只读变量的属性,因为获取方法并不访问数据,也不需要由实例变量来实现,可以这样实现:
@interface NSString (Associated)
@property (nonatomic, copy, readonly) NSArray* ary;
- (NSString*)eocString;
@end
- (NSArray *)ary {
return @[@13, @43];
}
//
NSString* str = @"123";
NSLog(@"%@", str.ary);
//输出结果:
(
13,
43
)
Type: Notice | Timestamp: 2025-01-23 21:38:59.187293+08:00 | Process: fragment | Library: fragment | TID: 0x226da1e
但实际上这种做法很没有意义,我们实际上只需要声明一个方法就可以实现这样的效果,所以这种情况我们应该声明一个方法来获取对应的数据
小结
- 把分装数据所用全部属性定义在接口中
- 分类中可以定义存取方法,但是尽量不要定义属性
使用拓展隐藏实现细节
在拓展中定义实例变量中可以实现一个隐藏的效果,同时隐藏实现细节,首先看下面这段代码,这段代码是设置@private
也是可以在头文件中被别人看到的,但是如果声明到拓展中,就可以实现一个隐藏实现细节的效果。
#import <Foundation/Foundation.h>
@class EOCSuperSecretClass;
@interface EOCClass : NSObject {
@private
EOCSuperSecretClass *_secretInstance;
}
@end
但是把他放在拓展中就是这样的一个实现:
// EOCClass.h
#import ‹Foundation/Foundation.h>
@interface EOCClass : NSObject
@end
// EOCClass.m
#import "EOCClass.h"
#import "EOCSuperSecretClass.h"
@interface EOCClass () {
EOCSuperSecretClass *
_secretInstance;
}
@end
@implementation EOCClass
// Methods here
@end
第二个好处就是编写OC和C++的混代码的时候,可以通过这种拓展的方式来解决,让代码更加合适。
这里引用一段书中的话:
该类的实现文件可能叫做EOCClass.mm,其中.mm 扩展名表示编译器应该将此文件按 Objective-C++ 来编译,否则,就无法正确引入 SomeCppClass.h 了。然而请注意,名为SomeCppClass 的这个C++类必须完全引人,因为编译器要完整地解析其定义方能得知_cppClass实例变量的大小。于是,只要是包含EOCClass.h的类,都必须编译为Objective-C++ 才行,因为它们都引入了 SomeCppClass 类的头文件。这很快就会失控,最终导致整个应用程序全部都要编译 Objective-C++。这么做确实完全可行,不过笔者觉得相当别扭,尤其是将代码发布程序库供其他应用程序使用时,更不应该如此。要求第三方开发者将其源文件扩展名均改力.mm 不是很合适。
这里有一段代码:
#import <Foundation/Foundation.h>
class SomeCppClass;
@interface EOCClass : NSObject {
@private
SomeCppClass * _CppClass;
}
@end
可以把他改写成拓展的方式获得这个实例变量,这样可以让接口更加干净
// EOCClass.h
#import <Foundation/Foundation.h>
@interface EOCClass : NSObject
@end
// EOCClass.mm
#import "EOCClass.h"
#include "SomeCppClass.h"
@interface EOCClass () {
SomeCppClass_cppClass;
}
@end
@implementation EOCClass
@end
这样对外展示的是一套整洁的OC接口,会让代码更加清晰。
还有前面讲到的一个重新声明对象的读写性,这里就不多赘述了。
最后一种用法就是,如果某个对象所遵从额度协议只应该被视作私有的时候,可以在分类中声明,
@interface Person : NSObject <NSCopying>
@property (nonatomic, copy, readonly) NSString* name;
@property (nonatomic, copy, readonly) NSString* firstName;
@property (nonatomic, copy, readonly) NSSet* firend;
-(id) initWithName:(NSString*)firstName lastName:(NSString*)name;
@end
小结
- 在拓展中给类添加实例变量
- 在分类中重新声明属性课读写,让外部不可以读写,内部可以读写
- 私有方法的原型声明在拓展中
- 若想让类所遵循的协议不为人知,可以在拓展中声明
通过协议提供匿名对象
在OC中匿名对象表达的意思是下面这段代码中的内容:
@property (nonatomic, weak) id<ViewProtocol> delegate;
在这里任何类的对象都可以充当这一属性,这个类只要符合对应的一个协议就可以执行这个协议里的内容。
我们处理数据库的思路也是这样,如果没有办法让处理数据库的类全部继承于同一个基类,那么就得返回id类型的动心了,不过我们可以把所有数据库连接都具备的方法放到协议中,让返回的对象遵循这个协议。
@protocol EOCDatabaseConnection
- (void) connect;
- (void) disconnect;
- (BOOL) isConnected;
- (NSArray*) performQuery: (NSString*) query;
@end
#import ‹Foundation/Foundation.h>
@protocol EOCDatabaseConnection;
@interface EOCDatabaseManager : NSObject
+ (id) sharedInstance;
- (id<EOCDatabaseConnection>) connectionWithIdentifier:
(NSString*) identifier;
@end
这里使用这个API返回的对象只要可以满足连接断开查询数据库三个内容就可以作为一个对象了。这样在开发的后续版本,不需要改变公共API,就可以修改后段的一个实现类。
小结
- 协议可以在某种程度上提供匿名对象,具体对象类型可以淡化成遵从某协议的id类型,协议里规定对象所实现的方法。
- 匿名对象来隐藏类型明
- 如果具体类型不重要,重要的是对象能够响应特定方法,那么可以使用匿名对象来表示。