欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

编写高质量iOS与OSX代码的52个有效方法-第四章:协议与封装

程序员文章站 2022-07-14 20:53:39
...

协议(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、为第三方类的分类名称加前缀

分类机制通常用于向无源码的既有类中新增功能。

分类的方法是直接加到类中的,就好比是类中固有的方法,将分类方法加入类中这一操作时在运行期系统加载分类时完成的。运行期系统会把分类中所实现的每个方法都加入类的方法列表中。

如果类中本来就有这个方法,分类中又实现了一次,那么分类中方法会覆盖原来的实现代码。有可能会发生多次覆盖。

如果多个分类名称相同,在运行期,是不会报错的。但是加载的分类有可能不是你所期望的。

如果有相同的方法,那么运行时调用的不一定是你想要的方法。

运行期不会报错,但是在实现结果的时候,就会出现未知错误。

编写高质量iOS与OSX代码的52个有效方法-第四章:协议与封装
比如实现了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类型,协议里规定了对象所应实现的方法。
  • 使用匿名对象来隐藏类型名称(或类名)
  • 如果具体类型不重要,重要的是对象能够相应(定义在协议里的)特定方法,那么可以使用匿名对象来表示。
相关标签: 协议 iOS开发