前言
作为一个iOS程序员基本上都应该接触过Masonry这个自动布局库。这个库能够帮助程序员极大程度的简化自动布局的代码。使用这个库让我感到惊叹的不是如何能够将较为复杂的传统自动布局写法精简到如此程度,而是精简后的代码的书写方式:
make.left.right.bottom.mas_equalTo(0.f);
这种写法在做到简化的同时,通过点(.)调用的方式,将代码连接成一行,大大增加了代码的可读性,这就是本篇要提到的链式编程。
关于链式编程具体是种什么编程思想,这种概念性的东西,请自行百度,这里不多做介绍,本篇主要是通过一个简单的例子来实现上述的链式编程。
举个例子
日常开发中肯定少不了要与数据库打交道,而数据库相信不少项目是使用的splite3以及将其封装的很好的FMDB。如果在开发中直接使用FMDB,那么免不了要手动写SQL语句。当然现在网上也有不少的DAO,可以让开发者无需关心怎么去写SQL,甚至不需要懂SQL的语法,通过对对象的操作就可以达到在数据库的增删改查。
这里我们不讨论这种DAO是怎么实现的,我们实现的是通过一个工具类来帮助我们生成对应的SQL。免去我们手写“SELECT” “WHERE” 等关键字麻烦,而且可以智能提示,避免写错。
#不要问为什么有好好的DAO不去用。这只是个例子,不要太在意细节。
分析
既然要智能提示,免去手动书写“SELECT”等词的话,很明显是将其抽成一个方法供调用者调用,其内部将关键字拼接好。那么这个方法应该是如下类似的:
- (NSString *)select:(NSArray *)columns
from:(NSString *)tableName;
但是,SELECT的情况不止这一种,还有FROM,JOIN ON,GROUP BY,ORDER BY等各种操作,而且是可选的,那么就需要增加以下的一系列类似方法:
- (NSString *)select:(NSArray *)columns
from:(NSString *)tableName
where:(NSString *)where;
- (NSString *)select:(NSArray *)columns
from:(NSString *)tableName
where:(NSString *)where
orderBy:(NSString *)orderBy;
...
很明显,要为每一种组合情况创建一个方法,这样做显得不太明智。因为:这里的例子,SQL查询语句的组合还算是有限的,目前确实可以穷举,把每种可能性列出来,添加相应的方法,但是,要是哪天SQL支持ORDER BY可以写到前面去,那不就呵呵了,又要在增加对应组合情况的方法。显然这么做是不明智的。
那么就换一个方向,将SELECT等每一步操作拆分成一个个单独的方法,这样后续需要接WHERE条件还是直接ORDER BY都可以根据具体业务自由调用,比较灵活,而且这样更加符合面向对象的思想(设计模式中的建造者模式)。
传统做法
假如我们的工具类叫SQLTool,那么拆分后的方法定义应该是以下类似的:
@interface SQLTool : NSObject
//用于保存拼接后的SQL
@property (nonatomic, strong, readonly) NSString *sql;
//为了能后续接着调用,所以返回值还是该对象
- (SQLTool *)select:(NSArray *)columns;
- (SQLTool *)from:(NSString *)tableName;
- (SQLTool *)where:(NSString *)where;
- (SQLTool *)orderBy:(NSString *)orderBy;
...
@end
实现部分这里就省略了,无非就是将传进来的参数按照各自的format拼接到self.sql后面,再返回对象自己。
使用起来是这样的:
SQLTool *tool = [[SQLTool alloc] init];
NSString *testSQL1 = [[[tool select:nil] from:@"Table"] orderBy:@"Column DESC"].sql;
NSString *testSQL2 = [[[[tool select:nil] from:@"Table"] where:@"A = B"] orderBy:@"Column DESC"].sql;
拆分以后,使用起来确实是灵活了,这里 testSQL2 比 testSQL1 多了一个WHERE条件。当然还可以添加leftJoin,rightJoin等方法,使用的时候可以跟业务需求调用。但是仔细一看testSQL1看上去还可以,但是testSQL2就感觉有点臃肿了,要是再加上groupBy,或者多个Join操作,那么这种臃肿就更加明显了。而这种臃肿主要体现在方法调用时候的那一对“[]”上。就算是回车换行,头尾的括号还是太多了,各个方法之间的衔接也因为括号的存在,阅读性不是很强。
链式编程
如果采用链式编程的方式,那么参考下Masonry的实现,我们最终实现的目标是以下类似的:
SQLTool *tool = [[SQLTool alloc] init];
NSString *testSQL1 = tool.select(nil).from(@"Table").orderBy(@"Column DESC").sql;
这种方式明显看上去清爽许多,就算免不了调用的方法次数较多,使得整行代码较长,通过换行还是能保持一定的可阅读性。
那么我们来分析下这样的书写方式是怎么实现的。
首先,用点(.)的形式调用,那可以确定基本上是属性(虽然没有参数的方法也可以通过点(.)的形式调用,但是一般不推荐这么做,有兴趣的朋友可以查阅一些代码规范或者相关帖子的说明);然后,后面可以在括号里传参数,那么进一步确定,这个属性应该是一个block;最后,调完一次后,还可以继续调用,那么说明block的返回值应该还是这个对象。
那么以select方法为例,改造后的代码如下:
.h文件
//定义select的block
typedef SQLTool *(^Select)(NSArray<NSString *> *columns);
@property (nonatomic, strong, readonly) Select select;
.m文件
- (Select)select {
return ^(NSArray<NSString *> *columns) {
if (columns.count > 0) {
self.sql = [NSString stringWithFormat:@"SELECT %@", [columns componentsJoinedByString:@","]];
} else {
self.sql = @"SELECT *";
}
//这里将自己返回出去
return self;
}
}
看上面代码应该很清楚了,想要实现from、join等操作,也是用类似的方式就可以了,这里就不一一贴出来了。
一些总结
1.什么时候使用链式编程?
从上面的例子还有Masonry可以看出,在面向一些过程化处理的时候(拼接SQL、给View加约束,都可以看成需要一步步完成的过程),需要将这些“过程”拆分,然后在“组合”这些“过程”的时候,就可以使用链式编程,使得代码更加清晰,增加阅读性。
2.链式编程的核心实现
实现链式编程的关键就是声明一个block的属性,而这个block返回值必须还是一个对象(根据业务需求不同,可以返回的是这个对象实例本身,也可以是这个类的另一个实例,更可以是另一个类的实例对象)。而block中内部的逻辑就是项目的业务逻辑(在这个例子中是拼接了SQL语句)。
#补充:如果在调用后不需要传递参数的话,只需要声明一个类型是类自己的属性,并重写getter方法,在里面做相应的操作就可以了。以Masonry为例:
make.left.right.bottom.mas_equalTo(0.f);
前面部分的left,right,bottom都没有传递参数,那么这部分的属性是不需要声明为block的,具体可以去查看Masonry源码。
留在最后的思考
通过这个例子,虽然是简单的实现了链式编程,但是还是有一些问题值得我们去思考的:
- 在使用上跟Masonry还是有些区别的,为什么?
- 就本篇举的例子而言,还有些不足:在实际操作中,SELECT关键字及后面的列之后,应该且只能是FROM,这是固定的,既不是WHERE,也不是ORDER BY,而现在的例子里,因为block返回值是对象本身,所以可能会出现这样的情况:
tool.select(nil).from(@"table").from(@"table").select(nil);
也就是说,不但同一个操作可以重复调用,而且会出现不符合顺序的调用。怎么样才能避免这种情况呢,这些问题会在后续的文章中讲解。
谢谢阅读!