原文出处:Builder Pattern 在 Objective-C 中的使用

在说 Builder Pattern 之前,我们先来看看一个场景。假设我们要预定一个 iPhone 6,要 64G 的,金色的,用代码表述大概是这样

// PFX 是一个前缀,因为直接写 iPhone6 不符合类名大写的习惯,写成 IPhone6 更是怪异 ╮(╯▽╰)╭
PFXiPhone6 *iphone = [[PFXiPhone6 alloc] init];
iphone.storage = 64;
iphone.color = [UIColor goldenColor];

也可以是另一种方式

PFXiPhone6 *iPhone = [[PFXiPhone6 alloc] initWithStorage:64 color:[UIColor goldenColor]];

第一种方式可扩展性好些,但无法约束必须设置某些 property。第二种方式修正了这个问题,但扩展性也差了。

假如又有了新需求,要让客户可以选择发售区域,比如港行,国行,美版等等。对于第一种,自然可以新增一个属性,但使用者很可能完全不知道新增了这么个属性。对于第二种,则只能再新建一个初始化方式,如 -[initWithStorage:color:place]。那如果又有新的需求,比如选择是否刻字,以及刻哪些字呢?或者可以选择外壳的种类等等。这两种方式都不能很好地处理需求的变更。

现在我们来说说 Builder Pattern,这个模式可以在各种语言里被很方便地实现,比如 javascript

new PFXiPhone6Builder()
  .setStorage(64)
  .setColor('golden')
  .setPlace('HK')
  .build();

当有新的属性时,再加一个 setProperty 即可。如果漏写了某个属性,可以在 build 里检查。

在 Objective-C里,这样的链式写法不是很流行(Masonry里这种写法用的比较多),所以,在OC 里写起来大概会是这样

PFXiPhone6Builder *builder = [[PFXiPhone6Builder alloc] init];
builder.storage = 64;
builder.color = [UIColor goldenColor];
builder.place = @"HK";
PFXiPhone6 *iphone = [builder build];

如果少了什么属性,在 build 时检查下即可。这样既解决了不方便扩展的问题,当有新的属性时也可以知道。

不过看起来还是不够优雅,这个 builder 只是一个临时工具,用完了就扔掉了,既然这样,那有没有可能写法上符合 OC 的传统,又让这个 builder「临时出现」一下?且看下面这段代码

PFXiPhone6 *iPhone6 = [PFXiPhone6 createWithBuilder:^(PFXiPhone6Builder *builder){
    builder.storage = 64;
    builder.color = [UIColor goldenColor];
    builder.place = @"HK";
}];

是不是看起来舒服多了。builder 只是在 block 范围内起作用,不会影响当前 context 的变量。这个+[createWithBuilder:] 的代码如下

+ (instancetype)createWithBuilder:(BuilderBlock)block {
    NSParameterAssert(block);
    PFXiPhone6Builder *builder = [[PFXiPhone6Builder alloc] init];
    block(builder);
    return [builder build];
}

这里 build 方法,也有两种实现,第一种

// PFXiPhone6Builder
- (PFXiPhone6 *)build
{
    return [[PFXiPhone6 alloc] initwithBuilder:self];
}
// PFXiPhone6
- (instancetype)initwithBuilder:(PFXiPhone6Builder *)builder
{
    if (self = [super init]) {
        _storage = builder.storage;
        _color = builder.color;
        _place = builder.place;
    }
}

另外一种是把两个过程合并为一个过程

// PFXiPhone6Builder
- (PFXiPhone6 *)build
{
    // 可以在这里对 property 做检查
    NSAssert(self.place, @"发行区别忘了填哦");
    PFXiPhone6 *iphone6 = [[PFXiPhone6 alloc] init];
    iPhone6.storage = self.storage;
    iPhone6.color = self.color;
    iPhone6.place = self.place;
    return iPhone6;
}

这两种方式的区别在于对参数的处理,前一个是在目标 Class 中处理,后一种是在 Builder 中处理。

在 Facebook 的 pop 中也有类似的使用,如

POPAnimatableProperty *animatableProperty = [POPAnimatableProperty propertyWithName:@"property" initializer:^(POPMutableAnimatableProperty *prop) {
    prop.writeBlock = ^(id obj, const CGFloat values[]) {
    };
    prop.readBlock = ^(id obj, CGFloat values[]) {
    };
}];

这里的 initializer 其实就是 builder

我在写蘑菇街的基础框架时,也有用到过几处,觉得还是蛮方便的,尤其对使用者来说。比如这个可以横向或纵向滚动的包含可点击 Items 的collectionView。

self.collectionView = [MGJFlowCollectionView collectionViewWithBuilder:^(MGJFlowCollectionViewBuilder *builder) {
    builder.scrollDirection = UICollectionViewScrollDirectionHorizontal;
    builder.minimumInteritemSpacing = 10;
    builder.minimumLineSpacing = 10;
    builder.sectionInset = UIEdgeInsetsMake(10, 10, 10, 10);
    CGSize itemSize = CGSizeMake(81, 100);
    builder.itemSize = itemSize;
    builder.dataSource = @[@1,@2,@3,@4,@5,@6, @7,@8, @9, @10];
    builder.cellBuilder = ^UIView *(NSNumber *number){
        UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, itemSize.width, itemSize.height)];
        view.backgroundColor = [UIColor mgj_random];
        return view;
    };
}];

这样就能通过简单的配置来生成一个水平的或垂直的 collectionView 了。

使用 Builder Pattern 还有一个好处,就是可以将零散的配置统一起来。比如要创建一个 CollectionView,我们需要设置layout,还要设置 layout 的一些属性,还要设置 DataSource / Delegate 等,现在可以在一个地方统一设置,可读性上也会好一些。

所以如果遇到需要多个参数,甚至某个参数自己还包含了各种参数时,可以考虑下 Builder Pattern。

参考:http://www.annema.me/the-builder-pattern-in-objective-c


原文出处:THE BUILDER PATTERN IN OBJECTIVE-C

Learning Android and by extension Java has given me some new patterns to apply in my Objective-C code. The one that I'm most excited about is the builder pattern.

The builder pattern is an object creation pattern that fits well with Java's syntax. I've seen some adaptions to Objective-C but they all directly copy Java's implementation directly and are not very idiomatic.

Let's start off with what it looks like in Java. For my talk about applying patterns from other languages I used a Pizza model as an example, I'll use the same here. This is what it looks like:

Pizza pizza = new Pizza.Builder()
    .size(12)
    .pepperoni(true)
    .mushrooms(true)
    .build();

Think about what this would look like if you had to use a constructor. What would happen to the constructor if the restaurant decides to add more toppings or perhaps different crust types? For these kinds of objects it's easy to end up with an array of possible constructors all covering different permutations of the arguments.

This is the problem the builder pattern solves. It decouples the construction of a complex object from it's representation. The builder pattern is a so called fluent interface; an API that aims to provide more readable code.

A direct port to Objective-C looks something like this:

Pizza *pizza = [[[[[PizzaBuilder alloc] init] setPepperoni:YES] setMushrooms:YES] setSize:12] build];

Not very fluent is it? Unlike others I don't mind Objective-C's square brackets but there's a limit. The following is a more idiomatic approach. It makes use of Objective-C's dot syntax to set the properties:

PizzaBuilder *builder = [[PizzaBuilder alloc] init];
builder.size = 12;
builder.pepperoni = YES;
builder.mushrooms = YES;
Pizza *pizza = [builder build];

This definitely reads better but we're exposing the builder beyond it's minimum scope. It's possible to accidentally re-use the same object somewhere in the same method. In general the scope of variables should always be limited to their absolute minimum.

One of the most common mistakes I see when people are trying to adopt a new language is copy patterns from their favorite language and subsequently complain how bad the adopted language is. Copying the Java implementation has served us well so far but we have to let go now and make the pattern ours.

Good artists copy, great artists steal.

In order to make this pattern fit Objective-C we're going to apply another pattern. This one comes from Ruby. I don't know what the official name for it is, I just call it the Ruby configuration block pattern. This is our final idiomatic Objective-C implementation:

Pizza *pizza = [Pizza pizzaWithBlock:^(PizzaBuilder *builder]) {
    builder.size = 12;
    builder.pepperoni = YES;
    builder.mushrooms = YES;
}];

We made the interface fluent, the scope of the builder is limited to within the block and as an added benefit the call to build is now implicit. When the block returns the pizzaWithBlock: method knows that configuration is finished and can call build for us. Not only did we make the pattern idiomatic Objective-C, we also removed one of the Java implementation's major headaches; forgetting to call the sentinel method.

To finish, this is the pizzaWithBlock: method implementation:

+ (instancetype)pizzaWithBlock:(PizzaBuilderBlock)block {
    NSParameterAssert(block);
    PizzaBuilder *builder = [[PizzaBuilder alloc] init];
    block(builder);
    return [builder build];
}

The builder's build method is implemented as:

- (Pizza *)build;
{
    return [[Pizza alloc] initWithBuilder:self];
}

And finally Pizza's initWithBuilder: method:

- (id)initWithBuilder(PizzaBuilder *)builder;
{
    self = [super init];
    if (self) {
        _size = builder.size;
        _pepperoni = builder.pepperoni;
        _mushrooms = builder.mushrooms;
    }
    return self;
}

I use a similar approach in APIClient. Note that I wrote this code before I knew about the builder pattern so it isn't an exact implementation. Having read this post you should now be able to improve my implementation.

I believe we can learn a lot from other languages and their communities. Unfortunately most of us don't have the time to really adopt a new language. This post serves as a shortcut. You just learned a new pattern to apply in your Objective-C code without first having to learn Java or Ruby.