将链式调用的DSL进行到底

一、前言

  • 通过本文,你可以了解到 什么是DSL怎么实现链式DSL如何去封装优化,以及 轻松使用 UIBezierPath

  • 当然,本文所采用的例子是对 UIBezierPath 的封装,一句代码就可以画贝塞尔曲线,全程没有任何 [] ,你会发现,Objective-C 原来也可以这样玩,先给效果吧!

恩,我姓孔
UIBezierPath.fl_path.maker.moveTo(90,200).addLineTo(200,200).addLineTo(130,300).addLineTo(130,450).addLineTo(90,380).addLineTo(250,200).addLineTo(250,450).addLineTo(300,450).addLineTo(300,380).lineWidth(7).lineCapStyle(kCGLineCapRound).lineJoinStyle(kCGLineJoinRound).stroke();

二、什么是DSL(Domain Specific Language)

DSL是一种基于特定领域的语言,它使工作更贴近于客户的理解,而不是实现本身,这样有利于开发过程中,所有参与人员使用同一种语言进行交流。

  • 而不管做什么开发,总有一个任务就是希望能够更加简洁、更加语义化地去表达自己的逻辑,链式调用是一种常见的处理方式。

  • 实际iOS开发中,相信大家应该都用过,比如说常用来做 界面约束布局Masonry ,它的调用是这样的,对比系统的 NSLayoutConstraint 约束,确实使用更加方便和易懂,但我觉得还是不够彻底,因为还是有 [],我能不能把这个也去掉,这个后文会分析。

UIEdgeInsets padding = UIEdgeInsetsMake(10, 10, 10, 10);

[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
    make.top.equalTo(superview.mas_top).with.offset(padding.top); //with is an optional semantic filler
    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);
}]

三、如何实现链式DSL

读了 Masonry 源码,你应该能发现,有 两种方式 能实现链式 DSL,同时适用了两种场合(需不需要外部传参),其实根本来说就一种方式,就是通过重写属性的getter方法,只不过属性返回值不一样,对应了不同的场合而已。

  • ** 1、通过属性的getter方法,返回对象本身,此时不需要外界传参 **

  • ** 2、通过属性的getter方法,返回block回调,此时可以通过block传参 **

个人看法,链式语法的结构一般可分三层,创建对象 - 中间变量 - 结束词 。其中 中间变量 这部分建议使用中间类来处理,一来体现封装性,对某一部分的功能应该独立出来处理,避免单个类代码冗余;二来调用更加方便清晰,可读性更强。本文是针对 UIBezierPath 的封装,查看了 UIBezierPath 的 api ,很容易发现对应的结构。

  • 1、创建对象方法(就是创建 UIBezierPath 对象的方法)(部分api举例)
+ (instancetype)bezierPath;

+ (instancetype)bezierPathWithRect:(CGRect)rect;
  • 2、中间变量(就是初始化 UIBezierPath 对象的属性以及方法,不限于系统,可自定义)(部分api举例)
@property(nonatomic) CGFloat lineWidth;

- (void)moveToPoint:(CGPoint)point;
  • 3、结束词(就是结束当前的链式语法,我们都知道,只有当 UIBezierPath 对象调用 strokefill 才会绘制出来)(部分api举例)
- (void)fill;

- (void)stroke;

四、链式实现 UIBezierPath的封装

为了更加方便大家使用,本次封装是为 UIBezierPath 添加分类,使用更加方便简单。

  • 1、中间类创建,本文中间类命名为 FLBezierPathMaker ,继承自 NSObject。定义需要初始化 UIBezierPath 对象的属性,将系统的api转换(此时属性可自定义,不局限于系统的),因为只需要 getter 方法,因此属性都是 readonly 修饰,如果需要参数传入,那么就定义block属性,如果不需要参数传入,普通中间类的对象即可。

    注意:需要链式拼接的属性都需要返回中间类FLBezierPathMaker 对象本身(部分api举例)

/**

  • @author gitKong
  • 当前绘制BezierPath的颜色,stroke 对应 线,fill 对应填充
    */
    @property (nonatomic,weak,readonly)FLBezierPathMaker (^color)(UIColor color);
    /
  • @author gitKong
  • 起点,可多次设置,对应系统api:@property(nonatomic) CGFloat lineWidth;
    /
    @property (nonatomic,weak,readonly)FLBezierPathMaker (^lineWidth)(CGFloat lineWidth);
    /
  • @author gitKong
  • 起点,可多次设置,对应系统api:- (void)moveToPoint:(CGPoint)point;
    */
    @property (nonatomic,weak,readonly)FLBezierPathMaker *(^moveTo)(CGFloat x,CGFloat y);

- 2、创建对象,参考 [Masonry ](https://github.com/SnapKit/Masonry) 的做法,通过传入设置中间变量的 `makeOperation` 回调,并实现,这个回调参数是 `FLBezierPathMaker` 在创建对象的时候传递出去。从而实现 `bezierPath` 的初始化信息。(部分api举例)

/**

  • @author gitKong
  • 回调通过 maker 初始化中间类信息,对比系统方法:+ (instancetype)bezierPath;
    */
  • (instancetype)fl_bezierPath:(void(^)(FLBezierPathMaker maker))makeOperation;
    /
    *
  • @author gitKong
  • 回调通过 maker 初始化中间类信息,对比系统方法:+ (instancetype)bezierPathWithRect:(CGRect)rect;
    */
  • (instancetype)fl_bezierPathWithRect:(CGRect) rect makeOperation:(void(^)(FLBezierPathMaker *maker))makeOperation;

- 3、结束词,通过结束词,结束当前的链式语法,并且实现相对于的功能,本文中就是实现绘制贝塞尔曲线,同样,`stroke` 和 `fill` 的api已经在中间类 `FLBezierPathMaker` 中定义。(部分api举例)

/**

  • @author gitKong
  • 默认绘制颜色为UIColor.blackColor,对应系统方法:- (void)stroke;- (void)fill;
    */
    @property (nonatomic,weak,readonly)UIBezierPath * (^stroke)();
    @property (nonatomic,weak,readonly)UIBezierPath * (^fill)();

- 4、调用,类似 `Masonry ` 的调用方式,是不是觉得链式语法实现很简单

[UIBezierPath fl_bezierPath:^(FLBezierPathMaker *maker) {
maker.moveTo(0,20).addLineTo(30,50).stroke();
}];


---

# 五、精简链式DSL代码

> 上文提过,此时调用还是需要使用 `[]` 创建对象,能不能像 swift 一样,可以直接去掉 `[]` ,只需要通过 `类名+点语法` 就能实现对象的创建呢

- 要实现通过 `类名+点语法` 创建对象,必须是一个 `getter` 方法,而且这个 `getter` 方法是类方法。简单来说,就是需要 **类属性** 的 `getter` 方法!

- 属性就是对象嘛,虽然类属性在 Objective-C 是存在的,因为一切皆对象,但以前从来没有能显式开放使用,但这个东西在 Swift 中是有的,例如:`public var characters: String.CharacterView`,是 `String` 的一个类属性。

- 可喜的是,在 [WWDC 2016 What’s New in LLVM](https://developer.apple.com/videos/play/wwdc2016/405/) 中(文字在 Transcript 中),有提到从 X-code 8开始,LLVM已经支持 Objective-C 显式声明类属性了,这是为了与 Swift 中的类属性互操作而引入的,原文如下:

>Objective-C now supports class properties. This feature started in Swift as type properties, and we've brought it to Objective-C. Interoperation works great. In this example, the class property someString is declared using property syntax, by adding a class flag. Later, this someString property is accessed using dot syntax. Class properties are never synthesized.

>Objective-C现在支持类属性。 这个特性从Swift开始作为类型属性,我们把它带到Objective-C。 互操作就可以很好处理了。 在此示例中,通过添加类标志class,使用属性语法声明类属性someString。 然后,就可以使用点语法访问此someString属性。 **注意:类属性系统不会帮我们实现(setter 和 getter 方法)**。

不会声明的,可以在查看系统的 `UIColor` 类:你用X-code 8 以上的话,都可以像Swift一样获取颜色:`UIColor.redColor` (不过系统没提示,我的x-code版本: Version 8.2.1 (8C1002))

![截图来自苹果官方API](http://upload-images.jianshu.io/upload_images/1085031-11051d866a16b157.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)


---

那么现在就很好办了,创建对象可以直接使用类属性来实现了返回,而从上文可知,需要提供一个参数  `makeOperation` 回调,那就意味着,我们需要一个block 的类属性,这个block类属性返回值是 `UIBezierPath` 对象,参数只有一个,就是 `makeOperation` 回调block(`makeOperation` 参数是`FLBezierPathMaker`对象,不需要返回值),为了方便大家理解,可看下面代码(h 和 m文件):

/**

  • @author gitKong

  • 通过添加 class 修饰,就是类属性

  • fl_bezierPath:参数:makeOperation(block);返回值:UIBezierPath *

  • makeOperation(block):参数:FLBezierPathMaker *;返回值:void

*/
@property (class,nonatomic,weak,readonly)UIBezierPath *(fl_bezierPath)(void(makeOperation)(FLBezierPathMaker *));


UIBezierPath * FLCreatePathMaker(UIBezierPath *path,void (^makeOperation)(FLBezierPathMaker *)){
// 初始化 中间类 对象
FLBezierPathMaker *mk = [[FLBezierPathMaker alloc] init];
mk.path = path;
// 实现传递过来 makeOperation,并传出参数:FLBezierPathMaker 对象
if (makeOperation) {
makeOperation(mk);
}
// 存储当前的 FLBezierPathMaker 对象
_maker = mk;
return path;
}

  • (UIBezierPath * (^)(void (^)(FLBezierPathMaker )))fl_bezierPath{
    /
    *
    • @author gitKong
    • 类属性需要调用者手动实现 getter 方法
      */
      return ^(void (^makeOperation)(FLBezierPathMaker *)){
      // 传递参数,返回 UIBezierPath 对象
      return FLCreatePathMaker([UIBezierPath bezierPath],makeOperation);
      };
      }

---

那么现在创建对象就不需要通过 `[]` 实现了,勇敢地点出来吧(x-code 不提示)

UIBezierPath.fl_bezierPath(^(FLBezierPathMaker *maker) {
maker.moveTo(0,20).addLineTo(30,50).stroke();
});


对比一下之前写的 `[]` ,是不是觉得舒服很多了,虽然现在全部都是点语法了,但是整体代码看上去不是那么好读,而且还有一个可优化的点,就是属性名称太长了(你可以理解为方法名),写过 swift的都知道,swift 的api 都是很精简,同时因为x-code不提示,因此 `^(FLBezierPathMaker *maker) {}` 都是要自己手写上去,这就不友好了,因为要手写的太多了,举个例子:(选个参数最多的)

/**

  • @author gitKong
  • 绘制圆弧,对应系统方法:+ (instancetype)bezierPathWithArcCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise;
    */
    @property (class,nonatomic,weak,readonly)UIBezierPath *(^fl_arcCenter)(CGPoint center,CGFloat radius,CGFloat startAngle,CGFloat endAngle,BOOL clockwise,void(^maker)(FLBezierPathMaker *));

/**

  • @author gitKong
  • 因为x-code 不提示,我最快的方法只能通过去h文件copy过来填写,参数太多,而且参数还带个block,太麻烦
    */
    UIBezierPath.fl_arcCenter(CGPointMake(30, 30),30,0,60,YES,^(FLBezierPathMaker *maker){
    maker.moveTo(0,20).addLineTo(30,50).stroke();
    });

那么怎么去掉`^(FLBezierPathMaker *maker) {}` 呢?

- 首先我们要清楚,这个东西是干什么的,这个是外界传入的一个block,这个block提供一个 `FLBezierPathMaker ` 对象可以让外界设置中间类的属性,同时又能返回当前的 `UIBezierPath` 实例,那么我们可以分开这两个功能,通过两个属性分别实现:**(如果需要传递参数,那么就使用block属性,注意不要带入`FLBezierPathMaker ` 参数就行)**
  

/**

  • @author gitKong
  • 对外提供的 中间类对象,设置中间类的对象属性
    /
    @property (nonatomic,weak,readonly)FLBezierPathMaker maker;
    /
  • @author gitKong
  • 类属性,创建UIBezierPath实例
    */
    @property (class,nonatomic,weak,readonly)UIBezierPath *fl_path;

static FLBezierPathMaker *_maker = nil;

  • (FLBezierPathMaker *)maker{
    return _maker;
    }
  • (UIBezierPath *)fl_path{
    return FLCreatePath([UIBezierPath bezierPath]);
    }
    UIBezierPath * FLCreatePath(UIBezierPath *path){
    FLBezierPathMaker *mk = [[FLBezierPathMaker alloc] init];
    mk.path = path;
    // 存储当前的FLBezierPathMaker
    _maker = mk;
    return path;
    }

此时调用起来就是我所需要的效果了,而且属性名称精简了,需要传入的参数也少了,看代码翻译成自然语言就是:**创建一个path,让移动起点到(0,20),添加线条到点(30,50),绘制**。

UIBezierPath.fl_path.maker.moveTo(0,20).addLineTo(30,50).stroke();


对比系统方式调用:

UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(0, 20)];
[path addLineToPoint:CGPointMake(30, 50)];
[path stroke];


---

# 六、总结

- 1、以上是封装的思路以及讲解链式语法是如何实现的,不单是Swift,OC也可以写得很优雅。

- 2、封装了 `UIBezierPath` 类,调用起来很方便,特别是如果绘制线条比较多的时候,可读性更强。

- 3、也许你会有个疑惑,为什么 `stroke` 和 `fill` 设计成一个block,而不是单纯一个普通属性,因为根本不需要传参!确实可以这样实现,而且能正常使用,但会报警告⚠️(如下图),意思就是:**不要用.调用,用[]括号调用** 如果这样不就是回到原点么,因此使用block来实现,就能避免这种警告。

![报警告是不是很不爽](http://upload-images.jianshu.io/upload_images/1085031-2610015efc6b880c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

- 4、本文封装的 `stroke` 和 `fill` block,能返回当前的 `UIBezierPath` 对象,而系统的 `stroke` 和 `fill` 是返回 `void`,因为考虑到绘制完毕后可能还需要操作path。

- 5、部分方法需要设置角度angle的值,系统默认是以弧度表示,框架内部处理了,比如你使用 `addArcWith`,设置 `startAngle` 值为 0,那么角度就是 0 度。

- 6、如果你有什么疑惑或建议,欢迎留言!同时欢迎大家关注我,喜欢点个like,会不定时更新技术文章。

- 7、框架支持cocoaPod,输入:`pod search FLBezierPath` 查询最新版本 ,对应 [Github 地址](https://github.com/gitkong/FLBezierPath)  如果觉得可以,点个star喔

![FLBezierPath](http://upload-images.jianshu.io/upload_images/1085031-7dca2528341b3537.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/620)


文章已经同步到我的 [个人博客](https://gitkong.github.io)

推荐阅读更多精彩内容