最浪漫的抽象 Promise & Future

这个世界不乏浪漫之人,但在我们程序设计的圈子里,能将代码写得像诗般的人,还是凤毛麟角的。本篇文章要介绍的 Promise&Future 便是我觉得非常浪漫的一种抽象思维,无论是它的命名、实际解决的问题,还是它最终的代码风格,都让我们向诗人更近了一步。

当然,我本就是一名诗人。

基本概念

Promise&Future 在很多语言里都有提及,我也无法追溯到它的最初出处了。时至今日,Promise&Future 已经推演成了各种样式,这里我们只关注它最核心的思想,而推演后的其它衍生概念,只会稍稍说明。

从 Promise&Future 这样的命名来看,本身就是一件非常浪漫的事,承诺未来,这两个词总会让人不禁想起那些年少不羁的往事。多少承诺都已经烟消云散,多少未来终是未能到来,Promise&Future,年少轻狂中的轻描淡写,却烙印下太多的悲愤与不甘。

浪漫是感性的,但程序设计必须是理性的,所以,程序中的 Promise 是不能被遗忘的,Future 是必须要到达的(所以,我还是更喜欢程序构建的世界)。在我们程序设计中,Future 是 Promise 最终产生的值,它是确定的,不可变的

就好像我们向自己心仪的女孩子许下诺言,未来会执子之手与子偕老。那么当时我们这个许诺的行为就是 Promise,而最终我们期望的“执子之手与子偕老”便是 Future。Future 一般包含两种结果:成功失败,这是非常容易理解的,当然还是可以扩充其它状态的,比如:取消

那么放置到我们的程序世界,把上面所述的感性思维理性的抽象后,便是 Promise&Future。这种抽象思维,带来最实际的好处,便是将异步编程模型同步化,使得我们可以很方便的将一连串相关的异步计算组织起来。最早让我关注到 Promise&Future 便是在非常知名的网络组件 netty 中,其源码里大量用到了 Promise&Future,Wiki 里这样的一句话算是非常精炼的总结:

a future is a read-only placeholder view of a variable, while a promise is a writable

到目前为止,上面所阐述的都是 Promise&Future 的核心思想,并且是以面向对象软件设计的抽象思维来推导的。那么,当函数式编程火热起来后,Promise&Future 为了能够更好的将一连串相关的异步计算组织起来,便被扩充成了 Monad,关于什么是 Monad,可以去看看这篇文章。扩充成了 Monad 后,便有了andthen等等不同的连接操作,这也使得它更加优雅,以至于让很多人认为,做不到这样的 Promise&Future,便不是 Promise&Future,这是不对的,难道没有锦衣玉食的乞丐就不是人了么?

实际例子

说了这么多,还是来一段代码,大家实际感受下:

[[[self login] onSuccess:^{
    // do something success
}] onFailure:^(NSError *error) {
    // do something error
}];

仔细分析下上面这简单的代码,[self login] 是一个有同步返回值的方法,但其本身行为是异步的,通过 block 我们也可以达到类似的效果,但对于异步返回值的级联处理就会很麻烦了,并且无法挂钩给多个处理方法。我们可以通过 PromiseKit 中提供的实例代码对比而知:

// load JSON
[NSURLConnection sendAsynchronousRequest:rq1 queue:q completionHandler:^(id, id data1, id err) {
    // load related JSON
    [NSURLConnection sendAsynchronousRequest:rq2 queue:q completionHandler:^(id, id data2, id err) {
        // load image
        [NSURLConnection sendAsynchronousRequest:rq3 queue:q completionHandler:^(id, id dat3a, id err) {
        }];
    }];
}];
[NSURLConnection promise:rq1].then(^(id data1){
    return [NSURLConnection promise:rq2];
}).then(^(id data2){
    return [NSURLConnection promise:rq3];
}).then(^(id data3){
    // yay! the code looks consecutive!
});

PromiseKit 明显是按照 Monad 的思维来实现的,其官网关于嵌套有这样一段话,我觉得还是蛮有道理的:

“Rightward-drift” is not just ugly, it can lead to bugs: rightward closures have access to all higher variables, so we may accidental use them or overwrite them, breaking encapsulation at the least and causing logic errors at the worst.

也就说,嵌套不仅仅是丑陋,还有可能因为变量作用域的问题而引入 BUG。

Objective-C 简洁实现

既然 Promise&Future 这么浪漫,那么为了向诗人迈进,我们可能想要自己去实现一个。当然,这只是理由之一,而更实际的理由,是我们很多时候只想拥有一个非 Monad 版本的 Promise&Future,因为它够简单。这里有一些设计要点:

  • Future 是由 Promise 产生的,并且不可改变
  • Future 中的回调是必须要触发的,无关乎于什么时间去关联(即便 Future 已经有实际值
  • Future 中的回调不能被多次触发
  • Future 中的回调最好能指定回调队列(这个可以用上下文来实现,详见我的这篇文章

大体也就这几点,那么下面是贴代码时间:

@interface CCMPromise ()
@property (nonatomic, strong, readonly) CCMFuture *future;
@end

@implementation CCMPromise

- (instancetype)init {
    if (self = [super init]) {
        _future = [CCMFuture new];
    }
    return self;
}

- (id)dynamicFuture {
    return self.future;
}

- (void)successWithValue:(id)value {
    [self.future success:value];
}

- (void)failureWithError:(NSError *)error {
    [self.future failure:error];
}

@end

//////////////////////////////////////////////////////////////////////////////////////
#pragma mark - BlockMethodSignature
//////////////////////////////////////////////////////////////////////////////////////

struct NSBlockLiteral {
    void *isa; 
    int flags;
    int reserved;
    void (*invoke)(void *, ...);
    struct block_descriptor {
        unsigned long int reserved;
        unsigned long int size;
        void (*copy_helper)(void *dst, void *src);
        void (*dispose_helper)(void *src);
        const char *signature;
    } *descriptor;
};

typedef NS_OPTIONS(NSUInteger, NSBlockDescriptionFlags) {
    NSBlockDescriptionFlagsHasCopyDispose = (1 << 25),
    NSBlockDescriptionFlagsHasCtor = (1 << 26), 
    NSBlockDescriptionFlagsIsGlobal = (1 << 28),
    NSBlockDescriptionFlagsHasStret = (1 << 29),
    NSBlockDescriptionFlagsHasSignature = (1 << 30)
};

static NSMethodSignature *NSMethodSignatureForBlock(id block) {
    if (!block)
        return nil;
    
    struct NSBlockLiteral *blockRef = (__bridge struct NSBlockLiteral *)block;
    NSBlockDescriptionFlags flags = (NSBlockDescriptionFlags)blockRef->flags;
    
    if (flags & NSBlockDescriptionFlagsHasSignature) {
        void *signatureLocation = blockRef->descriptor;
        signatureLocation += sizeof(unsigned long int);
        signatureLocation += sizeof(unsigned long int);
        
        if (flags & NSBlockDescriptionFlagsHasCopyDispose) {
            signatureLocation += sizeof(void(*)(void *dst, void *src));
            signatureLocation += sizeof(void (*)(void *src));
        }
        
        const char *signature = (*(const char **)signatureLocation);
        return [NSMethodSignature signatureWithObjCTypes:signature];
    }
    return 0;
}

//////////////////////////////////////////////////////////////////////////////////////
#pragma mark - Context
//////////////////////////////////////////////////////////////////////////////////////

@interface CCMFutureContext : NSObject

+ (instancetype)contextWithQueue:(CCMDispatchQueue *)queue
                          parent:(CCMFutureContext *)parent;

+ (CCMFutureContext *)currentContext;
+ (void)setCurrentContext:(CCMFutureContext *)context;

@property (nonatomic, strong, readonly) CCMDispatchQueue *dispatchQueue;
@property (nonatomic, strong, readonly) CCMFutureContext *parentContext;

@end

void CCMFutureBeginContext(CCMDispatchQueue *queue) {
    CCMFutureContext *context = [CCMFutureContext contextWithQueue:queue
                                                            parent:[CCMFutureContext currentContext]];
    [CCMFutureContext setCurrentContext:context];
}

void CCMFutureEndContext() {
    CCMFutureContext *context = [CCMFutureContext currentContext].parentContext;
    [CCMFutureContext setCurrentContext:context];
}

@implementation CCMFutureContext

+ (instancetype)contextWithQueue:(CCMDispatchQueue *)queue
                          parent:(CCMFutureContext *)parent {
    CCMFutureContext *context = [CCMFutureContext new];
    context->_dispatchQueue = queue;
    context->_parentContext = parent;
    return context;
}

+ (void)setCurrentContext:(CCMFutureContext *)context {
    if (context == nil) {
        [[NSThread currentThread].threadDictionary removeObjectForKey:@"CCMCurrentFutureContext"];
    } else {
        [NSThread currentThread].threadDictionary[@"CCMCurrentFutureContext"] = context;
    }
}

+ (CCMFutureContext *)currentContext {
    CCMFutureContext *context = [NSThread currentThread].threadDictionary[@"CCMCurrentFutureContext"];
    if (context == nil) {
        context = [self contextWithQueue:[CCMDispatchQueue main] parent:nil];
        [self setCurrentContext:context];
    }
    return context;
}

@end

//////////////////////////////////////////////////////////////////////////////////////
#pragma mark - Future
//////////////////////////////////////////////////////////////////////////////////////

typedef void(^CCMFutureBlockWrapper)(id value);

@interface CCMFuture ()

@property (nonatomic, strong) id futureValue;
@property (nonatomic, strong, readonly) CCMDispatchQueue *dispatchQueue;
@property (nonatomic, strong, readonly) NSMutableArray *blocks;

- (void)tryExecuteCallbacks;

@end

@implementation CCMFuture

- (instancetype)init {
    if (self = [super init]) {
        _dispatchQueue = [CCMDispatchQueue serialQueueWithName:@"queue.util.future"];
        _blocks = [NSMutableArray new];
    }
    return self;
}

#define ccm_future_complete_block_case(type, value) \
case type: \
completeBlock(nil, value); \
break; \

- (instancetype)onComplete:(void(^)())completeBlock {
    CCMDispatchQueue *q = [CCMFutureContext currentContext].dispatchQueue;
    CCMFutureBlockWrapper c = ^(id value) {
        [q async:^{
            if ([value isKindOfClass:[NSError class]]) {
                completeBlock(value, nil);
            } else if (value == [NSNull null]) {
                completeBlock(nil, nil);
            } else if (value != nil) {
                NSMethodSignature *methodSignature = NSMethodSignatureForBlock(completeBlock);
                if (methodSignature  == nil) {
                    completeBlock(nil, value);
                } else {
                    
                    if (methodSignature.numberOfArguments < 3) {
                        completeBlock();
                        return ;
                    }
                    
                    const char type = [methodSignature getArgumentTypeAtIndex:2][0];
                    switch (type) {
                            ccm_future_complete_block_case('@', value)
                            ccm_future_complete_block_case('c', [value charValue])
                            ccm_future_complete_block_case('i', [value intValue])
                            ccm_future_complete_block_case('s', [value shortValue])
                            ccm_future_complete_block_case('l', [value longValue])
                            ccm_future_complete_block_case('q', [value longValue])
                            ccm_future_complete_block_case('C', [value unsignedCharValue])
                            ccm_future_complete_block_case('I', [value unsignedIntValue])
                            ccm_future_complete_block_case('S', [value unsignedShortValue])
                            ccm_future_complete_block_case('L', [value unsignedLongValue])
                            ccm_future_complete_block_case('Q', [value unsignedLongLongValue])
                            ccm_future_complete_block_case('f', [value floatValue])
                            ccm_future_complete_block_case('d', [value doubleValue])
                            ccm_future_complete_block_case('B', [value boolValue])
                        default:
                            break;
                    }
                }
            }
        }];
    };
   
    [self.dispatchQueue sync:^{
        [self.blocks addObject:[c copy]];
    }];
    
    [self tryExecuteCallbacks];
    
    return self;
}

#define ccm_future_success_block_case(type, value) \
case type: \
successBlock(value); \
break; \

- (instancetype)onSuccess:(void(^)())successBlock {
    return [self onComplete:^(NSError *error, id value) {
        if (value) {
            NSMethodSignature *methodSignature = NSMethodSignatureForBlock(successBlock);
            if (methodSignature == nil) {
                successBlock(value);
            } else {
                
                if (methodSignature.numberOfArguments < 2) {
                    successBlock();
                    return ;
                }
                
                const char type = [methodSignature getArgumentTypeAtIndex:1][0];
                switch (type) {
                        ccm_future_success_block_case('@', value)
                        ccm_future_success_block_case('c', [value charValue])
                        ccm_future_success_block_case('i', [value intValue])
                        ccm_future_success_block_case('s', [value shortValue])
                        ccm_future_success_block_case('l', [value longValue])
                        ccm_future_success_block_case('q', [value longValue])
                        ccm_future_success_block_case('C', [value unsignedCharValue])
                        ccm_future_success_block_case('I', [value unsignedIntValue])
                        ccm_future_success_block_case('S', [value unsignedShortValue])
                        ccm_future_success_block_case('L', [value unsignedLongValue])
                        ccm_future_success_block_case('Q', [value unsignedLongLongValue])
                        ccm_future_success_block_case('f', [value floatValue])
                        ccm_future_success_block_case('d', [value doubleValue])
                        ccm_future_success_block_case('B', [value boolValue])
                    default:
                        break;
                }
            }
        }
    }];
}

- (instancetype)onFailure:(void(^)(NSError *))failureBlock {
    return [self onComplete:^(NSError *error, id value) {
        if (error) {
            failureBlock(error);
        }
    }];
}



- (void)tryExecuteCallbacks {
    [self.dispatchQueue async:^{
        if (self.futureValue == nil) return;
        
        __block id value = self.futureValue;
        [self.blocks enumerateObjectsUsingBlock:^(CCMFutureBlockWrapper block, NSUInteger idx, BOOL *stop) {
            block(value);
        }];
       
        [self.blocks removeAllObjects];
    }];
}

@end

@implementation CCMFuture (Private)

- (void)success:(id)value {
    if (self.futureValue != nil) { return; } // process duplicate set future value
    
    [self.dispatchQueue async:^{
        if (self.futureValue != nil) { return; }
        
        if (value == nil) {
            self.futureValue = [NSNull null];
        } else {
            self.futureValue = value;
        }
        [self tryExecuteCallbacks];
    }];
}

- (void)failure:(NSError *)error {
    NSAssert(error != nil, @"error can not be a nil");
    if (self.futureValue != nil) { return; }
    
    [self.dispatchQueue async:^{
        if (self.futureValue != nil) { return; }
        
        self.futureValue = error;
        [self tryExecuteCallbacks];
    }];
}

@end

@implementation CCMDispatchQueue {
    void *queueTag;
}

+ (instancetype)main {
    static CCMDispatchQueue *queue = nil;
    static dispatch_once_t pred;
    
    dispatch_once(&pred, ^{
        queue = [[self alloc] initWithGCDQueue:dispatch_get_main_queue()];
    });
    
    return queue;
}

+ (instancetype)global {
    static CCMDispatchQueue *queue = nil;
    static dispatch_once_t pred;
    
    dispatch_once(&pred, ^{
        queue = [[self alloc] initWithGCDQueue:dispatch_get_global_queue(0, 0)];
    });
    
    return queue;
}

+ (instancetype)serialQueueWithName:(NSString *)name {
    return [[self alloc] initWithName:name serial:YES];
}

+ (instancetype)concurrentQueueWithName:(NSString *)name {
    return [[self alloc] initWithName:name serial:NO];
}


- (instancetype)initWithName:(NSString *)name serial:(BOOL)serial {
    dispatch_queue_attr_t attr = serial ? DISPATCH_QUEUE_SERIAL : DISPATCH_QUEUE_CONCURRENT;
    dispatch_queue_t queue = dispatch_queue_create([name cStringUsingEncoding:NSUTF8StringEncoding], attr);
    
    return [self initWithGCDQueue:queue];
}

- (instancetype)initWithGCDQueue:(dispatch_queue_t)queue {
    if (self = [super init]) {
        _underlyingQueue = queue;
        queueTag = &queueTag;
        dispatch_queue_set_specific(_underlyingQueue, queueTag, queueTag, NULL);
    }
    return self;
}

- (void)sync:(dispatch_block_t)block {
    if ([self isCurrent]) {
        block();
    } else {
        dispatch_sync(self.underlyingQueue, block);
    }
}

- (void)async:(dispatch_block_t)block {
    dispatch_async(self.underlyingQueue, block);
}

- (void)barrierSync:(dispatch_block_t)block {
    if (dispatch_get_specific(queueTag)) {
        block();
    } else {
        dispatch_barrier_sync(self.underlyingQueue, block);
    }
}

- (void)barrierAsync:(dispatch_block_t)block {
    dispatch_barrier_async(self.underlyingQueue, block);
}

- (void)after:(NSTimeInterval)delay block:(dispatch_block_t)block {
    if (block == nil) return;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), self.underlyingQueue, block);
}

- (BOOL)isCurrent {
    if (self.underlyingQueue == dispatch_get_main_queue()  && [NSThread isMainThread])
        return YES;
    
    return dispatch_get_specific(queueTag) != NULL;
}

@end

在上面的代码里有个很基础的概念要清楚:void(^)() 这种定义的 Block,并不是代码没有参数,而是可以指定任何参数。没有参数的 Block 定义是这样的:void(^)(void),所以,上面我们要用NSMethodSignature来解析 Block 的参数个数和类型,这使得我们的客户端代码不用关注值类型的装箱/拆箱(比如 NSInteger 和 NSNumber 的转换)。

为了方便使用到类似泛型的特性,我们还需要定义一个如下的宏:

#define CCMFutureOf(name) id

#define CCMFutureDef(name, type) \
@protocol CCMFutureOf##name  \
@required \
 \
- (instancetype)onComplete:(void(^)(NSError *, type))completeBlock; \
 \
- (instancetype)onSuccess:(void(^)(type))successBlock; \
 \
- (instancetype)onFailure:(void(^)(NSError *))failureBlock; \
 \
@end \

所以可以这样使用咯:

CCMFutureDef(Int, NSInteger)
CCMFutureDef(String, NSString *)

- (CCMFutureOf(Int))requestUserCount {
    CCMPromise *promise = [CCMPromise new];
    
    // 模拟异步
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [promise successWithValue:@10];
    });
    
    return promise.dynamicFuture;
}

...

[[[xxx requestUserCount] onSuccess:^(NSInteger value) {
    // TODO
}] onFailure:^(NSError *error) {
    // TODO
}];

接下来做什么

Promise&Future 很浪漫,很优雅,那么你是否可以考虑将自己网络请求相关的 API 尝试用这种方式来封装呢?

还有,我真的是名诗人,我叫李白。

你可能感兴趣的:(最浪漫的抽象 Promise & Future)