kotlin的 dsl 与 java 原生实现的对比与思考

看 kotlin 实现了一段 html 构建器的 dsl 代码,非常简短:

fun main() {

    println("table  = ${createTable()}")

}

fun createTable() = table {
    tr {
        td { }
    }
}


open class Tag(private val name: String) {
    private val children = mutableListOf<Tag>()

    protected fun <T : Tag> doInit(child: T, init: T.() -> Unit) {
        child.init()
        children.add(child)
        // err("class: ${javaClass.simpleName}, size:${children.size}, children:${children.joinToString()}")
    }

    override fun toString() = "<$name>${children.joinToString(separator = "")}</$name>"
}

class TABLE : Tag("table") {
    fun tr(init: TR.() -> Unit) = doInit(TR(), init)
}

class TR : Tag("tr") {
    fun td(init: TD.() -> Unit) = doInit(TD(), init)
}

class TD : Tag("td")


fun table(init: TABLE.() -> Unit) = TABLE().apply(block = init)

代码很少,实现的功能也很简单,看输出:

table  = <table><tr><td></td></tr></table>

这段代码说实话,不好理解。

但是不好理解的不是这段代码本身,而是它的表达方式。对于 带接收者的 lambda ,之前已经了解过了,而且比较容易理解。不过这种实战性质的代码,貌似一下不能理解了。

不过如果你看到Java的实现,也不会立即理解的。不能理解的关键在于,我们平常的代码中很少使用这种表达方式。

先看一下对应的Java实现。(功能完全相同,逻辑上大同小异。)

对于Java实现,代码量必然要多一点点,不过总体上看,还是比较简洁的。

// 这个接口几乎没任何作用,但是在这里又是必不可少的。
interface Wrapper<T> {
    void invoke(T t);
}
// 工具类不必说了,这里还省略了构造方法私有化
class ListUtils {
    static <T> String toString(List<T> list) {
        return list.toString()
                .replace("[", "")
                .replace("]", "")
                .replace(", ", "");
    }
}
// 最简单,不需要理解的类
class TD {
    @Override
    public String toString() {
        return "<td></td>";
    }
}

// 仔细看这里的 td(wrapper)方法的实现,这里就用到了前面定义的接口 Wrapper
class TR {
    private List<TD> children = new ArrayList<>();
    void td(Wrapper<TD> wrapper) {
        TD td = new TD();
        wrapper.invoke(td);
        children.add(td);
    }
    @Override
    public String toString() {
        String name = "tr";
        return String.format(Locale.CHINA,
                "<%s>%s</%s>",
                name, ListUtils.toString(children), name);
    }
}

// 和 TR 的实现逻辑完全相同,注意这里的 tr(wrapper) 方法
class TABLE {
    private List<TR> children = new ArrayList<>();
    void tr(Wrapper<TR> wrapper) {
        TR tr = new TR();
        wrapper.invoke(tr);
        children.add(tr);
    }
    @Override
    public String toString() {
        String name = "table";
        return String.format(Locale.CHINA,
                "<%s>%s</%s>",
                name, ListUtils.toString(children), name);
    }
}

// 看调用,为了便于观看,这里不使用任何 lambda
class HtmlClient {

    public static void main(String[] args) {
        System.out.println("getTable ==== " + getTable());
    }
    private static TABLE getTable() {
        TABLE table = new TABLE();
        table.tr(new Wrapper<TR>() {
            @Override
            public void invoke(TR tr) {
                tr.td(new Wrapper<TD>() {
                    @Override
                    public void invoke(TD td) {
                        // System.err.println("be invoked.");
                    }
                });
            }
        });
        return table;
    }
}

对应的 Java 代码就这些了。输出的效果跟上面的 kotlindsl 输出的效果完全相同。

先不管上面的 kotlindsl 实现,先看一下这里的Java实现的逻辑。

直接看 HtmlClient 里面的 getTable()方法。

  1. 首先,创建了一个 TABLE对象,这个完全没毛病,任何人都能理解。
  2. 然后,调用了TABLE对象的tr()方法。这个有一点点难度了,要看一下 tr()具体做了什么
    1. tr()里面,首先是创建了一个TR对象,并且调用了参数 wrapperinvoke()方法。

      a. 这个解释几乎就没有解释,似乎毫无意义的
      b. 再看一下,这里是执行了wrapper.invoke(tr); 。不过到里面并没有看到invoke()的具体实现是什么。(这个具体实现是什么很重要
      c. 具体实现在哪?就在HtmlClient#getTable()里面,这里的具体实现就是:执行了tr.td(...)
      d. 这个实现到底有什么意义呢?没有其他的意义,唯一的作用就是让TABLE#tr(wrapper)这个方法里面创建的那个tr对象被调用者感知,并且让调用者用这个 tr对象去调用它自己的TR#td(wrapper)方法。
      d.d 特别注意,这里的 tr.td(new Wrapper(){...}), 这个 invoke()是个空实现。为啥是空实现,而不是具体的一些逻辑呢?这就要明白这里把 Wrapper<T>作为tr()以及 td()的参数的意义了。wrapper的目的不是真的去执行什么逻辑,就是通过回调的方法把自己持有的对象暴露给调用者,这样调用者就可以通过这个回调拿到这里的对象(去执行该对象的方法),从而实现一层一层的包裹。
      e. 为什么要这么做?为了让 table所关联的 tr 能够去关联一个 td . 否则就不能实现这种层层包裹的效果了。(效果见输出:<table><tr><td></td></tr></table>

    2. 然后把这里创建的 TR对象放进了成员对象List<TR> children 里面了。(这一步很明显是为 toString() 用的)
  3. 最后是返回了这个对象,这个也没毛病。不过注意,这时候,这个table它的成员变量children里面包含了一个TR对象,而这个TR对象的成员变量children包含了一个TD对象。
  4. 那么在打印的时候,根据重写的toString() 就实现了对应的 包裹效果。

然后, 这个 Java 实现是有优化空间的:

  • 第一,可以像上面的 kotlin 实现一样,通过继承来减少重复代码。
  • 第二,可以让 tr(), td() 方法返回当前对象,而不是 void. 方便调用者。

不过这里主要是要说明怎样去用 Java 实现上面 kotlin dsl 的同样效果。

最后,上面的 kotlindsl 现在应该会容易理解一点了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值