第十八条 尽量使用不可变对象

2017/8/26 posted in  第三章 接口与API设计

我们在设计类的时候,运用属性来封装数据。并且使用属性时候,可以将属性直接设置为“只读”(read-only)。默认情况下是“可读也可写”的。

不过,一般情况下我们要建模的数据未必需要改变。比方说,某数据所表示的对象源自一项只读的网络服务(web service),里面可能包含一系列需要显示在地图上的相关点,像这种对象就没必要改变其内容。即使修改了,新数据也不会推送回服务器.

而且如果把可变对象(mutable object)放入collection之后又修改其内容,那么很容易就会破坏set的内部数据结构,使其失去固有的语义。

我们来举例:

为了将EOCPointOflnterest做成不可变的类,需要把所有属性都声明为readonly:

#import <Foundation/Foundation.h>
@interface EOCPointOfInterest : NSObject
@property (nonatomic, copy, readonly) NSString *identifier; 
@property (nonatomic, copy, readonly) NSString* title;
@property (nonatomic, assign, readonly) float latitude; 
@property (nonatomic, assign, readonly) float longitude;
-(id) initWithldentifier: (NSString*) identifier 
                   title:(NSString*)title 
                   latitude: (float) latitude 
                   longitude: (float) longitude;
@end

这样后如果有人想要改变属性值,那么编译时就会报错。对象中的属性值可以读出,但是无法写入,这就能保证EOCPointOfluterest中的各个数据之间总是相互协调的。

但是我们有时想要修改封装在对象内部的数据,不想令这些数据为外人所改动。这种情况下,通常做法是在对象内部将readonly属性重新声明为readwrite。当然,如果该属性是nonatomic 的,那么这样做可能会产生“竞争条件”(racecondition)。在对象内部写人某属性时,对象外的 观察者也许正读取该属性。若想避免此问题,我们可以在必要时通过“派发队列"(dispatchqueue)等手段,将(包括对象内部的)所有数据存取操作都设为同步操作将属性在对象内部重新声明为readwrite这一操作可于“class-continuation分类”中完成,在公共接口中声明的属性可于此处重新声明,属性的其他特质必须保持不变,而readonly可扩展为readwrite

“class-continuation分类”可以这样写:

// .m文件中
#import "EOCPointOfInterest.h"

@interface EOCPointOfInterest : NSObject
@property (nonatomic, copy, readwrite) NSString *identifier; 
@property (nonatomic, copy, readwrite) NSString* title;
@property (nonatomic, assign, readwrite) float latitude; 
@property (nonatomic, assign, readwrite) float longitude;

@implementation EOCPointOfInterest

/* ... */

@end

现在,只能于EOCPoimOflnterest实现代码内部设置这些属性值了。但是我们其实可以同构KVC键值编码来设置这些属性值。不过,这样显然违背了我们的本意,绕过了提供的API。不推荐这种做法。

我们定义类公共的API时,要注意一件事情:对象里表示各种collection的那些属性究竞应该设成可变的,还是不可变的。例如,我们用某个类来表示个人信息,该类里还存放了一些引用,指向此人的诸位朋友。你可能想把这个人的全部朋友都放在一个“列表"(list)里,并将其做成属性。假如开发者可以添加或删除此人的朋友,那么这个属性就需要用可变的set来实现。在这种情况下,通常应该提供一个readonly属性供外界使用,该属性将返回不可变的set, 而此set则是内部那个可变set的一份拷贝。比方说,下面这段代码就能够实现出这样一个类:

// EOCPerson.h
#import <Foundation/Foundation•h>
@interface EOCPerson : NSObject
@property (nonatomic, copy, readonly) NSString *firstName; 
@property (nonatomic, copy, readonly) NSString *lastName; 
@property (nonatomic, strong, readonly) NSSet *friends;
-(id)initWithFirstName:(NSString*)firstName
          andLastName:(NSString*)lastName;
-(void)addFriend:(EOCPerson*)person;
-(void)removeFriend:(EOCPerson*)person;

@end

// EOCPerson.m #import "EOCPerson•!!
@interface EOCPerson ()
@property (nonatomic, copy, readwrite) NSString *firstName; 
@property (nonatomic, copy, readwrite) NSString *lastName;
@end
@implementation EOCPerson {
    NSMutableSet *_internalFriends;
}

-(NSSet*)friends {
    return [_internalFriends copy];
}
-(void)addFriend:(EOCPerson*)person {
    [_internalFriends addObject:person];
} 

-(void)removeFriend:(EOCPerson*)person {
    [_internalFriends removeObjectrperson];
}

-(id)initWithFirstName: (NSString*)firstName andLastName:(NSString*)lastName { 
    if ((self = [super init】)){
         _firstName = firstName;
         _lastName = lastName;
         _internalFriends = [NSMutableSet new];
    }
    return self;
}

@end

要点

  • 尽量创建不可变的对象。
  • 若某属性仅可于对象内部修改,则在“class-continuation分类”中将其由readonly属性扩展为readwrite属性
  • 不要把可变的collection作为属性公开,而应提供相关方法,以此修改对象中的可变collection