Improving Immutable Object Initialization in Objective-C 提高oc中不可变对象的初始化方法

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.
已经有了很多关于不可变对象的优点的讨论。在过去的几个月里面,我一直尽可能的确保系统组成部分是不可变的。这样做时,我注意到不可变对象的创建可能变得麻烦,所以我开始改进它。你可以通过一个叫AHKBuilder的库来理解我思考的结果,接下来来理解学习这个库为什么,怎么样实现。

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:
假设我们正在构建一个简单的待办事宜应用程序。如果没有提醒,应用程序将不是非常有用。因此,我们继续创建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.
因为它是不可变的,它的所有属性都是只读的,所以我们必须设置它们的值而不使用setter,因为setter不可用。

Initializer mapping arguments to properties 初始化程序将参数映射到属性

The simplest way to do that, is to add an initializer:
最简单的方法是添加一个初始化构造器:

- (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:
在大多数情况下,这种初始化是我们需要的。,虽然很容易注意到它的缺点:

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.
当我们添加更多的属性(这不是那么难以想到几个更多的这样的类),我们将最终只有太多的参数。

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.
使用这个初始化器必须总是提供所有的值 - 我们不能容易地使showsAlert在默认情况下为true; 理论上,我们可以创建另一个初始化器:initWithTitle:date :,但是如果我们想对每个组合都这样做,我们最终会得到很多初始化器,例如5个属性有31个这样的组合。

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中使用的模式固定。 初始化器采用以下形式(其实现可以在GitHub上找到):

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

This way of initializating works fine in the context of Mantle, but in general has its bad points:
这种初始化方式在Mantle的环境中工作正常,但是一般有其不好的地方:

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}并导致运行时崩溃。作为旁注,使用NSStringFromSelector(@selector(title))而不是字符串有帮助,但只有一点。

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:
我们最终不满意,并继续我们的追求最好的方式来初始化不可变对象。 Cocoa一块丰富的领域,所以我们可以 - 而且应该 - 参照苹果在其框架中使用的一些想法。 我们可以创建一个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在可变对象和不可变对象之间转换。 最常见的情况符合我们的例子:一个基类是不可变的,它的子类是可变的。

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.
这种方式的主要缺点是我们最多有两倍的类。 此外,可变子类通常仅作为初始化和修改其不可变版本的方式存在。 偶然使用一个可变的子类可能导致许多错误。 例如,设置属性时会担心出错。 我们必须总是检查一个可变子类是否存在,如果是这样的话,对基类使用copy修饰符而不是strong。

Builder pattern 构建器模式

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:
构建器模式处于初始化字典和可变子类之间。我在Objective-C中看到的第一个使用是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.
我没有看到在这种形式使用它的许多优点,但事实证明,它可以大大改善。

Improving builder pattern 改进构建器模式

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:
我们应该摆脱的第一件事是在构建器block中使用的另一个类。我们可以通过引入一个协议来做到:

@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!
不是简单地引入一个符合这个(ReminderBuilder)协议的新类,我们会做一些更有趣的事情。我们将利用Objective-C的动态特性来而不用创建这样的类!

The initializer will be declared in a category onNSObject, so it won't be tied to ourReminder example:
初始化程序将在NSObject的类别中声明,因此不会与我们的Reminder 示例绑定:

@interface NSObject (AHKBuilder)

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

@end

Its implementation will take the following form:
其实现将采取以下形式:

- (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
正如你可以看到所有的魔法发生在AHKForwarder。 我们希望AHKForwarder的行为就像是实现构建器协议。 因为我想保持解决方案一般我认为我可以只从方法签名(initWithBuilder_ahk:^(id <ReminderBuilder> builder)获取协议名称)。 原来,在运行时所有的对象都是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.
第二个想法,我注意到,生成器协议声明与我们的不可变类相同的属性,唯一的区别是它使用readwrite修饰符。 因此,我们甚至不必知道构建器协议是如何命名的或它包含什么! 我们可以假设它在不可变类中声明了readonly属性的setters。 对于配置的约定在Objective-C中没有太多使用,但我认为它在这里有它的地方。

Let's go step by step viaAHKForwarder source:
让我们一步一步验证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中:我们使用目标对象(在我们的示例中,Reminder类的实例)getter的实现来为setter构建签名。 我们使用的大多数东西在Objective-C Runtime文档中都有描述,所以没有必要在这里重复。

InforwardInvocation: we check whether a selector is a setter, and then do one of two things:
forwardInvocation:我们检查一个选择器是否是一个setter,然后做两个事情之一:
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.
如果它是一个setter,我们使用KVC,设置一个属性的值。提醒:KVC允许我们更改readonly属性的值,因为它们是默认合成的。

2.If it is not a setter, we invoke the selector on the target object. This allows getters to function properly inside the block.
如果它不是一个setter,我们调用目标对象上的选择器。 这允许getter在Block内正确地运行。

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.
这就是所有的了。 一些技巧让我们创建一个简单的API。 我们可以类似地实现copyWithBuilder:。 我们不会在这里展示它的源码,但你能在GitHub上看到它。

Summary 概要

Finally, here's a comparison of the described builder pattern with other initialization methods:
最后,下面是所描述的构建器模式与其他初始化方法的比较:

Pros: 优点

  • allows for compile-time safe initialization of immutable objects with many properties
    允许具有许多属性的不可变对象的编译时安全初始化
  • 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
    允许通过在不可变类中实现init来使用默认值

Cons:缺点

  • works best with the described case: classes withreadonly properties
    类有readonly的情况下使用最好
  • doesn't support custom setter names in a builder protocol
    不支持构建器协议中的自定义设置器名称
  • object passed in the block doesn't respond toconformsToProtocol: correctly, because we don't know the protocol's name
    block中的对象传递不能正确的响应conformsToProtocol:,因为我们不知道协议的名字

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,826评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,968评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,234评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,562评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,611评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,482评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,271评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,166评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,608评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,814评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,926评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,644评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,249评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,866评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,991评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,063评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,871评论 2 354

推荐阅读更多精彩内容