看 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 代码就这些了。输出的效果跟上面的 kotlin 的 dsl 输出的效果完全相同。
先不管上面的 kotlin 的 dsl 实现,先看一下这里的Java实现的逻辑。
直接看 HtmlClient 里面的 getTable()方法。
- 首先,创建了一个
TABLE对象,这个完全没毛病,任何人都能理解。 - 然后,调用了
TABLE对象的tr()方法。这个有一点点难度了,要看一下tr()具体做了什么tr()里面,首先是创建了一个TR对象,并且调用了参数wrapper的invoke()方法。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>)- 然后把这里创建的
TR对象放进了成员对象List<TR> children里面了。(这一步很明显是为toString()用的)
- 最后是返回了这个对象,这个也没毛病。不过注意,这时候,这个
table它的成员变量children里面包含了一个TR对象,而这个TR对象的成员变量children包含了一个TD对象。 - 那么在打印的时候,根据重写的
toString()就实现了对应的 包裹效果。
然后, 这个 Java 实现是有优化空间的:
- 第一,可以像上面的
kotlin实现一样,通过继承来减少重复代码。 - 第二,可以让
tr(), td()方法返回当前对象,而不是void. 方便调用者。
不过这里主要是要说明怎样去用 Java 实现上面 kotlin dsl 的同样效果。
最后,上面的 kotlin 的 dsl 现在应该会容易理解一点了。

本文探讨了使用Kotlin的DSL特性构建HTML结构的简洁代码实现,对比Java的实现方式,深入解析了其背后的逻辑与设计思路。
799

被折叠的 条评论
为什么被折叠?



