Objective-C 语言环境下关于可继承的链式调用实现的思考

问题背景

链式语法相信我们并不陌生,Masonry 就是一个很好的例子;利用 Objective-C 的“点语法”,结合 block,就可以实现了一个简单的的链式调用的设计:

@interface Base : NSObject
- (Base *)one;
- (Base *(^)(void))blockOne;
@end

这样可以做到类型为 Base 的对象 obj 实现如下的链式调用:obj.blockOne().one.blockOne().one....;但是我们今天要来思考的问题是如何让这个链式调用可以被继承呢? 比如有一个类 A 继承自 Base,自身有 two 和 blockTwo 两个签名相同的方法,只是返回值类型需要是 A ,要让类型为 A 的对象 obj 实现形如 obj.blockOne().two.blockTwo().one.... 的链式调用该怎么设计呢?

思考

假设我们这样设计 A :

@interface A : Base
- (A *)two;
- (A *(^)(void))blockTwo;
@end

类型为 A 的对象 obj 现在是可以链式调用 A 类本身的方法,原理同上,但是如果我们调用了父类 Base 的方法,此时就无法再去调用 A 类本身的方法了,也就是“断链”了。

所以我们该如何设计这两个类呢?仔细想了下其实问题的本质是如何让编译器“智能”的识别方法返回值类型(骗过编译器?),首先排除了重写父类 one/blockOne 方法的方案,因为如果 Base 的方法如果有很多那我们必须按都重写一遍显然不合理,并且如果还有类 B 继承自 A 呢,B 需要重写的方法是 A + Base 的,随着继承关系的深度线性增长。

指定返回值的类型

问题本质是由于方法返回值类型不确定造成的,那么如果我们用一个将返回值统一为一个实现了 Base 和 A 的接口的类呢?首先想到的方案是让一个对象实现所有的接口,然后把调用转移到具体的对象上:

@class ChainCall;
@protocol BaseProtocol <NSObject>
- (ChainCall *)one;
- (ChainCall *(^)(void))blockOne;
@end

@protocol AProtocol <NSObject>
- (ChainCall *)two;
- (ChainCall *(^)(void))blockTwo;
@end

@interface Base : NSObject<BaseProtocol>
@property (nonatomic, weak) ChainCall *chain;
@end

@interface A : Base<AProtocol>
@end

@interface ChainCall : NSObject<BaseProtocol, AProtocol>
@property (nonatomic, weak) Base *obj;
@end

先忽略上述类的方法实现,单纯从接口来看,确实可以实现类型为 A 的对象 obj 的链式调用 : obj.one.blockOne(0).blockTwo(0).two...,但是类型为 Base 的对象同样也可以使用如上的调用,这样接口没了边界,所有接口成了一锅大杂烩,并且需要在 ChainCall 的每个方法实现中判断持有对象是否支持对应的方法,以及完成消息转发。

由于觉得每个方法都手动实现转发会有些麻烦,于是想通过 runtime 的消息转发原理将调用转发到持有的对象调用,做了如下的尝试:

@implementation ChainCall
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *sig = [super methodSignatureForSelector:aSelector];
if (sig == nil) {
Method m = class_getInstanceMethod([self class], @selector(getSelf));
const char *encoding = method_getTypeEncoding(m);
sig = [NSMethodSignature signatureWithObjCTypes:encoding];
}
return sig;
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
if ([self.obj respondsToSelector:aSelector]) {
return self.obj;
}
return [super forwardingTargetForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
[anInvocation setSelector:@selector(getSelf)];
[anInvocation invokeWithTarget:self];
}

- (ChainableCall *)getSelf {
return self;
}
@end

通过 forwardingTargetForSelector 将调用转发确实没有问题,但是在调用不支持的 selector 的兼容处理上遇到了困难,设计的策略是调用不存在的方法直接返回当前对象,使得后面的调用可以生效,但是返回值为 block 类型的方法,返回 ChainCall 之后执行 block 就直接闪退了,想想也是理所当然的,把一个对象当成 block 来使用。但是如果需要针对 block 生成相应的方法转发是不现实的,首先通过 runtime 无法获取到返回值为 block 类型的确定参数个数和类型,再者也无法根据 block 类型生成对应的实现。

所以 ChainCall 只剩下一个手动一个个实现协议方法的方案了,每个协议方法的实现都需要判断是否支持,不支持则返回 self :

@implementation ChainCall
- (ChainCall *)one {
if ([self.obj respondsToSelector:_cmd]) {
id<BaseProtocol> obj = (id<BaseProtocol>)self.obj;
return [obj one];
}
return self;
}
- (ChainCall *(^)(void))blockOne {
if ([self.obj respondsToSelector:_cmd]) {
id<BaseProtocol> obj = (id<BaseProtocol>)self.obj;
return [obj blockOne];
}
return ^ ChainCall *(void) {
return self;
};
}
@end

这样倒是保证了不会程序的稳定(至少不会 crash),但是一个个重新实现一遍也挺烦的,结合上面提到的接口边界问题,另外还可能存在方法命名冲突等问题,所以这个方案并不是个好的解决方案,还得再想想。

instancetype关键字

说到继承关系对象类型的“智能”识别,你可能会想到 instancetype 关键字,确实如果我们将 one/two 的返回值修改为 instancetype,可以实现 obj.one.two的调用,但是 block 不行,instancetype 只能针对 oc 的方法做返回值类型关联,参考:

A method with a related result type can be declared by using the type instancetype as its result type. instancetype is a contextual keyword that is only permitted in the result type of an Objective-C method, e.g.
Clang docs about instancetype

泛型

除了 instancetype 关键字,返回值类型的智能识别我也只能想到使用泛型了,我们将 Base 设置成支持泛型:

@interface Base<T> : NSObject
- (T)one;
- (T(^)(void))blockOne;
@end

这时候如果 A 实现如下:

// A1
@class A;
@interface A : Base<A *>
- (A *)two;
- (A *(^)(void))blockTwo;
@end

类型为 A 的 obj 对象确实也可以实现链式调用,但是 A 本身的泛型变成了不可继承了,继承自 A 的类 B 对应的对象调用 A 的方法就不能调用 B 本身的方法了,这样自也是行不通的。那也就是 A 需要变成泛型可继承的:

// A2
@interface A<T> : Base<T>
- (T)two;
- (T (^)(void))blockTwo;
@end

由于 A 的返回值是泛型对象,那需要生成类型为 A<A *> 的 obj 来调用,但是 obj 调用任意方法之后,返回的 T 为 A 类型,并非 A<A *>, 也就导致了再次任意方法之后,返回值变成了 id 类型,后面也就“断链”了。

A<A *> *obj;
obj.one.two; // 执行one类型变成A,执行two之后类型变成了id

所以这要怎么搞呢,在自己的思路里面绕了好几天的弯子,后面想到其实在 A2 的基础上创建 A 的子类 ConcreteA,确定泛型的实际类型,保证后面的调用不会“断链”,而 A 本身作为一个可继承的泛型类即可,具体实现如下:

// 可供子类继承的类
@interface Base<T> : NSObject
- (T)one;
- (T(^)(void))blockOne;
@end
@implementation Base
- (id)one {
return self;
}
- (id (^)(void))blockOne {
return ^id {
return self;
};
}
@end

// ConcreteBase 继承自 Base,并且指定了返回值类型,然后其他什么都不用做
@class ConcreteBase;
@interface ConcreteBase : Base<ConcreteBase *>
@end
@implementation ConcreteBase
@end

// A 继承自 Base,同时支持泛型
@interface A<T> : Base<T>
- (T)two;
- (T(^)(void))blockTwo;
@end
@implementation A
- (id)two {
return self;
}
- (id(^)(void))blockTwo {
return ^id{
return self;
};
}
@end

// 同样 ConcreteA 继承自 A,并且指定了返回值类型
@interface ConcreteA : A<ConcreteA *>
@end
@implementation ConcreteA
@end

// 然后我们就可以像以下这样调用了
ConcreteA *obj = [ConcreteA new];
obj.blockOne().blockTwo().one.two.blockOne().blockTwo();

这样类型为 ConcreteA 的对象 obj 可以实现链式调用,同时子类也可以继承自 A 继承泛型,达到我们需要的效果;当然,这个方案的缺点是需要创建多一个类来使用,但是如果这是个 final 的类那就可以合并两个类,不需要考虑继承了,也就碰巧实现了 final 关键字?

总的来说这个方案使得调用接口有了边界,接口调用不会存在歧义,个人倾向于这个方案来解决这个问题。

最后

以上就是本次针对这个可继承链式调用的思考过程,最后 pick 的方案是使用泛型加上一个实际应用的类和一个可继承类解决这个问题;花了几天的时间去思考这个问题的实现方式,最后总算有个比较能接受的方案。如果你有更好的方案,请不吝赐教哈。