如何利用Objective-C写一个精美的DSL

玄学酱 2017-08-03

objective-c class view

iOS

推荐序:本文是来自美团的 iOS 技术专家臧成威的投稿。臧老师在 StuQ 开完 RactiveCocoa 的两次系列课程后,最近新开了一门 《iOS 实战黑魔法》的新课程,课程内容涉及很多 Objective-C Runtime, Swift 等底层的知识和应用技巧,如果你感兴趣,可以看文末的介绍。

感谢臧成威的授权,以下是文章正文。

背景

在程序开发中,我们总是希望能够更加简洁、更加语义化地去表达自己的逻辑,链式调用是一种常见的处理方式。我们常用的 Masonry、 Expecta 等第三方库就采用了这种处理方式。


  1. // Masonry 
  2. [view1 mas_makeConstraints:^(MASConstraintMaker *make) { 
  3.     make.top.equalTo(superview.mas_top).with.offset(padding.top); 
  4.     make.left.equalTo(superview.mas_left).with.offset(padding.left); 
  5.     make.bottom.equalTo(superview.mas_bottom).with.offset(-padding.bottom); 
  6.     make.right.equalTo(superview.mas_right).with.offset(-padding.right); 
  7. }]; 

  1. // Expecta 
  2. expect(@"foo").to.equal(@"foo"); // `tois a syntactic sugar and can be safely omitted. 
  3. expect(foo).notTo.equal(1); 
  4. expect([bar isBar]).to.equal(YES); 
  5. expect(baz).to.equal(3.14159); 

像这种用于特定领域的表达方式,我们叫做 DSL (Domain Specific Language),本文就介绍一下如何实现一个链式调用的 DSL.

链式调用的实现

我们举一个具体的例子,比如我们用链式表达式来创建一个 UIView,设置其 frame、backgroundColor, 并添加至某个父 View。

对于最基本的 Objective-C (在 iOS4 block 出现之前),如果要实现链式调用,只能是这个样子的:


  1. UIView *aView = [[[[UIView alloc] initWithFrame:aFrame] bgColor:aColor] intoView:aSuperView]; 

有了 block,我们可以把中括号的这种写法改为点语法的形式


  1. UIView *aView = AllocA(UIView).with.position(x, y).size(width, height).bgColor(aColor).intoView(aSuperView); 
  2.  
  3. // 当x和y为默认值0和0或者width和height为默认值0的时候,还可以省略 
  4. UIView *bView = AllocA(UIView).with.size(width, height).bgColor(aColor).intoView(aSuperView); 

可以看出,链式语法的语义性很明确,后者的语法更加紧凑,下面我们从两个角度看一下后者的实现。

1. 从语法层面来看

链式调用可以用两种方式来实现:

1).在返回值中使用属性来保存方法中的信息

比如,Masonry 中的 .left .right .top .bottom 等方法,调用时会返回一个 MASConstraintMaker 类的实例,里面有 left/right/top/bottom 等属性来保存每次调用时的信息;


  1. make.left.equalTo(superview.mas_left).with.offset(15); 

再比如,Expecta 中的方法 .notTo 方法会返回一个 EXPExpect 类的实例,里面有个 BOOL 属性 self.negative 来记录是否调用了 .notTo;


  1. expect(foo).notTo.equal(1); 

再比如,上例中的 .with 方法,我们可以直接 return self;

2).使用 block 类型的属性来接受参数

比如 Masonry 中的 .offset(15) 方法,接收一个 CGFloat 作为参数,可以在 MASConstraintMaker 类中添加一个 block 类型的属性:


  1. @property (nonatomic, copy) MASConstraintMaker* (^offset)(CGFloat); 

比如例子中的 .position(x, y),可以给的某类中添加一个属性:


  1. @property (nonatomic, copy) ViewMaker* (^position)(CGFloat x, CGFloat y); 

在调用 .position(x, y) 方法时,执行这个block,返回 ViewMaker 的实例保证链式调用得以进行。

2. 从语义层面来看

从语义层面上,需要界定哪些是助词,哪些是需要接受参数的。为了保证链式调用能够完成,需要考虑传入什么,返回什么。

还是以上面的例子来讲:


  1. UIView *aView = AllocA(UIView).with.position(x, y).size(width, height).bgColor(aColor).intoView(aSuperView); 

分步来看一下,这个 DSL 表达式需要描述的是一个祈使句,以 Alloc 开始,以 intoView 截止。在 intoView 终结语之前,我们对 UIView 进行一定的修饰,利用 position size bgColor 这些。

下面我们分别从四段来看,如何实现这样一个表达式:

(1) 宾语

在 AllocA(UIView) 的语义中,我们确定了宾语是 a UIVIew。由于确定 UIView 是在 intoView 截止那时,所以我们需要创建一个中间类来保存所有的中间条件,这里我们用 ViewMaker 类。


  1. @interface ViewMaker : NSObject 
  2. @property (nonatomic, strong) Class viewClass; 
  3. @property (nonatomic, assign) CGPoint position; 
  4. @property (nonatomic, assign) CGPoint size
  5. @property (nonatomic, strong) UIColor *color; 
  6. @end 

另外我们可以注意到AllocA是一个函数,而UIView无法直接传递到这个函数中,语法就要变成 AllocA([UIView class]) 而失去了简洁性。所以我们需要先定义一个宏来“吞”掉中括号和 class 这个方法:


  1. #define AllocA(aClass)  alloc_a([aClass class]) 
  2.  
  3. ViewMaker* alloc_a(Class aClass){ 
  4.     ViewMaker *maker = ViewMaker.new; 
  5.     maker.viewClass = aClass; 
  6.     return maker; 

(2) 助词

很多时候,为了让 DSL 的语法看起来更加连贯,我们需要一些助词来帮助,例如 Masonry 里面的 make.top.equalTo(superview.mas_top).with.offset(padding.top) 这句中的 with 就是这样一个助词。

而这个助词和我们学过的语法一样,通常没有什么实际效果,简单返回self就可以。


  1. @interface ViewMaker : NSObject 
  2. @property (nonatomic, strong) Class viewClass; 
  3. @property (nonatomic, assign) CGPoint position; 
  4. @property (nonatomic, assign) CGPoint size
  5. @property (nonatomic, strong) UIColor *color; 
  6. @property (nonatomic, readonly) ViewMaker *with
  7. @end 
  8.  
  9. @implementation ViewMaker 
  10.  
  11. - (ViewMaker *)with 
  12.     return self; 
  13. @end 

需要注意的是,返回自己,就没有办法阻止用户不断调用自己 .with.with.with ,为了避免这种情况,可以新生成一个类,每个类都拥有自己所在层次的方法,避免跃层调用。


  1. @interface ViewMaker : NSObject 
  2. @property (nonatomic, strong) Class viewClass; 
  3. @property (nonatomic, assign) CGPoint position; 
  4. @property (nonatomic, assign) CGPoint size
  5. @property (nonatomic, strong) UIColor *color; 
  6. @end 
  7.  
  8. @interface ViewClassHelper : NSObject 
  9. @property (nonatomic, strong) Class viewClass; 
  10. @property (nonatomic, readonly) ViewMaker *with
  11. @end 
  12.  
  13. #define AllocA(aClass)  alloc_a([aClass class]) 
  14.  
  15. ViewClassHelper* alloc_a(Class aClass){ 
  16.     ViewClassHelper *helper = ViewClassHelper.new; 
  17.     helper.viewClass = aClass; 
  18.     return helper; 
  19. @implementation ViewClassHelper 
  20.  
  21. - (ViewMaker *)with 
  22.     ViewMaker *maker = ViewMaker.new; 
  23.     maker.viewClass = self.viewClass; 
  24.     return maker; 
  25. @end 

这样就有效防止了,.with.with.with这样的语法。但是实际上,我们要根据真实的需要来进行开发,使用 DSL 的用户是为了更好的表达性,所以并不会写出.with.with.with这样的代码,这样的防护性措施就显得有点不必要了。

不过使用类来区分助词还有另外几个小好处,就是它可以确保在语法提示的时候,ViewClassHelper这个类只有.with这样一个语法提示,而ViewMaker不出现.with语法提示;并且同时确保.with一定要出现。

不过为了简化文章,我们都使用前者,既.with返回self来继续下文:


  1. @interface ViewMaker : NSObject 
  2. @property (nonatomic, strong) Class viewClass; 
  3. @property (nonatomic, assign) CGPoint position; 
  4. @property (nonatomic, assign) CGPoint size
  5. @property (nonatomic, strong) UIColor *color; 
  6. @property (nonatomic, readonly) ViewMaker *with
  7. @end 
  8.  
  9. @implementation ViewMaker 
  10.  
  11. - (ViewMaker *)with 
  12.     return self; 
  13. @end 

(3) 修饰部分——定语

像例子中的position size bgColor这些都是定语部分,用来修饰UIView,他们以属性的形势存在于ViewMaker的实例中,为了支持链式表达,所以实现的时候,都会继续返回self。

我们来试着实现下:


  1. @interface ViewMaker : NSObject 
  2. // ... 
  3. @property (nonatomic, copy) ViewMaker* (^position)(CGFloat x, CGFloat y); 
  4. @property (nonatomic, copy) ViewMaker* (^size)(CGFloat x, CGFloat y); 
  5. @property (nonatomic, copy) ViewMaker* (^bgColor)(UIColor *color); 
  6. @end 
  7.  
  8. @implementation ViewMaker 
  9.  
  10. - (instancetype)init 
  11.     if (self = [super init]) { 
  12.         @weakify(self) 
  13.         _position = ^ViewMaker *(CGFloat x, CGFloat y) { 
  14.             @strongify(self) 
  15.             self.position = CGPointMake(x, y); 
  16.         }; 
  17.         _size = ^ViewMaker *(CGFloat x, CGFloat y) { 
  18.             @strongify(self) 
  19.             self.size = CGPointMake(x, y); 
  20.         }; 
  21.         _bgColor = ^ViewMaker *(UIColor *color) { 
  22.             @strongify(self) 
  23.             self.color = color; 
  24.         }; 
  25.     } 
  26.     return self; 
  27. @end 

(4) 终结词

“终结词”这个实在是在现代语法里面找不到对应关系了,但是在 DSL 中,这一段尤为重要。ViewMaker的实例从头至尾收集了很多的修饰,需要最后的一个表达词语来产生最后的结果,这里就称为”终结词”。例如在 Expecta 这个开源库里面的 equal 就是把真正的行为表现出来的时候,to 和 notTo 都不会真正触发行为。

在我们的例子里,终结词.intoView(aSuperViwe)可以这样实现:


  1. @interface ViewMaker : NSObject 
  2. // ... 
  3. @property (nonatomic, copy) UIView* (^intoView)(UIView *superView); 
  4. @end 
  5.  
  6. @implementation ViewMaker 
  7.  
  8. - (instancetype)init 
  9.     if (self = [super init]) { 
  10.         @weakify(self) 
  11.         // ... 
  12.         _intoView = ^UIView *(UIView *superView) { 
  13.             @strongify(self) 
  14.             CGRect rect = CGRectMake(self.position.x, self.position.y, 
  15.                          self.size.width, self.size.height); 
  16.             UIView *view = [[UIView alloc] initWithFrame:rect]; 
  17.             view.backgroundColor = self.color; 
  18.             [superView addSubView:view]; 
  19.             return view
  20.         }; 
  21.     } 
  22.     return self; 
  23. @end 

这样,一个终结词就写好了。

最终代码的汇总:


  1. @interface ViewMaker : NSObject 
  2. @property (nonatomic, strong) Class viewClass; 
  3. @property (nonatomic, assign) CGPoint position; 
  4. @property (nonatomic, assign) CGPoint size
  5. @property (nonatomic, strong) UIColor *color; 
  6. @property (nonatomic, readonly) ViewMaker *with
  7. @property (nonatomic, copy) ViewMaker* (^position)(CGFloat x, CGFloat y); 
  8. @property (nonatomic, copy) ViewMaker* (^size)(CGFloat x, CGFloat y); 
  9. @property (nonatomic, copy) ViewMaker* (^bgColor)(UIColor *color); 
  10. @property (nonatomic, copy) UIView* (^intoView)(UIView *superView); 
  11. @end 
  12.  
  13. @implementation ViewMaker 
  14.  
  15. - (instancetype)init 
  16.     if (self = [super init]) { 
  17.         @weakify(self) 
  18.         _position = ^ViewMaker *(CGFloat x, CGFloat y) { 
  19.             @strongify(self) 
  20.             self.position = CGPointMake(x, y); 
  21.         }; 
  22.         _size = ^ViewMaker *(CGFloat x, CGFloat y) { 
  23.             @strongify(self) 
  24.             self.size = CGPointMake(x, y); 
  25.         }; 
  26.         _bgColor = ^ViewMaker *(UIColor *color) { 
  27.             @strongify(self) 
  28.             self.color = color; 
  29.         }; 
  30.         _intoView = ^UIView *(UIView *superView) { 
  31.             @strongify(self) 
  32.             CGRect rect = CGRectMake(self.position.x, self.position.y, 
  33.                          self.size.width, self.size.height); 
  34.             UIView *view = [[UIView alloc] initWithFrame:rect]; 
  35.             view.backgroundColor = self.color; 
  36.             [superView addSubView:view]; 
  37.             return view
  38.         }; 
  39.     } 
  40.     return self; 
  41.  
  42. - (ViewMaker *)with 
  43.     return self; 
  44. @end 

总结

这种链式调用能够使程序更加清晰,在特定场景下使程序的可读性更强。这种手段在Swift也是相同道理,大家可以善加利用,让自己的代码更加美观。

其实,iOS 开发者要想不断精进,成长为真正的大牛高手,必须将自己的视野凌驾于业务需求之上,精简强化核心技能,提升自己对语言和工具的掌握层次,才能提高开发效率,提升技能水平。

这里为你准备了更多好玩的,让你事半功倍的 iOS 高阶黑魔法攻防术,斯达克学院(StuQ ) 特别邀请备受学员喜爱的资深 iOS 技术专家臧成威老师开设《 iOS 实战黑魔法 》课程,6周12小时高效 Get iOS 必须掌握的高阶黑魔法攻防术,让你从普通的开发者中渐渐走出来,看到一个不一样的语言,感受不一样的开发!




作者:臧成威
来源:51CTO
登录 后评论
下一篇
corcosa
14478人浏览
2019-10-08
相关推荐
你与高薪仅差一本Kotlin
2061人浏览
2017-09-01 08:44:10
领域专用语言(DSL)
2042人浏览
2016-03-09 17:02:00
0
0
0
586