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 

@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 

@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 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 builder)). It turned out that at runtime all objects are ids, so it's not possible
正如你可以看到所有的魔法发生在AHKForwarder。 我们希望AHKForwarder的行为就像是实现构建器协议。 因为我想保持解决方案一般我认为我可以只从方法签名(initWithBuilder_ahk:^(id 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/

你可能感兴趣的:(Improving Immutable Object Initialization in Objective-C 提高oc中不可变对象的初始化方法)