iOS 创建对象的姿势

nugc5795 7年前
   <p>在写 iOS 代码的时候,怎么样去 new 一个新对象出来,都有一些讲究在里面。使用不同的姿势去创建对象,对后期维护所造成的影响会存在细微的差别。</p>    <h3>init 创建</h3>    <p>在之前一篇分析 iOS 代码耦合的文章中,提到过当我们给一个对象的 property 赋值的时候,通过 init 方法传入参数来初始化 property 会让我们的代码更可靠。</p>    <p>有些人在定义带 property 的 class 的时候,会这样定义:</p>    <pre>  <code class="language-objectivec">@interface User : NSObject  @property (nonatomic, strong) NSNumber*                 userID;  @end</code></pre>    <p>使用的时候如下:</p>    <pre>  <code class="language-objectivec">User* user = [[User alloc] init];  user.userID = @1000;</code></pre>    <p>尤其是在定义 model 的时候,很容易写出这种,先 init,而后挨个给 property 赋值的代码。这种代码的问题在于 property 对于外部是可写的,property 处于随时可能变化的状态。之前不少篇文章中都强调过 immutable 的重要性,同样对于一个 class,我们也应该优先考虑设计成 immutable 的。</p>    <h3>initWith 创建</h3>    <p>如果将 property 都设置成 readonly 的,或者不暴露 property,property 的赋值都通过 initWith 的方式来初始化,就可以得到一个具备 immutable 的 class 定义了,具体到上面的例子代码如下:</p>    <pre>  <code class="language-objectivec">//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</code></pre>    <p>userID 在 .h 文件当中是 readonly 的,userID 只有一次被赋值的机会,即在 User 的 initWith 方法中。这种方式的好处是一旦 User 对象创建完毕之后,就处于 immutable 的状态,property 都是不可修改的,安全可靠。</p>    <h3>Designated initializer</h3>    <p>Apple 为了方便开发者使用 init 方法,引入了一种名为 designated initializer 的 pattern。主要用来管理当一个 class 拥有多个 property 需要赋值的场景。比如上面我们的 User 类:</p>    <pre>  <code class="language-objectivec">@interface User : NSObject  @property (nonatomic, strong, readonly) NSNumber*   userID;  @property (nonatomic, strong, readonly) NSString*   userName;  @property (nonatomic, strong, readonly) NSString*   signature;  @end</code></pre>    <p>有些场景需要初始化 userID 和 userName,而有些场景只需要初始化 userID 和 signature,所以我们需要提供多个 initWith 方法给不同的场景使用。为了管理 initWith 方法,Apple 将 init 方法分为两种类型:designated initializer 和 convenience initializer (又叫 secondary initializer) 。</p>    <p>designated initializer 只有一个,它会为 class 当中每个 property 都提供一个初始值,是最完整的 initWith 方法。convenience initializer 则可以有很多个,它可以选择只初始化部分的 property。convenience initializer 最后到会调用到 convenience initializer,所以 convenience initializer 也可以叫做 final initializer。</p>    <p>无论我们定义何种类型的 class,给 class 中的每个 property 都赋予一个初始值是个很好的习惯,可以避免掉一些意外的 bug 产生,这也是 designated initializer 的重要职责。</p>    <p>在实际的项目当中,一个 class 的 property 数目可能会随着业务的增长而增加,最后的结果就是会生成越来越多的 convenience initializer。上述的 User 类,如果是 3 个 property,极端的情况下最多可以有 7 个 init 方法。Peak君在阅读代码的时候,也确实看到过有些 class 定义了一连串整整齐齐摆放的 init 方法,代码虽然看着规范,但显得啰嗦,而且每次需要肉眼搜索适合的 init 方法。</p>    <p>其实我们还可以用另一种姿势来 init 我们的对象。</p>    <h3>Builder pattern</h3>    <p>最初是在学习 Android 的时候,发现这个 builder pattern 也可以用来构建对象,而且可以很好的解决 init 方法过多难以管理的问题。先来看下如何实现,顾名思义,builder pattern 使用另一个名为 builder 的类来创建我们的目标对象,还是上面的例子,代码如下:</p>    <pre>  <code class="language-objectivec">//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</code></pre>    <p>接下来 User 的 init 方法从 Builder 中获取 property 的初始值:</p>    <pre>  <code class="language-objectivec">//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</code></pre>    <p>如果要创建 User 对象,则按照这种方式:</p>    <pre>  <code class="language-objectivec">UserBuilder* builder = [[[[UserBuilder new] userName:@"peak"] userID:@1000] signature:@"roll"];  User* user = [[User alloc] initWithUserBuilder:builder];</code></pre>    <p>这样我们避免了书写多个 init 方法,同样 User 对象也是 immutable 的,也做到了只在 init 方法中做一次赋值操作,每个场景都可以按照自己的需求初始化部分 property,当然最后我们需要在 initWithUserBuilder 中为每一个 property 赋值,  initWithUserBuilder 扮演的角色类似于 designated initializer。</p>    <p>追求代码美感的同学可能发现了, UserBuilder 的创建语法很丑陋,多个 [ ] 套嵌使用。为了让代码更好看一些,我们也可以使用 block 来创建:</p>    <pre>  <code class="language-objectivec">User* user = [User userWithBlock:^(UserBuilder* builder]) {      builder.userName = @"peak";      builder.userID = @1000;      builder.signature = YES;  }];</code></pre>    <p>builder pattern 在 Android 平台使用的比较多,我在 iOS 平台上鲜少有看到使用的场景。builder pattern 的不足之处也比较明显,需要另外定义一个 builder 类,多写一些代码(property 基本都重复写了一遍)。个人觉得,在 property 数量较多,初始化的场景也比较多的时候,在 iOS 上使用 builder pattern 也会是个不错的方案。</p>    <p>designated initializer vs builder pattern,这二者之间的不同其实很好的体现了语言本身的差异性。学习过 java 的同学就能明白,在 java 的世界中,一切都是可以被封装成对象的,使用 java 的时候,经常要定义各式各样的辅助类来完成某个任务,好处是封装度高,类职责划分粒度小,缺点是类太多,有时候会为了封装而封装,某些场景代码反而不够直观。</p>    <h3>总结</h3>    <p>简单梳理了下创建对象的不同姿势,希望对大家有些帮助。</p>    <p> </p>    <p>来自:http://mp.weixin.qq.com/s/io9rzhtn9l8RYoHvH4tWyg</p>    <p> </p>