DSL 全称是 Domain Specific Language领域特定语言,是用于解决特定领域问题的语言。Android开发中的build.gradle文件里就是DSL语言。
kotlin有非常多的语言特性,可以帮助我们实现各种各样的DSL语言,比如:
get和set语法、中缀函数、操作符重载、高阶函数与lambda表达式、扩展函数、作用域函数等等
我们接下来使用这些特性一步步实现一个地图创建的DSL
我们用到的类如下: Map地图类、Layer地图图层类、LayerListener 图层状态监听接口、DataSource图层数据源类、MapOptions地图视觉类
class Map {
val layerList = mutableListOf<Layer>()
var mapOptions: MapOptions? = null
var layerListener: LayerListener? = null
}
interface LayerListener {
fun onAdd()
fun onRemove()
fun onError()
}
class Layer {
var layerName: String? = null
var dataSource: DataSource? = null
}
class DataSource {
var url: String? = null
}
class MapOptions {
var centerPoint: Point? = null
var zoom = 0.0
var rotate = 0.0
override fun toString(): String {
return "中心点:$centerPoint,缩放:$zoom,旋转:$rotate"
}
data class Point(val lat: Double, val lng: Double) {
override fun toString(): String {
return "经纬度:($lat,$lng)"
}
}
}
1、高阶函数与lambda表达式的语法特性
当lambda表达式是高阶函数最后一个参数时,可以放在参数列表小括号的外部(实现大括号嵌套的关键语法特性,最重要);当lanbda表达式是高阶函数的唯一一个参数时,可以省略方法的参数列表小括号;当lambda表达式有且只有一个参数时,可以用it代替并且可以省略不写;lambda表达式可以指定接收者,接收者可以使用this代替并且可以省略不写。
lambda表达式是实现DSL的关键,它能将方法调用与逻辑代码块从观感上分离开来。
示例:
在没有进行DSL之前,我们创建一个MapOptions如下:
val options = MapOptions()
options.centerPoint = MapOptions.Point(355.53, 66.66)
options.zoom = 1.0
options.rotate = 180.0
我们使用高阶函数实现一个简单的DSL,如下:我们将对象的创建放在函数内,将对象的赋值放在lambda表达式中,因为指定了接收者,所以this可以省略
inline fun mapOptions(block: MapOptions.() -> Unit): MapOptions = MapOptions().apply(block)
使用如下:看起来还不错,就像是发布了一个创建对象的指令
mapOptions {
centerPoint = MapOptions.Point(355.53, 66.66)
zoom = 1.0
rotate = 180.0
}
我们再给Map添加一样的创建DSL,如下
inline fun map(block: Map.() -> Unit): Map = Map().apply(block)
使用如下:这样就套娃起来了
map {
mapOptions = mapOptions {
centerPoint = MapOptions.Point(355.53, 66.66)
zoom = 1.0
rotate = 180.0
}
}
2、扩展函数
我们接下来给Map设置Layer,因为Map里使用列表存储Layer,如果我们只是将Layer的创建和第一步一样实现DSL是不够的,如下:
inline fun dataSource(block: DataSource.() -> Unit): DataSource = DataSource().apply(block)
inline fun layer(block: Layer.() -> Unit): Layer = Layer().apply(block)
使用如下:可以看到我们需要调用集合的添加方法,这样整体结构和阅读性就很差
map {
mapOptions = mapOptions {
centerPoint = MapOptions.Point(355.53, 66.66)
zoom = 1.0
rotate = 180.0
}
layerList.add(layer {
layerName = "第一图层"
dataSource = dataSource {
url = "https://baidu.com"
}
})
}
我们肯定希望可以将列表添加列表项和构建对象只用一个大括号就完成,这样的话列表添加列表项的操作需要单独拿出来,这时候就可以用到扩展函数
inline fun Map.layer(block: Layer.() -> Unit): Layer {
return Layer().apply {
block()
layerList.add(this)
}
}
使用如下:
map {
mapOptions = mapOptions {
centerPoint = MapOptions.Point(355.53, 66.66)
zoom = 1.0
rotate = 180.0
}
layer {
layerName = "第一图层"
dataSource = dataSource {
url = "https://baidu.com"
}
}
}
其实这一步和上面的步骤没什么区别,我们完全可以将layer函数直接放到Map类里,效果是一样的,之所以使用扩展函数,是因为这里的Map类是我们自己创建的,如果是第三方库里不能更改的类,那就要用到扩展函数了。
接下来就是给Map设置图层监听LayerListener,这个也不能直接设置,因为这个接口有三个接口方法,我们没办法直接使用lambda表达式,所以我们需要将三个接口方法拆开,就是将接口方法委托给其他的方法,我们再在构建接口的时候分别传入,如下:
class ExtLayerListener : LayerListener {
private var addBlock: (() -> Unit)? = null
private var removeBlock: (() -> Unit)? = null
private var errorBlock: (() -> Unit)? = null
override fun onAdd() {
addBlock?.let {
it() }
}
fun onAdd(block: () -> Unit) {
addBlock = block
}
override fun onRemove() {
removeBlock?.let {
it() }
}
fun onRemove(block: () -> Unit) {
removeBlock = block
}
override fun onError() {
errorBlock?.let {
it() }
}
fun onError(block: () -> Unit) {
errorBlock = block
}
}
inline fun Map.listener(block: ExtLayerListener.() -> Unit) {
layerListener = ExtLayerListener().apply {
block()
}
}
使用如下:阅读效果和简洁性都拉满有没有
map {
mapOptions = mapOptions {
centerPoint = MapOptions.Point(355.53, 66.66)
zoom = 1.0
rotate = 180.0
}
layer {
layerName = "第一图层"
dataSource = dataSource {
url = "https://baidu.com"
}
}
listener {
onAdd {
Log.e("layer", "新增图层")
}
onRemove {
Log.e("layer", "移除图层")
}
onError {
Log.e("layer", "图层错误")
}
}
}
3. 使用中缀函数、操作符重载
中缀函数和操作符重载可以让我们使用一些特殊的操作符、符号去调用一些函数、连接代码块
如下:
我们给MapOptions类增加一个中缀函数to
infix fun Point?.to(point: Point?) {
centerPoint = point
}
使用:
mapOptions {
centerPoint to MapOptions.Point(355.53, 66.66)
zoom = 1.0
rotate = 180.0
}
我们给Layer类增加一个操作符重载,让Layer类可以和DataSource类做相加运算,相加的时候就是将DataSource赋值给Layer
operator fun plus(source: DataSource?) {
this.dataSource = source
}
效果如下:
layer {
layerName = "第一图层"
} + dataSource {
url = "https://baidu.com"
}
到这里,我们已经完成了地图类的构建,下面我贴上完整的代码:
interface LayerListener {
fun onAdd()
fun onRemove()
fun onError()
}
class Map {
val layerList = mutableListOf<Layer>()
var mapOptions: MapOptions? = null
var layerListener: LayerListener? = null
infix fun MapOptions?.to(options: MapOptions?) {
mapOptions = options
}
}
class Layer {
var layerName: String? = null
var dataSource: DataSource? = null
operator fun plus(source: DataSource?) {
this.dataSource = source
}
}
class DataSource {
var url: String? = null
}
class MapOptions {
var centerPoint: Point? = null
var zoom = 0.0
var rotate = 0.0
override fun toString(): String {
return "中心点:$centerPoint,缩放:$zoom,旋转:$rotate"
}
data class Point(val lat: Double, val lng: Double) {
override fun toString(): String {
return "经纬度:($lat,$lng)"
}
}
infix fun Point?.to(point: Point?) {
centerPoint = point
}
}
inline fun map(block: Map.() -> Unit): Map = Map().apply(block)
//inline fun layer(block: Layer.() -> Unit): Layer = Layer().apply(block)
inline fun dataSource(block: DataSource.() -> Unit): DataSource = DataSource().apply(block)
inline fun mapOptions(block: MapOptions.() -> Unit): MapOptions = MapOptions().apply(block)
inline fun Map.layer(block: Layer.() -> Unit): Layer {
return Layer().apply {
block()
layerList.add(this)
}
}
class ExtLayerListener : LayerListener {
private var addBlock: (() -> Unit)? = null
private var removeBlock: (() -> Unit)? = null
private var errorBlock: (() -> Unit)? = null
override fun onAdd() {
addBlock?.let {
it() }
}
fun onAdd(block: () -> Unit) {
addBlock = block
}
override fun onRemove() {
removeBlock?.let {
it() }
}
fun onRemove(block: () -> Unit) {
removeBlock = block
}
override fun onError() {
errorBlock?.let {
it() }
}
fun onError(block: () -> Unit) {
errorBlock = block
}
}
inline fun Map.listener(block: ExtLayerListener.() -> Unit) {
layerListener = ExtLayerListener().apply {
block()
}
}
Map地图类的构建DSL整体的效果如下:
map {
mapOptions to mapOptions {
centerPoint to MapOptions.Point(355.53, 66.66)
zoom = 1.0
rotate = 180.0
}
layer {
layerName = "第一图层"
} + dataSource {
url = "https://baidu.com"
}
listener {
onAdd {
Log.e("layer", "新增图层")
}
onRemove {
Log.e("layer", "移除图层")
}
onError {
Log.e("layer", "图层错误")
}
}
}
4. DSL的优缺点
到这里,大家应该都能做出一份属于自己的简单的DSL,那我们为什么要做DSL呢?直接用kotlin原来的语法写代码不行吗?当然行,DSL既然存在,肯定就有它的优缺点。
优点:
DSL整体的代码结构简单直接,比起传统的写法可以省略大量的代码并且阅读性极强,这也是为什么我们要使用kotlin语法糖将普通的代码片段变为DSL的原因。如果说,我们的某一些业务逻辑需要在一个项目中大量的使用或者多个项目中重复使用,我们完全可以将它们封装成DSL,毕竟简单写又容易阅读。
缺点:
需要我们自行封装,封装逻辑较为麻烦很容易被劝退,毕竟使用kotlin正常的代码编写完全可以实现DSL的代码逻辑。
对编写人以外的人来说,阅读和使用都一定的成本,毕竟封装完了以后,编写人知道编写和使用逻辑,而对另一个来说,需要一定的时间去了解学习才能使用。
学习的必要性:
现在许多的开源的kotlin项目里面大量的使用自己封装的DSL,如果不了解一些原理,在阅读这些开源库的时候会比较蒙。