iOS 创建对象的姿势

在写 iOS 代码的时候,怎么样去 new 一个新对象出来,都有一些讲究在里面。使用不同的姿势去创建对象,对后期维护所造成的影响会存在细微的差别。

init 创建

在之前一篇分析 iOS 代码耦合的文章中,提到过当我们给一个对象的 property 赋值的时候,通过 init 方法传入参数来初始化 property 会让我们的代码更可靠。

有些人在定义带 property 的 class 的时候,会这样定义:

@interface User : NSObject
@property (nonatomic, strong) NSNumber*                 userID;
@end

使用的时候如下:

User* user = [[User alloc] init];
user.userID = @1000;

尤其是在定义 model 的时候,很容易写出这种,先 init,而后挨个给 property 赋值的代码。这种代码的问题在于 property 对于外部是可写的,property 处于随时可能变化的状态。之前不少篇文章中都强调过 immutable 的重要性,同样对于一个 class,我们也应该优先考虑设计成 immutable 的。

initWith 创建

如果将 property 都设置成 readonly 的,或者不暴露 property,property 的赋值都通过 initWith 的方式来初始化,就可以得到一个具备 immutable 的 class 定义了,具体到上面的例子代码如下:

//User.h
@interface User : NSObject
@property (nonatomic, strong, readonly) NSNumber*   userID;
- (instancetype)initWithUserID:(NSNumber*)uid;
@end
  
//User.m
@implementation User
- (instancetype)initWithUserID:(NSNumber*)uid {
    self = [super init];
    if (!self) {
        return nil;
    }
    _userID = uid;
    return self;
}
@end

userID 在 .h 文件当中是 readonly 的,userID 只有一次被赋值的机会,即在 User 的 initWith 方法中。这种方式的好处是一旦 User 对象创建完毕之后,就处于 immutable 的状态,property 都是不可修改的,安全可靠。

Designated initializer

Apple 为了方便开发者使用 init 方法,引入了一种名为 designated initializer 的 pattern。主要用来管理当一个 class 拥有多个 property 需要赋值的场景。比如上面我们的 User 类:

@interface User : NSObject
@property (nonatomic, strong, readonly) NSNumber*                 userID;
@property (nonatomic, strong, readonly) NSString*                 userName;
@property (nonatomic, strong, readonly) NSString*                 signature;
@end

有些场景需要初始化 userID 和 userName,而有些场景只需要初始化 userID 和 signature,所以我们需要提供多个 initWith 方法给不同的场景使用。为了管理 initWith 方法,Apple 将 init 方法分为两种类型:designated initializer 和 convenience initializer (又叫 secondary initializer) 。

designated initializer 只有一个,它会为 class 当中每个 property 都提供一个初始值,是最完整的 initWith 方法。convenience initializer 则可以有很多个,它可以选择只初始化部分的 property。convenience initializer 最后到会调用到 designated initializer,所以 designated initializer 也可以叫做 final initializer。

无论我们定义何种类型的 class,给 class 中的每个 property 都赋予一个初始值是个很好的习惯,可以避免掉一些意外的 bug 产生,这也是 designated initializer 的重要职责。

在实际的项目当中,一个 class 的 property 数目可能会随着业务的增长而增加,最后的结果就是会生成越来越多的 convenience initializer。上述的 User 类,如果是 3 个 property,极端的情况下最多可以有 7 个 init 方法。Peak君在阅读代码的时候,也确实看到过有些 class 定义了一连串整整齐齐摆放的 init 方法,代码虽然看着规范,但显得啰嗦,而且每次需要肉眼搜索适合的 init 方法。

其实我们还可以用另一种姿势来 init 我们的对象。

Builder pattern

最初是在学习 Android 的时候,发现这个 builder pattern 也可以用来构建对象,而且可以很好的解决 init 方法过多难以管理的问题。先来看下如何实现,顾名思义,builder pattern 使用另一个名为 builder 的类来创建我们的目标对象,还是上面的例子,代码如下:

//UserBuilder.h
@interface UserBuilder : NSObject
@property (nonatomic, strong, readonly) NSNumber*                 userID;
@property (nonatomic, strong, readonly) NSString*                 userName;
@property (nonatomic, strong, readonly) NSString*                 signature;

- (UserBuilder*)userID:(NSNumber*)userID;
- (UserBuilder*)userName:(NSString*)userName;
- (UserBuilder*)signature:(NSString*)signature;
@end

//UserBuilder.m
@implementation UserBuilder
- (UserBuilder*)userID:(NSNumber*)userID {
    _userID = userID;
    return self;
}
- (UserBuilder*)userName:(NSString*)userName {
    _userName = userName;
    return self;
}
- (UserBuilder*)signature:(NSString*)signature {
    _signature = signature;
    return self;
}
@end

接下来 User 的 init 方法从 Builder 中获取 property 的初始值:

//User.h
@interface User : NSObject
@property (nonatomic, strong, readonly) NSNumber*                 userID;
@property (nonatomic, strong, readonly) NSString*                 userName;
@property (nonatomic, strong, readonly) NSString*                 signature;

- (instancetype)initWithUserBuilder:(UserBuilder*)builder;
@end
  
//User.m
@implementation User
- (instancetype)initWithUserBuilder:(UserBuilder*)builder {
    self = [super init];
    if (!self) {
        return nil;
    }
    
    _userID = builder.userID;
    _userName = builder.userName;
    _signature = builder.signature;
    
    return self;
}
@end

如果要创建 User 对象,则按照这种方式:

UserBuilder* builder = [[[[UserBuilder new] userName:@"peak"] userID:@1000] signature:@"roll"];
User* user = [[User alloc] initWithUserBuilder:builder];

这样我们避免了书写多个 init 方法,同样 User 对象也是 immutable 的,也做到了只在 init 方法中做一次赋值操作,每个场景都可以按照自己的需求初始化部分 property,当然最后我们需要在 initWithUserBuilder 中为每一个 property 赋值, initWithUserBuilder 扮演的角色类似于 designated initializer。

追求代码美感的同学可能发现了, UserBuilder 的创建语法很丑陋,多个 [ ] 套嵌使用。为了让代码更好看一些,我们也可以使用 block 来创建:

User* user = [User userWithBlock:^(UserBuilder* builder) {
    builder.userName = @"peak";
    builder.userID = @1000;
    builder.signature = YES;
}];

builder pattern 在 Android 平台使用的比较多,我在 iOS 平台上鲜少有看到使用的场景。builder pattern 的不足之处也比较明显,需要另外定义一个 builder 类,多写一些代码(property 基本都重复写了一遍)。个人觉得,在 property 数量较多,初始化的场景也比较多的时候,在 iOS 上使用 builder pattern 也会是个不错的方案。

designated initializer vs builder pattern,这二者之间的不同其实很好的体现了语言本身的差异性。学习过 java 的同学就能明白,在 java 的世界中,一切都是可以被封装成对象的,使用 java 的时候,经常要定义各式各样的辅助类来完成某个任务,好处是封装度高,类职责划分粒度小,缺点是类太多,有时候会为了封装而封装,某些场景代码反而不够直观。

经读者反馈,原来这篇文章的主题已经被写过了。看过之后发现比我写的更全面,推荐大家阅读,传送门

总结

简单梳理了下创建对象的不同姿势,希望对大家有些帮助。

欢迎关注公众号:MrPeakTech


Hosted by Coding Pages