当前位置: 首页 > 科技观察

如何用Objective-C写出精美的DSL

时间:2023-03-20 21:25:47 科技观察

推荐序言:本文由美团点评iOS技术专家臧成伟投稿。藏老师在StuQ上完RactiveCocoa的两个系列课程后,最近又开了一门新课程《iOS 实战黑魔法》。课程内容涉及Objective-CRuntime、Swift等大量底层知识和应用技巧。有兴趣的可以看文末的介绍。感谢臧成伟授权,以下为文章正文。背景在程序开发中,我们总是希望能够更加简洁和语义化地表达我们的逻辑,链式调用是一种常见的处理方式。我们常用的第三方库如Masonry、Expecta等都是采用这种处理方式。//砌体[view1mas_makeConstraints:^(MASConstraintMaker*make){make.top.equalTo(superview.mas_top).with.offset(padding.top);make.left.equalTo(superview.mas_left).with.offset(padding.left);make.bottom.equalTo(superview.mas_bottom).with.offset(-padding.bottom);make.right.equalTo(superview.mas_right).with.offset(-padding.right);}];//Expectaexpect(@"foo").to.equal(@"foo");//`to`是一个语法糖和canbesafelyomitted.expect(foo).notTo.equal(1);expect([barisBar]).to.equal(YES);期望(baz).to.equal(3.14159);这种在特定领域中使用的表达方式称为DSL(DomainSpecificLanguage)。本文介绍如何实现链式DSL。链式调用的实现下面举个具体的例子。例如,我们使用链式表达式创建一个UIView,设置它的frame和backgroundColor,并将它添加到父View。对于最基本的Objective-C(在iOS4block出现之前),如果要实现链式调用,只能这样:UIView*aView=[[[[UIViewalloc]initWithFrame:aFrame]bgColor:aColor]intoView:aSuperView];有了block,我们可以把方括号的写法改成点语法的形式UIView*aView=AllocA(UIView).with.position(x,y).size(width,height).bgColor(aColor).intoView(aSuperView);//当x和y默认值为0和0或者width和height默认值为0时,UIView*bView=AllocA(UIView).with.size(width,height)也可以省略.bgColor(aColor).intoView(aSuperView);可以看出链式语法的语义非常清晰,后者的语法更加紧凑。我们从两个角度来看后者的实现。1、从语法层面,链式调用可以通过两种方式实现:1)。使用返回值中的属性来保存方法中的信息。例如Masonry中的.left.right.top.bottom等方法,call会返回一个MASConstraintMaker类的实例,该实例有left/right/top/bottom等属性保存每次调用的信息;make.left.equalTo(superview.mas_left).with.offset(15);再比如,Expecta中的方法.notTo会返回一个EXPExpect类的实例,它有一个BOOL属性self.negative来记录是否.notTo;expect(foo).notTo.equal(1);.with方法,我们可以直接returnself;2).使用块类型属性接受参数比如Masonry中的.offset(15)方法,接收一个CGFloat作为参数,可以在MASConstraintMaker类中添加块类型属性:@property(nonatomic,copy)MASConstraintMaker*(^偏移量)(CGFloat);比如例子中的.position(x,y),你可以给一个类添加一个属性:@property(nonatomic,copy)ViewMaker*(^position)(CGFloatx,CGFloaty);在调用.position(x,y)方法时,执行这个block,返回ViewMaker的实例,保证可以进行链式调用。2.从语义层面从语义层面,需要定义哪些粒子是粒子,哪些需要接受参数。为了保证链式调用能够完成,需要考虑传入什么,返回什么。或者拿上面的例子来说:UIView*aView=AllocA(UIView).with.position(x,y).size(width,height).bgColor(aColor).intoView(aSuperView);我们一步步来看,这个DSL表达式需要描述的是一个祈使句,以Alloc开头,以intoView结尾。在intoView终结符之前,我们修改UIView,使用位置大小bgColor等。我们从四段来看如何实现这样的表达式:(1)对象在AllocA(UIView)的语义中,我们确定对象是一个UIVIew。既然确定了UIView在intoView的最后,那么我们就需要创建一个中间类来保存所有的中间情况。这里我们使用ViewMaker类。@interfaceViewMaker:NSObject@property(nonatomic,strong)ClassviewClass;@property(nonatomic,assign)CGPointposition;@property(nonatomic,assign)CGPointsize;@property(nonatomic,strong)UIColor*color;@end另外我们可以注意到AllocA是一个函数,而UIView不能直接传给这个函数,语法会变成AllocA([UIViewclass]),失去简洁性。所以我们需要定义一个宏来“吞掉”方括号和类方法:returnmaker;}(2)粒子很多时候,为了让DSL的语法看起来更连贯,我们需要一些粒子来帮忙,比如make.top.equalTo(superview.mas_top).with.offset(padding.topinMasonry)就是这样一个粒子。而这个助词和我们学过的语法一样,通常没有实际作用,简单returnself就可以了。@interfaceViewMaker:NSObject@property(nonatomic,strong)ClassviewClass;@property(nonatomic,assign)CGPointposition;@property(nonatomic,assign)CGPointsize;@property(nonatomic,strong)UIColor*color;@property(nonatomic,readonly)ViewMaker*with;@end@implementationViewMaker-(ViewMaker*)with{returnsself;}@end需要注意的是,没有办法阻止用户不断调用自己.with.with.with,为了避免这种情况,你可以new生成一个类,每个类都有自己层次的方法,避免跳层调用。@interfaceViewMaker:NSObject@property(nonatomic,strong)ClassviewClass;@property(nonatomic,assign)CGPointposition;@property(nonatomic,assign)CGPointsize;@property(nonatomic,strong)UIColor*color;@end@interfaceViewClassHelper:NSObject@property(非原子,强)ClassviewClass;@property(nonatomic,readonly)ViewMaker*with;@end#defineAllocA(aClass)alloc_a([aClassclass])ViewClassHelper*alloc_a(ClassaClass){ViewClassHelper*helper=ViewClassHelper.new;helper.viewClass=aClass;returnhelper;}@implementationViewClassHelper-(ViewMaker*)with{ViewMaker*maker=ViewMaker.new;maker.viewClass=self.viewClass;returnmaker;}@end这有效地阻止了类似.with.with.with的语法。但实际上,我们要根据真正的需求来开发。使用DSL的用户是为了更好的表现力,所以不会写.with.with.with这样的代码。这样的保护措施似乎有点没有必要。不过使用类来区分粒子还有另外几个小好处,就是可以保证在提示语法的时候,ViewClassHelper类只有.with这样的语法提示,不会出现ViewMaker。出现。不过为了文章的简洁,我们都使用前者,即.withreturnsself继续下面的:@interfaceViewMaker:NSObject@property(nonatomic,strong)ClassviewClass;@property(nonatomic,assign)CGPointposition;@属性(非原子,分配)CGPointsize;@property(nonatomic,strong)UIColor*color;@property(nonatomic,readonly)ViewMaker*with;@end@implementationViewMaker-(ViewMaker*)with{returnsself;}@end(3)修改部分-定语如示例位置sizebgColor这些是属性部分,用来修改UIView,它们以属性的形式存在于ViewMaker实例中,为了支持链式表达,所以当它们被执行时,会不断返回self。让我们尝试实现:@interfaceViewMaker:NSObject//...@property(nonatomic,copy)ViewMaker*(^position)(CGFloatx,CGFloaty);@property(nonatomic,copy)ViewMaker*(^size)(CGFloatx,CGFloaty);@property(nonatomic,copy)ViewMaker*(^bgColor)(UIColor*color);@end@implementationViewMaker-(instancetype)init{if(self=[superinit]){@weakify(self)_position=^ViewMaker*(CGFloatx,CGFloaty){@strongify(self)self.position=CGPointMake(x,y);};_size=^ViewMaker*(CGFloatx,CGFloaty){@strongify(self)self.size=CGPointMake(x,y);};_bgColor=^ViewMaker*(UIColor*color){@strongify(self)self.color=color;};}returnsself;}@end(4)终端词“终端词”真的是现代语法我可以在里面找对应关系,但是在DSL中,这一段尤为重要。ViewMaker的实例从头到尾收集了很多修改,需要最好的表达词才能产生最好的结果,这里称之为“终端词”。例如,当开源库Expecta中的equal表达真实行为时,to和notTo都不会真正触发该行为。在我们的示例中,终结器.intoView(aSuperViwe)可以这样实现:@interfaceViewMaker:NSObject//...@property(nonatomic,copy)UIView*(^intoView)(UIView*superView);@end@implementationViewMaker-(instancetype)init{if(self=[superinit]){@weakify(self)//..._intoView=^UIView*(UIView*superView){@strongify(self)CGRectrect=CGRectMake(self.position.x,self.position.y,self.size.width,self.size.height);UIView*view=[[UIViewalloc]initWithFrame:rect];view.backgroundColor=self.color;[superViewaddSubView:view];returnview;};}returnsself;}@end这样一个终端词就写好了。最终代码的汇总:@interfaceViewMaker:NSObject@property(nonatomic,strong)ClassviewClass;@property(nonatomic,assign)CGPointposition;@property(nonatomic,assign)CGPointsize;@property(nonatomic,strong)UIColor*color;@property(非原子,只读)ViewMaker*with;@property(非原子,复制)ViewMaker*(^position)(CGFloatx,CGFloaty);@property(非原子,复制)ViewMaker*(^size)(CGFloatx,CGFloaty);@property(非原子,copy)ViewMaker*(^bgColor)(UIColor*color);@property(nonatomic,copy)UIView*(^intoView)(UIView*superView);@end@implementationViewMaker-(instancetype)init{if(self=[superinit]){@weakify(self)_position=^ViewMaker*(CGFloatx,CGFloaty){@strongify(self)self.position=CGPointMake(x,y);};_size=^ViewMaker*(CGFloatx,CGFloaty){@strongify(self)self.size=CGPointMake(x,y);};_bgColor=^ViewMaker*(UIColor*color){@strongify(self)self.color=color;};_intoView=^UIView*(UIView*superView){@strongify(self)CGRectrect=CGRectMake(self.position.x,self.position.y,self.size.width,self.size.height);UIView*view=[[UIViewalloc]initWithFrame:rect];view.backgroundColor=self.color;[superViewaddSubView:view];returnview;};}returnsself;}-(ViewMaker*)with{returnself;}@end综上所述,这种链式调用可以让程序更清晰,在特定场景下让程序更易读的方法在Swift中也是一样的。你可以好好利用它,让你的代码更漂亮。事实上,iOS开发者要想精益求精,成长为真正的高手,就必须将视野置于业务需求之上,精简和强化核心技能,提高对语言和工具的掌握程度,才能提高开发效率。提高你的技能水平。这里有更多好玩更高级的iOS黑魔法攻防技巧,让你事半功倍。StarkeyAcademy(StuQ)特邀深受学生喜爱的资深iOS技术专家臧成伟老师,为大家奉上《 iOS 实战黑魔法 》课程,为期6周12小时的高阶黑魔法攻防技能必掌握高效搞定iOS,让你逐步从普通开发者中走出来,看到不一样的语言,体验不一样的开发!