Objective-C实现链式编程语法(DSL)

您越着急开始写代码,代码就会花费越长的时间。 - Carlson, University of Wisconsin

前言

熟悉Objective-C这一门编程语言的人都知道,Objective-C中方法的调用都是通过中括号[]实现的。比如[self.view addSubview:xxxView];如果想要在一个对象上连续调用多个方法,就要使用多组中括号嵌套(当然要保证每个方法都能把该对象作为返回值return)。比如[[[UILabel alloc] init] setText:@"xxx"];。这对于有其他编程语言经验的开发者而言,Objective-C无异于就是众多语言中的一朵奇葩。因为其他多数的高级语言方法调用都是以点语法.的形式实现的。好在Objective-C在iOS4.0之后推出了block这个语法(相当于其他语言中的匿名函数)。我们可以利用block的来实现Objective-C方法的链式调用。像这种用于特定领域的表达方式,我们叫做 DSL (Domain Specific Language),本文就介绍一下如何让Objective-C实现链式调用,其最终调用方式如下:

DSLObject *obj = DSLObject.new.name(@"ws").age(27).address(@"beijing");

很明显,相比较传统的Objective-C的方法调用方式,使用点语法进行方法调用更加简洁连贯、一气呵成。
不难看出,这种点语法连续调用的方式,需要保证每次调用都能返回对象本身,这样链式调用才得以继续,并且在必要的时候还可以传入参数,比如上例中的“ws”、“27”、“beijing”。
而至于为什么使用block来实现DSL链式调用语法?正是因为block完全符合构造链式调用的要求:既可以接收参数,又可以有返回值。
不喜欢读文章的可以直接看代码

链式调用的实现

现在要给系统原生的类增扩展链式调用语法。比如给UIView的frame、backgroundColor增加链式调用,目前能想到的有以下两种实现方式。

  1. 第一种方式是使用category给UIView类扩展一些方法,每个方法的返回值都是一个block,block的参数是要给UIView对象的属性设置的值(比如frame),block的返回值是一个UIView对象。block接收到传入的参数后,会对view对象的响应属性进行赋值,然后把view对象作为返回值返回。开发者想使用链式调用,必须要调用category中的方法
  2. **第二种方式是为我们要支持链式调用的系统类(比如UIView类)增加一个中间类(比如叫做DSLViewMaker),DSLViewMaker对象内部持有一个UIView对象,然后DSLViewMaker会声明并实现一些和UIView同名的方法。和方式一一样,每个方法的返回值也是一个block,block的参数是要给UIView对象的属性设置的值,block的返回值是这个UIView对象**。然后在合适的时候把这个view对象返回给调用者。

下面针对于两种实现方式分别说明。

category方式实现

/// category 头文件
@interface UIView (DSL)
- (UIView* (^)(CGRect))DSL_frame;
- (UIView* (^)(UIColor *))DSL_backgroundColor;
@end
/// category 实现文件
#import "UIView+DSL.h"
#define weak_Self __weak typeof(self) weakSelf = self
#define strong_Self __strong typeof((weakSelf)) strongSelf = (weakSelf)

@implementation UIView (DSL)
- (UIView *(^)(CGRect))DSL_frame {
    weak_Self;
    return ^UIView* (CGRect frame) {
        strong_Self;
        strongSelf.frame = frame;
        return strongSelf;
    };
}

- (UIView *(^)(UIColor *))DSL_backgroundColor {
    weak_Self;
    return ^UIView* (UIColor *backgroundColor) {
        strong_Self;
        strongSelf.backgroundColor = backgroundColor;
        return strongSelf;
    };
}
@end
/// 客户端调用
/// 客户端调用category指定的带有“DSL_”前缀的方法
UIView *view = UIView.new.DSL_frame(CGRectMake(0, 0, 100, 250)).DSL_backgroundColor([UIColor orangeColor]);

那么问题来了,现在要给UIImageView的一些方法和属性增加DSL的链式调用语法。因为UIImageView继承自UIView,这就代表UIImageView还要拥有UIView的DSL_frame方法和DSL_backgroundColor方法。经过简单的实现,大致如下:

/// UIImageView category的头文件
@interface UIImageView (DSL)

- (UIImageView* (^)(UIImage *))DSL_image;
- (UIImageView* (^)(UIImage *))DSL_HighlightedImage;
- (UIImageView* (^)(BOOL))DSL_UserInteractionEnabled;
- (UIImageView* (^)(BOOL))DSL_highlighted;
- (UIImageView* (^)(NSArray <UIImage *> *))DSL_AnimationImages;
- (UIImageView* (^)(NSArray <UIImage *> *))DSL_HighlightedAnimationImages;
- (UIImageView* (^)(NSTimeInterval))DSL_AnimationDuration;
- (UIImageView* (^)(NSInteger))SDL_AnimationRepeatCount;
- (UIImageView* (^)(UIColor *))DSL_TintColor;

@end
#import "UIImageView+DSL.h"
#define weak_Self __weak typeof(self) weakSelf = self
#define strong_Self __strong typeof((weakSelf)) strongSelf = (weakSelf)

@implementation UIImageView (DSL)
- (UIImageView* (^)(UIImage *))DSL_image {
    weak_Self;
    return ^UIImageView *(UIImage *image) {
        strong_Self;
        strongSelf.image = image;
        return strongSelf;
    };
}
- (UIImageView* (^)(UIImage *))DSL_HighlightedImage {
    weak_Self;
    return ^UIImageView *(UIImage *highlightedImage) {
        strong_Self;
        strongSelf.highlightedImage = highlightedImage;
        return self;
    };
}
- (UIImageView* (^)(BOOL))DSL_UserInteractionEnabled {
    weak_Self;
    return ^UIImageView *(BOOL userInteractionEnabled) {
        strong_Self;
        strongSelf.userInteractionEnabled = userInteractionEnabled;
        return strongSelf;
    };
}

/// 此处省略...,请自行脑补...

@end
/// 客户端调用
UIImageView *imageView = UIImageView.new
.DSL_frame(CGRectMake(100, 100, 100, 60))
.DSL_image([UIImage imageNamed:@"imgxxx"]);

基于以上代码,然后进行编译,编译器会报以下错误:

报错

DSL_image这个东西在UIView中找不到,为什么是UIView呢?明明我们创建的是一个UIImageView。原因很简单,因为我们的DSL_frame是在UIView的category中声明并实现的,更要命的是,UIView(DSL)中声明的DSL_frame这个方法返回的block的返回值是一个UIView对象,UIView对象当然没有DSL_image方法。当DSL_frame返回的block返回了一个UIView类型的对象后,这个imageView就会被当成UIView使用,后面所有对UIImageView的方法的调用都不会成功,UIView(DSL)声明的方法如下:

 - (UIView* (^)(CGRect))DSL_frame;,

针对于这个问题,目前笔者只想到一种解决方法:把在UIView(DSL)中声明的方法拷贝一份到UIImageView(DSL).h中,并修改block的返回值类型为UIImageView。最终的UIImageView(DSL)头文件 如下:

@interface UIImageView (DSL)
#pragma mark - UIView
/// 这些是在UIView(DSL)中拷贝过来的方法,不同的是,需要修改block的返回值类型为UIImageView,而不是原来的UIView,如下所示:
- (UIImageView* (^)(CGRect))DSL_frame;
- (UIImageView* (^)(UIColor *))DSL_backgroundColor;

#pragma mark - UIImageView
- (UIImageView* (^)(UIImage *))DSL_image;
- (UIImageView* (^)(UIImage *))DSL_HighlightedImage;
- (UIImageView* (^)(BOOL))DSL_UserInteractionEnabled;
- (UIImageView* (^)(BOOL))DSL_highlighted;
- (UIImageView* (^)(NSArray <UIImage *> *))DSL_AnimationImages;
- (UIImageView* (^)(NSArray <UIImage *> *))DSL_HighlightedAnimationImages;
- (UIImageView* (^)(NSTimeInterval))DSL_AnimationDuration;
- (UIImageView* (^)(NSInteger))SDL_AnimationRepeatCount;
- (UIImageView* (^)(UIColor *))DSL_TintColor;
@end

而UIImageView(DSL).m实现文件中不需要再实现DSL_frame和DSL_backgroundColor这两个方法,因为已经在UIView(DSL).m中实现过。只需要消除对应的警告即可。

综上,通过category的方式实现链式调用好处在于每次调用都会返回对象本身,缺点在于category中的方法不能和系统的方法重名,因此笔者在这里使用了一个前缀DSL_来进行区分。而中间类方式实现链式调用就可以避免前缀的问题。

中间类方式实现

上面已经说过,使用category的方式给类扩展链式调用的方法,我们必须要和原生的方法进行区分(比如增加前缀)。这样的缺点在于开发者开发者链式调用的时候还必须要时刻谨记调用指定前缀的方法,使用起来不是很友好。
所以,还有另一种方法,我们可以使用一个中间类,中间类持有一个UIView对象,给这个中间类增加和UIView同名的方法,通过调用这个中间类的方法来间接调用UIView对象的方法。具体实现如下:

/// DSLViewMaker.h文件

@interface DSLViewMaker : NSObject
DSLViewMaker *alloc_view(void);

/// 一些和UIView同名的方法
- (DSLViewMaker *(^)(CGRect))frame;
- (DSLViewMaker *(^)(UIColor *))backgroundColor;
/// 返回DSLViewMaker配置的对象
- (id)view;

@end
/// DSLViewMaker.m文件

#import "DSLViewMaker.h"
#define weak_Self __weak typeof(self) weakSelf = self
#define strong_Self __strong typeof((weakSelf)) strongSelf = (weakSelf)

@interface DSLViewMaker()
@property(nonatomic, strong) UIView *view;
@end

DSLViewMaker *alloc_view(void) {
    return DSLViewMaker.new;
}

@implementation DSLViewMaker
- (instancetype)init {
    if (self = [super init]) {
        _view = [UIView new];
    }
    return self;
}

- (DSLViewMaker *(^)(CGRect))frame {
    weak_Self;
    return ^DSLViewMaker *(CGRect frame) {
        strong_Self;
        strongSelf.view.frame = frame;
        return strongSelf;
    };
}

- (DSLViewMaker *(^)(UIColor *))backgroundColor {
    weak_Self;
    return ^DSLViewMaker *(UIColor *backgroundColor) {
        strong_Self;
        strongSelf.view.backgroundColor = backgroundColor;
        return strongSelf;
    };
}

- (id)view {
    return _view;
}
@end
/// 客户端调用
    UIView *view = alloc_view().frame(CGRectMake(0, 20, 100, 100)).backgroundColor([UIColor redColor]).view;
    [self.view addSubview:view];

看完上面的代码,你可能会有几个疑惑:

  1. 为什么客户端进行链式调用是以一个函数开头的?
  2. 为什么最后要使用一个.view来返回我们创建的view?

针对于第一个问题,我们是以一个中间类DSLViewMaker来创建了一个view,然后链式调用DSLViewMaker的对象方法对这个view进行配置。为了不让外部调用的客户端感知到DSLViewMaker的存在,所有使用了一个函数直接返回一个DSLViewMaker对象。

针对于第二个问题,还是因为中间类,因为链式调用要保证每次都要返回链式调用的对象(这里是指的maker对象),而客户端无法拿到maker配置好的view,为了让客户端能够获取链式调用配置好的view对象,所以暴露了一个view方法供外部调用。

如果你觉得使用函数作为链式调用的开头不够面向对象。那么还可以给UIView增加一个如下分类:

/// category头文件
#import <UIKit/UIKit.h>

@class DSLViewMaker;

@interface UIView (DSLMaker)
+ (DSLViewMaker *)make;
@end
/// category实现文件
#import "UIView+DSLMaker.h"
#import "DSLViewMaker.h"

@implementation UIView (DSLMaker)
+ (DSLViewMaker *)make {
    return [DSLViewMaker new];
}

@end

然后客户端的调用就变成了这样:

    
//    UIView *view = alloc_view().frame(CGRectMake(0, 20, 100, 100)).backgroundColor([UIColor redColor]).view;
//    [self.view addSubview:view];

UIView *view = UIView.make.frame(CGRectMake(0, 20, 100, 100)).backgroundColor([UIColor redColor]).view;
[self.view addSubview:view];

总结

综上,Objective-C语言实现链式语法可以有两种形式,但最终都是使用block实现的。使用category实现链式语法,需要加前缀。使用中间类来实现链式语法,需要有一个特定的方法返回被配置的对象。两种方式各有利弊。
最后附上代码地址

文/VV木公子(简书作者)
PS:如非特别说明,所有文章均为原创作品,著作权归作者所有,转载请联系作者获得授权,并注明出处。

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