主题
构建简单的 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); }
public DslBuilder and(Condition condition) { return appendWithDirection(condition, DIRECT_AND, DIRECT_OR, "current operation is 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; }
public DslBuilder thenDo(ExecutableTarget executable) { targets.add(idx, executable); return this; }
public DslBuilder elseDo(ExecutableTarget executable) { idx++; direct = DIRECT_UNKNOWN; conditionsList.add(Collections.emptyList()); directs.add(idx, direct); targets.add(idx, executable); return this; }
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 {
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); }
private static boolean determineAnd(Domain domain, List<Condition> conditions) { for (Condition condition : conditions) { if (!condition.determine(domain)) { return false; } } return true; }
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-else
或 switch-case
的 lambda
表达式中。
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
对象,可以反复使用,只要确保 Condition
和 ExecutableTarget
是无状态的。所以,代码只有 dsl.execute(domain)
这一句,代码极为简单。
- 结构被高度抽象,只留下骨架,业务高层逻辑非常清晰。
- 抽象彻底的情况下,测试
Condition
和 ExecutableTarget
都非常简单,依赖少。业务聚焦,也测试简单。