Improving Immutable Object Initialization in Objective-C 提高oc中不可變對(duì)象的初始化方法

Much has been written and said about advantages of using completely immutable objects. For the past few months I’ve been making sure that as many parts as possible of systems I build are immutable. When doing that I've noticed that creation of immutable objects can become cumbersome, so I set out to improve it. You can find the outcome of my thinking in a small library called AHKBuilder. Read on to learn whys and hows behind this library.
已經(jīng)有了很多關(guān)于不可變對(duì)象的優(yōu)點(diǎn)的討論。在過去的幾個(gè)月里面,我一直盡可能的確保系統(tǒng)組成部分是不可變的。這樣做時(shí),我注意到不可變對(duì)象的創(chuàng)建可能變得麻煩,所以我開始改進(jìn)它。你可以通過一個(gè)叫AHKBuilder的庫來理解我思考的結(jié)果,接下來來理解學(xué)習(xí)這個(gè)庫為什么,怎么樣實(shí)現(xiàn)。

Common patterns 常見的模式

Let's say we're building a simple to-do application. The application wouldn't be very useful without reminders. So, we proceed to create a Reminder class:
假設(shè)我們正在構(gòu)建一個(gè)簡單的待辦事宜應(yīng)用程序。如果沒有提醒,應(yīng)用程序?qū)⒉皇欠浅S杏?。因此,我們繼續(xù)創(chuàng)建Reminder類:

@interface Reminder : NSObject

@property (nonatomic, copy, readonly) NSString *title;
@property (nonatomic, strong, readonly) NSDate *date;
@property (nonatomic, assign, readonly) BOOL showsAlert;

@end

Since it's immutable, all its properties are readonly, so we have to set their values without using setters, because they're unavailable.
因?yàn)樗遣豢勺兊模乃袑傩远际侵蛔x的,所以我們必須設(shè)置它們的值而不使用setter,因?yàn)閟etter不可用。

Initializer mapping arguments to properties 初始化程序?qū)?shù)映射到屬性

The simplest way to do that, is to add an initializer:
最簡單的方法是添加一個(gè)初始化構(gòu)造器:

- (instancetype)initWithTitle:(NSString *)title date:(NSDate *)date showsAlert:(BOOL)showsAlert
{
  self = [super init];
  if (self) {
    _title = title;
    _date = date;
    _showsAlert = showsAlert;
  }

  return self;
}

In most cases this kind of an initializer is all we need. It's easy to notice its drawbacks, though:
在大多數(shù)情況下,這種初始化是我們需要的。,雖然很容易注意到它的缺點(diǎn):

1.When we add a few more properties (and it's not that hard to think of a few more for such a class) we'll end up with just too many parameters1.
當(dāng)我們添加更多的屬性(這不是那么難以想到幾個(gè)更多的這樣的類),我們將最終只有太多的參數(shù)。

2.User of this initializer has to always provide all values – we can't easily enforce that by default showsAlertshould be true; theoretically we could create another initializer: initWithTitle:date:
, but if we wanted to do that for every combination we would end up with a lot of initializers, for example for 5 properties there's 31 such combinations.
使用這個(gè)初始化器必須總是提供所有的值 - 我們不能容易地使showsAlert在默認(rèn)情況下為true; 理論上,我們可以創(chuàng)建另一個(gè)初始化器:initWithTitle:date :,但是如果我們想對(duì)每個(gè)組合都這樣做,我們最終會(huì)得到很多初始化器,例如5個(gè)屬性有31個(gè)這樣的組合。

Initializer taking dictionary 初始化字典

Above issues can be fixed with a pattern used in Mantle. The initializer takes the following form (its implementation can be found on GitHub):

以上問題可以用Mantle中使用的模式固定。 初始化器采用以下形式(其實(shí)現(xiàn)可以在GitHub上找到):

- (instancetype)initWithDictionary:(NSDictionary *)dictionary;

This way of initializating works fine in the context of Mantle, but in general has its bad points:
這種初始化方式在Mantle的環(huán)境中工作正常,但是一般有其不好的地方:

1.We lose any help from the compiler. Nothing stops us from passing @{@"nonexistentProperty" : @1} and getting a runtime crash. As a sidenote, using NSStringFromSelector(@selector(title)) instead of a string helps, but only by a little.
我們失去了編譯器的任何幫助。沒有什么能阻止我們傳遞@ {@“nonexistentProperty”:@ 1}并導(dǎo)致運(yùn)行時(shí)崩潰。作為旁注,使用NSStringFromSelector(@selector(title))而不是字符串有幫助,但只有一點(diǎn)。

2.We have to wrap primitive types used in the dictionary.
我們必須包裝字典中使用的原始類型。

Mutable subclass 可變的子類

We end up unsatisfied and continue our quest for the best way to initialize immutable objects. Cocoa is a vast land, so we can – and should – steal some of the ideas used by Apple in its frameworks. We can create a mutable subclass ofReminder class which redefines all properties asreadwrite:
我們最終不滿意,并繼續(xù)我們的追求最好的方式來初始化不可變對(duì)象。 Cocoa一塊豐富的領(lǐng)域,所以我們可以 - 而且應(yīng)該 - 參照蘋果在其框架中使用的一些想法。 我們可以創(chuàng)建一個(gè)Remable類的可變子類,將所有屬性重新定義為readwrite:

@interface MutableReminder : Reminder <NSCopying, NSMutableCopying>

@property (nonatomic, copy, readwrite) NSString *title;
@property (nonatomic, strong, readwrite) NSDate *date;
@property (nonatomic, assign, readwrite) BOOL showsAlert;

@end

Apple uses this approach for example in NSParagraphStyle
and NSMutableParagraphStyle
. We move between mutable and immutable counterparts with -copy and -mutableCopy. The most common case matches our example: a base class is immutable and its subclass is mutable.
蘋果在NSParagraphStyle和NSMutableParagraphStyle中使用這種方法。 我們使用-copy和-mutableCopy在可變對(duì)象和不可變對(duì)象之間轉(zhuǎn)換。 最常見的情況符合我們的例子:一個(gè)基類是不可變的,它的子類是可變的。

The main disadvantage of this way is that we end up with twice as many classes. What's more, mutable subclasses often exist only as a way to initialize and modify their immutable versions. Many bugs can be caused by using a mutable subclass by accident. For example, a mental burden shows in setting up properties. We have to always check if a mutable subclass exists, and if so use copy modifier instead of strong for the base class.
這種方式的主要缺點(diǎn)是我們最多有兩倍的類。 此外,可變子類通常僅作為初始化和修改其不可變版本的方式存在。 偶然使用一個(gè)可變的子類可能導(dǎo)致許多錯(cuò)誤。 例如,設(shè)置屬性時(shí)會(huì)擔(dān)心出錯(cuò)。 我們必須總是檢查一個(gè)可變子類是否存在,如果是這樣的話,對(duì)基類使用copy修飾符而不是strong。

Builder pattern 構(gòu)建器模式

Somewhere between initializing with dictionary and mutable subclass lies the builder pattern. First use of it that I saw in Objective-C was by Klaas Pieter:
構(gòu)建器模式處于初始化字典和可變子類之間。我在Objective-C中看到的第一個(gè)使用是Klaas Pieter:

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

I don't see many advantages of using it in that form, but it turns out it can be vastly improved.
我沒有看到在這種形式使用它的許多優(yōu)點(diǎn),但事實(shí)證明,它可以大大改善。

Improving builder pattern 改進(jìn)構(gòu)建器模式

First thing that we should want to get rid off is another class used just in the builder block. We can do that by introducing a protocol instead:
我們應(yīng)該擺脫的第一件事是在構(gòu)建器block中使用的另一個(gè)類。我們可以通過引入一個(gè)協(xié)議來做到:

@protocol ReminderBuilder <NSObject>

@property (nonatomic, strong, readwrite) NSString *title;
@property (nonatomic, strong, readwrite) NSDate *date;
@property (nonatomic, assign, readwrite) BOOL showsAlert;

@end

Let's take a step back and look at the final API first:
讓我們退一步,先看一下最終的API:

Reminder *reminder = [[Reminder alloc] initWithBuilder_ahk:^(id<ReminderBuilder> builder) {
  builder.title = @"Buy groceries";
  builder.date = [NSDate dateWithTimeIntervalSinceNow:60 * 60 * 24];
}];

Instead of simply introducing a new class that conforms to this (ReminderBuilder) protocol, we'll do something more interesting. We'll leverage Objective-C's dynamism to not create such class at all!
不是簡單地引入一個(gè)符合這個(gè)(ReminderBuilder)協(xié)議的新類,我們會(huì)做一些更有趣的事情。我們將利用Objective-C的動(dòng)態(tài)特性來而不用創(chuàng)建這樣的類!

The initializer will be declared in a category onNSObject, so it won't be tied to ourReminder example:
初始化程序?qū)⒃?code>NSObject的類別中聲明,因此不會(huì)與我們的Reminder 示例綁定:

@interface NSObject (AHKBuilder)

- (instancetype)initWithBuilder_ahk:(void (^)(id))builderBlock;

@end

Its implementation will take the following form:
其實(shí)現(xiàn)將采取以下形式:

- (instancetype)initWithBuilder_ahk:(void (^)(id))builderBlock
{
  NSParameterAssert(builderBlock);
  self = [self init];
  if (self) {
    AHKForwarder *forwarder = [[AHKForwarder alloc] initWithTargetObject:self];
    builderBlock(forwarder);
  }

  return self;
}

As you can see all the magic happens in AHKForwarder. We want AHKForwarder
to behave as if it was implementing builder protocol. As I wanted to keep the solution general I thought that I could just get the protocol name from the method signature (initWithBuilder_ahk:^(id<ReminderBuilder> builder)). It turned out that at runtime all objects are ids, so it's not possible
正如你可以看到所有的魔法發(fā)生在AHKForwarder。 我們希望AHKForwarder的行為就像是實(shí)現(xiàn)構(gòu)建器協(xié)議。 因?yàn)槲蚁氡3纸鉀Q方案一般我認(rèn)為我可以只從方法簽名(initWithBuilder_ahk:^(id <ReminderBuilder> builder)獲取協(xié)議名稱)。 原來,在運(yùn)行時(shí)所有的對(duì)象都是ids,所以這是不可能的。

On second thought I noticed that builder protocol declares the same properties as our immutable class, the only difference is that it usesreadwrite modifier for them. So, we don't even have to know how the builder protocol is named or what it contains! We can just assume that it declares setters forreadonly properties in the immutable class. Convention over configuration isn't that much used in Objective-C, but I think it has its place here.
第二個(gè)想法,我注意到,生成器協(xié)議聲明與我們的不可變類相同的屬性,唯一的區(qū)別是它使用readwrite修飾符。 因此,我們甚至不必知道構(gòu)建器協(xié)議是如何命名的或它包含什么! 我們可以假設(shè)它在不可變類中聲明了readonly屬性的setters。 對(duì)于配置的約定在Objective-C中沒有太多使用,但我認(rèn)為它在這里有它的地方。

Let's go step by step viaAHKForwarder source:
讓我們一步一步驗(yàn)證AHKForwarder源碼:

@interface AHKForwarder : NSObject

@property (nonatomic, strong) id targetObject;

@end

@implementation AHKForwarder

- (instancetype)initWithTargetObject:(id)object
{
  NSParameterAssert(object);
  self = [super init];
  if (self) {
    self.targetObject = object;
  }

  return self;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
  if (isSelectorASetter(sel)) {
    NSString *getterName = getterNameFromSetterSelector(sel);
    Method method = class_getInstanceMethod([self.targetObject class], NSSelectorFromString(getterName));

    const NSInteger stringLength = 255;
    char dst[stringLength];
    method_getReturnType(method, dst, stringLength);

    NSString *returnType = @(dst);
    NSString *objCTypes = [@"v@:" stringByAppendingString:returnType];

    return [NSMethodSignature signatureWithObjCTypes:[objCTypes UTF8String]];
  } else {
    return [self.targetObject methodSignatureForSelector:sel];
  }
}

- (void)forwardInvocation:(NSInvocation *)invocation
{
  if (isSelectorASetter(invocation.selector)) {
    NSString *getterName = getterNameFromSetterSelector(invocation.selector);
    id argument = [invocation ahk_argumentAtIndex:2];
    [self.targetObject setValue:argument forKey:getterName];
  } else {
    invocation.target = self.targetObject;
    [invocation invoke];
  }
}

@end

InmethodSignatureForSelector: we build a signature for setter using target object's (in our example, instance ofReminder class) getter's implementation. We use mostly stuff described in Objective-C Runtime Reference, so there's no need to repeat it here.
在methodSignatureForSelector中:我們使用目標(biāo)對(duì)象(在我們的示例中,Reminder類的實(shí)例)getter的實(shí)現(xiàn)來為setter構(gòu)建簽名。 我們使用的大多數(shù)東西在Objective-C Runtime文檔中都有描述,所以沒有必要在這里重復(fù)。

InforwardInvocation: we check whether a selector is a setter, and then do one of two things:
forwardInvocation:我們檢查一個(gè)選擇器是否是一個(gè)setter,然后做兩個(gè)事情之一:
1.If it is a setter, we use KVC, to set the value of a property.Reminder: KVC allows us to change values of readonly properties, because they're synthesized by default.
如果它是一個(gè)setter,我們使用KVC,設(shè)置一個(gè)屬性的值。提醒:KVC允許我們更改readonly屬性的值,因?yàn)樗鼈兪悄J(rèn)合成的。

2.If it is not a setter, we invoke the selector on the target object. This allows getters to function properly inside the block.
如果它不是一個(gè)setter,我們調(diào)用目標(biāo)對(duì)象上的選擇器。 這允許getter在Block內(nèi)正確地運(yùn)行。

And that's really all there's to it. A couple of tricks that allow us to create a simple API. We can implementcopyWithBuilder: analogously. We won't go through its source here, but you should see it on GitHub.
這就是所有的了。 一些技巧讓我們創(chuàng)建一個(gè)簡單的API。 我們可以類似地實(shí)現(xiàn)copyWithBuilder:。 我們不會(huì)在這里展示它的源碼,但你能在GitHub上看到它。

Summary 概要

Finally, here's a comparison of the described builder pattern with other initialization methods:
最后,下面是所描述的構(gòu)建器模式與其他初始化方法的比較:

Pros: 優(yōu)點(diǎn)

  • allows for compile-time safe initialization of immutable objects with many properties
    允許具有許多屬性的不可變對(duì)象的編譯時(shí)安全初始化
  • it's easy to add and remove properties, change their names and types
    很容易添加和刪除屬性,更改其名稱和類型
  • allows the use of default values by implementinginit in the immutable class
    允許通過在不可變類中實(shí)現(xiàn)init來使用默認(rèn)值

Cons:缺點(diǎn)

  • works best with the described case: classes withreadonly properties
    類有readonly的情況下使用最好
  • doesn't support custom setter names in a builder protocol
    不支持構(gòu)建器協(xié)議中的自定義設(shè)置器名稱
  • object passed in the block doesn't respond toconformsToProtocol: correctly, because we don't know the protocol's name
    block中的對(duì)象傳遞不能正確的響應(yīng)conformsToProtocol:,因?yàn)槲覀儾恢绤f(xié)議的名字

原文地址:http://holko.pl/2015/05/12/immutable-object-initialization/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容