美文网首页架构师之路iOS开发技术分享iOS Developer
Objective-C实现链式编程语法(DSL)

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

作者: VV木公子 | 来源:发表于2018-08-01 11:42 被阅读181次

    您越着急开始写代码,代码就会花费越长的时间。 - 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:如非特别说明,所有文章均为原创作品,著作权归作者所有,转载请联系作者获得授权,并注明出处。

    相关文章

      网友评论

      • 新地球说着一口陌生腔调:666 我语法都没写过
      • terryl1n:在cocoachina上看到,特地登录上来指导一下楼主。
        `UIView(DSL)中声明的DSL_frame这个方法返回的block的返回值是一个UIView对象,UIView对象当然没有DSL_image方法`

        这里要把返回值声明为( __kindof UIView*) 就可以了
        VV木公子:@terryl1n __kindof不行,instancetype 肯定也不行呀
        terryl1n:@VV木公子 不好意思,看错关系了。返回值改成instancetype就可以了
        VV木公子:__kindof应该没有什么用,解决不了这个问题
      • 小县城公务员:你好!我们是程序员大咖旗下专注于程序员生态的公众号程序员大咖(微信号 CodePush)。我们很赞赏你的文章,希望能获得转载授权。授权后,你的文章将会在公众号程序员大咖、程序员共读、源码共读、Java编程精选、iOS开发等渠道发布。我们会注明来源和作者姓名。
        非常感谢~~~
        小县城公务员:@VV木公子 十分感谢~
        VV木公子:@源码共读 好的
      • 愤怒的小懒懒:现在链式是主流吗?看到好多大神都写这块,对一些基础控件进行二次封装成链式调用
        VV木公子:@愤怒的小懒懒 个人感觉,明白这是怎么回事就行了,视情况而定。对于自己封装的库可以使用链式调用。对于其他的第三方库,还是保持原样好一点

      本文标题:Objective-C实现链式编程语法(DSL)

      本文链接:https://www.haomeiwen.com/subject/foycvftx.html