使用DSL优雅的实现串口协议

来源:互联网 发布:淘宝的广告语 编辑:程序博客网 时间:2024/05/17 18:18

先看看效果

kotlin代码:

fun ledOnBoardInstruction(index: Byte, ledIndex: Byte, r: Short, g: Short, b: Short) =                rj25Instruction {                    index(index)                    mode(modeWrite)                    command(commandLed)                    port(7)                    slot(2)                    data(ledIndex)                    data(r)                    data(g)                    data(b)                }

这段代码的作用,是完成这样一条蓝牙串口协议:
LED灯协议图
协议需要的是动态计算的,长度不定的Byte数组,能写成这样,酷不酷炫,想不想学?

什么是DSL

Domain-specific language,领域特定语言,定义自行看维基百科,以下是我个人的总结:

  • 求专不求全,所有的代码就是为了解决一个特定问题,以上面的代码为例,length是动态的,后面的每个字段的顺序也是不确定的,如果按Java的方式(作为一个Android开发,我说的显然不是Java8(╥﹏╥)),类会是这个样子:

    不采用DSL的类
    现在是这样:
    采用DSL的类
    当然这也有kotlin允许在同一个文件定义多个类的原因,但DSL的方式,完全只考虑了协议本身,所以很多问题都迎刃而解

  • DSL不会减少你的工作量,甚至会造成稍多一些的开发工作量,但是多出来的工作量,换取别的好处,我认为是值得的
  • 代码即文档,从例子的两张图就可以看出,代码与实际协议的字段,是一一对应的,只要定义好最基础的东西,随便谁接收,都可以很轻松的继续编写出可读性非常高的代码

具体怎么实现

  • 首先,这其实是使用Lambda 表达式来构建Builder,所以Java8,或者其他支持Lambda 表达式的语言,也可以做到类似的事,只是可能没有kotlin这种立志成为语法糖语言的语言简洁~~
  • Lambda 表达式如果不熟悉,可以参考Kotlin中文文档,下面在介绍的过程中也会逐步介绍
  • rj25Instruction是一个函数,它接收的参数,是一个Lambda表达式,而kotlin中,如果最后一个参数为Lambda表达式,可以省略括号,直接接方括号写表达式:
fun rj25Instruction(init: Rj25Instruction.() -> Unit): Rj25Instruction {    val rj25Instruction = Rj25Instruction()    rj25Instruction.init()    return rj25Instruction}

所以这段代码的作用为新建一个Rj25Instruction类,依次执行这个类的这些方法:

index(index)mode(modeWrite)command(commandLed)port(7)slot(2)data(ledIndex)data(r)data(g)data(b)

当然在这些之前,还有Rj25Instruction内部定义的初始化方法:

init {   head()   length()}

对照着文档,是不是一目了然?
- 内部的函数:

private fun head() = addChild(ArrayByteElement(headBytes))private fun length() = addChild(lengthElement)fun index(byte: Byte) {    index = byte.toInt()    addChild(ByteElement(byte))}fun mode(byte: Byte) = addChild(ByteElement(byte))fun command(byte: Byte) = addChild(ByteElement(byte))fun port(byte: Byte) = addChild(ByteElement(byte))fun slot(byte: Byte) = addChild(ByteElement(byte))fun secondaryCommand(byte: Byte) = addChild(ByteElement(byte))fun data(byte: Byte) = addChild(ByteElement(byte))fun data(int: Int) = addChild(IntElement(int))fun data(float: Float) = addChild(FloatElement(float))fun data(short: Short) = addChild(ShortElement(short))fun data(bytes: Array<Byte>) = addChild(ArrayByteElement(bytes))override fun getBytes(): ByteArray {    calculateLength()    return super.getBytes()}private fun calculateLength() {    val length = getSize() - headBytes.size - lengthElement.getSize()    lengthElement.data = length.toByte()}

更加深入的代码就没有必要贴出来了,也就是更进一步的转换数据而已,到这里可以很明显的看出实现DSL的思路了,使用Lambda 表达式来构建Builder,已经明白了吧?如果这都还不懂,建议温习下StringBuilder的用法~~

更多的应用场景

  • 以我所在的项目为例,还有别的协议需要实现,比如协议字段多重校验,需要计算校验和,用DSL的方式可以更加优雅的实现
  • HTML网页的输出,见官方的例子:HTML
  • 对Android来说,书写UI布局:anko(这个其实我不推荐,不能预览的布局,不太适合真正的项目合作)