看 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
现在应该会容易理解一点了。