Objective-C實(shí)現(xiàn)鏈?zhǔn)骄幊陶Z法(DSL)

您越著急開始寫代碼,代碼就會花費(fèi)越長的時(shí)間农猬。 - Carlson, University of Wisconsin

前言

熟悉Objective-C這一門編程語言的人都知道乃沙,Objective-C中方法的調(diào)用都是通過中括號[]實(shí)現(xiàn)的宣渗。比如[self.view addSubview:xxxView];如果想要在一個(gè)對象上連續(xù)調(diào)用多個(gè)方法熔掺,就要使用多組中括號嵌套(當(dāng)然要保證每個(gè)方法都能把該對象作為返回值return)释移。比如[[[UILabel alloc] init] setText:@"xxx"];捣郊。這對于有其他編程語言經(jīng)驗(yàn)的開發(fā)者而言辽狈,Objective-C無異于就是眾多語言中的一朵奇葩。因?yàn)槠渌鄶?shù)的高級語言方法調(diào)用都是以點(diǎn)語法.的形式實(shí)現(xiàn)的呛牲。好在Objective-C在iOS4.0之后推出了block這個(gè)語法(相當(dāng)于其他語言中的匿名函數(shù))刮萌。我們可以利用block的來實(shí)現(xiàn)Objective-C方法的鏈?zhǔn)秸{(diào)用。像這種用于特定領(lǐng)域的表達(dá)方式娘扩,我們叫做 DSL (Domain Specific Language)着茸,本文就介紹一下如何讓Objective-C實(shí)現(xiàn)鏈?zhǔn)秸{(diào)用,其最終調(diào)用方式如下:

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

很明顯琐旁,相比較傳統(tǒng)的Objective-C的方法調(diào)用方式涮阔,使用點(diǎn)語法進(jìn)行方法調(diào)用更加簡潔連貫、一氣呵成灰殴。
不難看出敬特,這種點(diǎn)語法連續(xù)調(diào)用的方式,需要保證每次調(diào)用都能返回對象本身,這樣鏈?zhǔn)秸{(diào)用才得以繼續(xù)伟阔,并且在必要的時(shí)候還可以傳入?yún)?shù)辣之,比如上例中的“ws”、“27”皱炉、“beijing”怀估。
而至于為什么使用block來實(shí)現(xiàn)DSL鏈?zhǔn)秸{(diào)用語法?正是因?yàn)閎lock完全符合構(gòu)造鏈?zhǔn)秸{(diào)用的要求:既可以接收參數(shù)合搅,又可以有返回值奏夫。
不喜歡讀文章的可以直接看代碼

鏈?zhǔn)秸{(diào)用的實(shí)現(xiàn)

現(xiàn)在要給系統(tǒng)原生的類增擴(kuò)展鏈?zhǔn)秸{(diào)用語法历筝。比如給UIView的frame酗昼、backgroundColor增加鏈?zhǔn)秸{(diào)用,目前能想到的有以下兩種實(shí)現(xiàn)方式梳猪。

  1. 第一種方式是使用category給UIView類擴(kuò)展一些方法麻削,每個(gè)方法的返回值都是一個(gè)block,block的參數(shù)是要給UIView對象的屬性設(shè)置的值(比如frame)春弥,block的返回值是一個(gè)UIView對象呛哟。block接收到傳入的參數(shù)后,會對view對象的響應(yīng)屬性進(jìn)行賦值匿沛,然后把view對象作為返回值返回扫责。開發(fā)者想使用鏈?zhǔn)秸{(diào)用,必須要調(diào)用category中的方法逃呼。
  2. **第二種方式是為我們要支持鏈?zhǔn)秸{(diào)用的系統(tǒng)類(比如UIView類)增加一個(gè)中間類(比如叫做DSLViewMaker)鳖孤,DSLViewMaker對象內(nèi)部持有一個(gè)UIView對象,然后DSLViewMaker會聲明并實(shí)現(xiàn)一些和UIView同名的方法抡笼。和方式一一樣苏揣,每個(gè)方法的返回值也是一個(gè)block,block的參數(shù)是要給UIView對象的屬性設(shè)置的值推姻,block的返回值是這個(gè)UIView對象**平匈。然后在合適的時(shí)候把這個(gè)view對象返回給調(diào)用者。

下面針對于兩種實(shí)現(xiàn)方式分別說明藏古。

category方式實(shí)現(xiàn)

/// category 頭文件
@interface UIView (DSL)
- (UIView* (^)(CGRect))DSL_frame;
- (UIView* (^)(UIColor *))DSL_backgroundColor;
@end
/// category 實(shí)現(xiàn)文件
#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
/// 客戶端調(diào)用
/// 客戶端調(diào)用category指定的帶有“DSL_”前綴的方法
UIView *view = UIView.new.DSL_frame(CGRectMake(0, 0, 100, 250)).DSL_backgroundColor([UIColor orangeColor]);

那么問題來了增炭,現(xiàn)在要給UIImageView的一些方法和屬性增加DSL的鏈?zhǔn)秸{(diào)用語法。因?yàn)閁IImageView繼承自UIView拧晕,這就代表UIImageView還要擁有UIView的DSL_frame方法和DSL_backgroundColor方法隙姿。經(jīng)過簡單的實(shí)現(xiàn),大致如下:

/// 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;
    };
}

/// 此處省略...防症,請自行腦補(bǔ)...

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

基于以上代碼孟辑,然后進(jìn)行編譯哎甲,編譯器會報(bào)以下錯(cuò)誤:

報(bào)錯(cuò)

DSL_image這個(gè)東西在UIView中找不到,為什么是UIView呢饲嗽?明明我們創(chuàng)建的是一個(gè)UIImageView炭玫。原因很簡單,因?yàn)槲覀兊腄SL_frame是在UIView的category中聲明并實(shí)現(xiàn)的貌虾,更要命的是吞加,UIView(DSL)中聲明的DSL_frame這個(gè)方法返回的block的返回值是一個(gè)UIView對象,UIView對象當(dāng)然沒有DSL_image方法尽狠。當(dāng)DSL_frame返回的block返回了一個(gè)UIView類型的對象后衔憨,這個(gè)imageView就會被當(dāng)成UIView使用,后面所有對UIImageView的方法的調(diào)用都不會成功袄膏,UIView(DSL)聲明的方法如下:

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

針對于這個(gè)問題,目前筆者只想到一種解決方法:把在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實(shí)現(xiàn)文件中不需要再實(shí)現(xiàn)DSL_frame和DSL_backgroundColor這兩個(gè)方法锌奴,因?yàn)橐呀?jīng)在UIView(DSL).m中實(shí)現(xiàn)過兽狭。只需要消除對應(yīng)的警告即可。

綜上鹿蜀,通過category的方式實(shí)現(xiàn)鏈?zhǔn)秸{(diào)用好處在于每次調(diào)用都會返回對象本身箕慧,缺點(diǎn)在于category中的方法不能和系統(tǒng)的方法重名,因此筆者在這里使用了一個(gè)前綴DSL_來進(jìn)行區(qū)分耻姥。而中間類方式實(shí)現(xiàn)鏈?zhǔn)秸{(diào)用就可以避免前綴的問題销钝。

中間類方式實(shí)現(xiàn)

上面已經(jīng)說過有咨,使用category的方式給類擴(kuò)展鏈?zhǔn)秸{(diào)用的方法琐簇,我們必須要和原生的方法進(jìn)行區(qū)分(比如增加前綴)。這樣的缺點(diǎn)在于開發(fā)者開發(fā)者鏈?zhǔn)秸{(diào)用的時(shí)候還必須要時(shí)刻謹(jǐn)記調(diào)用指定前綴的方法座享,使用起來不是很友好婉商。
所以,還有另一種方法渣叛,我們可以使用一個(gè)中間類丈秩,中間類持有一個(gè)UIView對象,給這個(gè)中間類增加和UIView同名的方法淳衙,通過調(diào)用這個(gè)中間類的方法來間接調(diào)用UIView對象的方法蘑秽。具體實(shí)現(xiàn)如下:

/// 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
/// 客戶端調(diào)用
    UIView *view = alloc_view().frame(CGRectMake(0, 20, 100, 100)).backgroundColor([UIColor redColor]).view;
    [self.view addSubview:view];

看完上面的代碼饺著,你可能會有幾個(gè)疑惑:

  1. 為什么客戶端進(jìn)行鏈?zhǔn)秸{(diào)用是以一個(gè)函數(shù)開頭的?
  2. 為什么最后要使用一個(gè).view來返回我們創(chuàng)建的view肠牲?

針對于第一個(gè)問題幼衰,我們是以一個(gè)中間類DSLViewMaker來創(chuàng)建了一個(gè)view,然后鏈?zhǔn)秸{(diào)用DSLViewMaker的對象方法對這個(gè)view進(jìn)行配置缀雳。為了不讓外部調(diào)用的客戶端感知到DSLViewMaker的存在渡嚣,所有使用了一個(gè)函數(shù)直接返回一個(gè)DSLViewMaker對象。

針對于第二個(gè)問題肥印,還是因?yàn)橹虚g類识椰,因?yàn)殒準(zhǔn)秸{(diào)用要保證每次都要返回鏈?zhǔn)秸{(diào)用的對象(這里是指的maker對象),而客戶端無法拿到maker配置好的view深碱,為了讓客戶端能夠獲取鏈?zhǔn)秸{(diào)用配置好的view對象腹鹉,所以暴露了一個(gè)view方法供外部調(diào)用。

如果你覺得使用函數(shù)作為鏈?zhǔn)秸{(diào)用的開頭不夠面向?qū)ο蠓蠊琛D敲催€可以給UIView增加一個(gè)如下分類:

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

@class DSLViewMaker;

@interface UIView (DSLMaker)
+ (DSLViewMaker *)make;
@end
/// category實(shí)現(xiàn)文件
#import "UIView+DSLMaker.h"
#import "DSLViewMaker.h"

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

@end

然后客戶端的調(diào)用就變成了這樣:

    
//    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];

總結(jié)

綜上上沐,Objective-C語言實(shí)現(xiàn)鏈?zhǔn)秸Z法可以有兩種形式荆隘,但最終都是使用block實(shí)現(xiàn)的。使用category實(shí)現(xiàn)鏈?zhǔn)秸Z法,需要加前綴侣诺。使用中間類來實(shí)現(xiàn)鏈?zhǔn)秸Z法,需要有一個(gè)特定的方法返回被配置的對象诅岩。兩種方式各有利弊声离。
最后附上代碼地址

文/VV木公子(簡書作者)
PS:如非特別說明锉走,所有文章均為原創(chuàng)作品滨彻,著作權(quán)歸作者所有,轉(zhuǎn)載請聯(lián)系作者獲得授權(quán)挪蹭,并注明出處亭饵。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市梁厉,隨后出現(xiàn)的幾起案子辜羊,更是在濱河造成了極大的恐慌,老刑警劉巖词顾,帶你破解...
    沈念sama閱讀 216,744評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件八秃,死亡現(xiàn)場離奇詭異,居然都是意外死亡肉盹,警方通過查閱死者的電腦和手機(jī)昔驱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,505評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來上忍,“玉大人骤肛,你說我怎么就攤上這事纳本。” “怎么了腋颠?”我有些...
    開封第一講書人閱讀 163,105評論 0 353
  • 文/不壞的土叔 我叫張陵饮醇,是天一觀的道長。 經(jīng)常有香客問我秕豫,道長朴艰,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,242評論 1 292
  • 正文 為了忘掉前任混移,我火速辦了婚禮祠墅,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘歌径。我一直安慰自己毁嗦,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,269評論 6 389
  • 文/花漫 我一把揭開白布回铛。 她就那樣靜靜地躺著狗准,像睡著了一般。 火紅的嫁衣襯著肌膚如雪茵肃。 梳的紋絲不亂的頭發(fā)上腔长,一...
    開封第一講書人閱讀 51,215評論 1 299
  • 那天,我揣著相機(jī)與錄音验残,去河邊找鬼捞附。 笑死,一個(gè)胖子當(dāng)著我的面吹牛您没,可吹牛的內(nèi)容都是我干的鸟召。 我是一名探鬼主播,決...
    沈念sama閱讀 40,096評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼氨鹏,長吁一口氣:“原來是場噩夢啊……” “哼欧募!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起仆抵,我...
    開封第一講書人閱讀 38,939評論 0 274
  • 序言:老撾萬榮一對情侶失蹤跟继,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后肢础,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體还栓,經(jīng)...
    沈念sama閱讀 45,354評論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,573評論 2 333
  • 正文 我和宋清朗相戀三年传轰,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片谷婆。...
    茶點(diǎn)故事閱讀 39,745評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡慨蛙,死狀恐怖辽聊,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情期贫,我是刑警寧澤跟匆,帶...
    沈念sama閱讀 35,448評論 5 344
  • 正文 年R本政府宣布,位于F島的核電站通砍,受9級特大地震影響玛臂,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜封孙,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,048評論 3 327
  • 文/蒙蒙 一迹冤、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧虎忌,春花似錦泡徙、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,683評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至挑围,卻和暖如春礁竞,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背杉辙。 一陣腳步聲響...
    開封第一講書人閱讀 32,838評論 1 269
  • 我被黑心中介騙來泰國打工苏章, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人奏瞬。 一個(gè)月前我還...
    沈念sama閱讀 47,776評論 2 369
  • 正文 我出身青樓枫绅,卻偏偏與公主長得像,于是被迫代替她去往敵國和親硼端。 傳聞我的和親對象是個(gè)殘疾皇子并淋,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,652評論 2 354