构建简单的 DSL

主题

构建简单的 DSL

什么是 DSL

DSL Domain Specific Language 的缩写,中文翻译为领域特定语言。

在代码开发中,若是有一套完整的 DSL 支持,代码表现张力就特别大,能灵活地表现复杂的业务。

前提/需求

我们希望有一个通用的路由执行,在特定条件下,执行特定动作。条件和动作最好是开放式的,能不断扩展。

分析

实际上,我们需要的是一个结构:

# if
if condition then do_something
# if-else
if condition then do_something
# if-elseif-else
if condition then do_something

我们抽象一下,实际上,这里有几个实体:

  • if / if-else / if-elseif-else 结构
  • condition,条件
  • do_something,动作

进一步分析,结构实际上是固定的,可以被固化。而开放式的条件和动作,只要输入固定,输出是一定的。比如 condition 必然返回 bool 量。我们得到了两个接口(interface)。

据此,我们可以构建我们的微型 DSL 了。

构建 DSL

借助工厂模式和链式方法,我们可以构建出一个完整的 DSL 生成器。同时,考虑到支持多条件,我们可以再引入链式调用模式。

DslBuilder 构造器

public class DslBuilder {
private static final int DIRECT_UNKNOWN = 0;
private static final int DIRECT_AND = 1;
private static final int DIRECT_OR = 2;
private int direct = DIRECT_UNKNOWN;
private int idx = 0;
private List<List<Condition>> conditionsList;
private List<ExecutableTarget> targets;
private List<Integer> directs;

/**
* 入口,构建第一步
*/
public static DslBuilder ifMatch(Condition condition) {
DslBuilder builder = new DslBuilder();
builder.append(condition);
return builder;
}

/**
* 放置条件
*/
private void append(Condition condition) {
conditionsList.get(idx).add(condition);
}

/**
* 添加条件,and模式
*/
public DslBuilder and(Condition condition) {
return appendWithDirection(condition, DIRECT_AND, DIRECT_OR, "current operation is or");
}

/**
* 添加条件,or模式
*/
public DslBuilder or(Condition condition) {
return appendWithDirection(condition, DIRECT_OR, DIRECT_AND, "current operation is and");
}

/**
* 添加条件
*/
private DslBuilder appendWithDirection(Condition condition, int directTarget, int directWrong, String msg) {
if (direct == DIRECT_UNKNOWN) {
direct = directTarget;
directs.add(idx, direct);
} else if (direct == directWrong) {
throw new RuntimeException(msg);
}
append(condition);
return this;
}

/**
* ifMatch和elseMatch跟随的动作
*/
public DslBuilder thenDo(ExecutableTarget executable) {
targets.add(idx, executable);
return this;
}

/**
* else跟随的动作
*/
public DslBuilder elseDo(ExecutableTarget executable) {
idx++;
direct = DIRECT_UNKNOWN;
conditionsList.add(Collections.emptyList());
directs.add(idx, direct);
targets.add(idx, executable);
return this;
}

/**
* elseMatch
*/
public DslBuilder elseMatch(Condition condition) {
idx++;
direct = DIRECT_UNKNOWN;
List<Condition> conditions = new ArrayList<>();
conditions.add(condition);
conditionsList.add(conditions);
return this;
}

/**
* 终止,生成执行器
*/
public Dsl end() {
int conditionsListSize = conditionsList.size();
int directsSize = directs.size();
int targetsSize = targets.size();
if (conditionsListSize == directsSize && conditionsListSize == targetsSize) {
return new Dsl(conditionsListSize, conditionsList, directs, targets);
} else {
throw new RuntimeException("size of lists is not match");
}
}

private DslBuilder() {
conditionsList = new ArrayList<>();
conditionsList.add(new ArrayList<>());
targets = new ArrayList<>();
directs = new ArrayList<>();
}

}

Dsl 执行体

public class Dsl {
/**
* 数组化,避免if-else或switch-case
*/
private static final Determiner[] DETERMINERS = new Determiner[]{
Dsl::determineEmptyOrOnlyOne,
Dsl::determineAnd,
Dsl::determineOr
};

private final int length;
private final List<List<Condition>> conditionsList;
private final List<Integer> directs;
private final List<ExecutableTarget> targets;

public Dsl(int length,
List<List<Condition>> conditionsList,
List<Integer> directs,
List<ExecutableTarget> targets) {
this.length = length;
this.conditionsList = conditionsList;
this.directs = directs;
this.targets = targets;
}

/**
* 根据输入,依照条件自动执行对应动作
*/
public Domain execute(Domain domain) {
for (int i = 0; i < length; i++) {
// 判断
if (determine(domain, conditionsList.get(i), directs.get(i))) {
// 具体执行
return targets.get(i).execute(domain);
}
}
throw new RuntimeException("not match");
}

/**
* 根据方向,调用不同的检查逻辑
*/
private boolean determine(Domain domain, List<Condition> conditions, int direct) {
return DETERMINERS[direct].determine(domain, conditions);
}

/**
* and判断
*/
private static boolean determineAnd(Domain domain, List<Condition> conditions) {
for (Condition condition : conditions) {
if (!condition.determine(domain)) {
return false;
}
}
return true;
}

/**
* or判断
*/
private static boolean determineOr(Domain domain, List<Condition> conditions) {
for (Condition condition : conditions) {
if (condition.determine(domain)) {
return true;
}
}
return false;
}

/**
* 无需判断或只需要判断一次
*/
private static boolean determineEmptyOrOnlyOne(Domain domain, List<Condition> conditions) {
if (conditions.isEmpty()) {
return true;
} else {
return conditions.get(0).determine(domain);
}
}

}

Condition 触发条件,开放式实现

public interface Condition {
boolean determine(Domain domain);
}

ExecutableTarget 调用动作,开放式实现

public interface ExecutableTarget {
Domain execute(Domain domain);
}

Determiner 判断器,一个辅助接口,主要用在 Dsl 内部消除 if-elseswitch-caselambda表达式中。

interface Determiner {
boolean determine(Domain domain, List<Condition> conditions);
}

使用

if

Domain domain = new Domain();
domain.setAa1(true);
Dsl dsl = DslBuilder.ifMatch(Objects::nonNull)
.and(d -> d.isAa1())
.thenDo(d -> d)
.end();
Domain domain1 = dsl.execute(domain);
Assert.assertSame(domain, domain1);

if-else

Domain domain = new Domain();
domain.setAa1(true);
domain.setBb2(3);
Dsl dsl = DslBuilder.ifMatch(Objects::nonNull)
.and(d -> !d.isAa1())
.thenDo(d -> d)
.elseMatch(d -> d.getBb2() == 1)
.or(d -> Objects.isNull(d.getCc3()))
.thenDo(d -> null)
.end();
Domain domain1 = dsl.execute(domain);
Assert.assertNull(domain1);

if-elseif-else

Domain domain = new Domain();
domain.setAa1(true);
domain.setBb2(-1);
Dsl dsl = DslBuilder.ifMatch(Objects::nonNull)
.and(d -> !d.isAa1())
.thenDo(d -> d)
.elseMatch(d -> d.getBb2() == 1)
.or(d -> Objects.isNull(d.isAa1()))
.thenDo(d -> null)
.elseDo(d -> {
Domain x = new Domain();
x.setAa1(true);
x.setBb2(3);
return x;
})
.end();
Domain domain1 = dsl.execute(domain);
Assert.assertEquals(3, domain1.getBb2());

优点

这么做的优点在哪里?

  • 生成的 Dsl 对象,可以反复使用,只要确保 ConditionExecutableTarget 是无状态的。所以,代码只有 dsl.execute(domain) 这一句,代码极为简单。
  • 结构被高度抽象,只留下骨架,业务高层逻辑非常清晰。
  • 抽象彻底的情况下,测试 ConditionExecutableTarget 都非常简单,依赖少。业务聚焦,也测试简单。