《DSL》第1章基本例子

第1章        基本例子

当开始写这本书的时候,需要解释写的是什么?基于此,为了解释Domain Specific LanguagesDSL),先给出具体实例,演示一个DSL,然后抽象的定义,下一章将泛化定义使得适应范围更广。

第1节         哥特式安全

我头脑还隐约保留着看历险电影的童年记忆,通常,这种电影的场景在古老的城堡或者安全部位、通道,英雄们需要拉动楼梯上或者墙上的蜡台两次,才能达到目的。

想象一下,一个公司将基于这种情况创建一个安全系统,建造无线网络,然后安装设备,当发生某种情况,可以发送四个字符的消息,例如,当抽屉打开时,抽屉上的传感器发送D2OP消息。还有一个设备用于接受四个字符消息命令,例如当接收到D1UL消息,则使用这个设备锁门。

这些的核心是,控制软件监听消息事件,然后处理,发送命令消息,这个公司使用Java实现控制器,当客户购买了哥特式安全系统,需要安装许多的设备和用Java实现的控制程序。

针对这个例子,关注控制程序,每个客户都有独立的需求,但是存在一个共同的模式。Miss Grant在他办公室里有一个保密柜,她必须关上门,取下墙上的画,开关桌子上的灯三次,打开橱柜最上面的抽屉,最后保密柜开锁,如果她在打开保险柜的时候忘了关桌子上的灯,将告警。

这个实例是刻意编排的,这个系统族共享大部分组件和行为,但是还有些不同,例如,对所有客户都有控制器收发消息,但是事件和命令顺序不同,为了让这个公司用最小的代价安装一套新的系统,需要一种方便的方式来实现能应对不同序列活动的控制器。

描述这种情况的一种好的方式是使用状态机,每个传感器可以触发事件,进而改变控制器的状态,当控制器处于一个状态,可以向网络中发送一个消息命令。

状态机是一个很好的DSL实例。

Miss Grant的控制器

虽然这个虚构的公司会有成千上万的客户,但是聚焦在Miss Grant身上,她有一个保险柜在卧室里,通常情况下是锁着的,为了打开它,她必须关门,打开橱柜的第二个抽屉,然后打开床头灯,一旦按这个顺序完成,保险柜将打开。

使用状态图来表示。

 

图1             Miss Grant保密柜的状态图

控制器有不同的状态,当处于一个特定的状态,发送特定事件,进而转换到另外一个状态,事件序列导致状态的转换,在这个模型里,当进入一个状态会执行动作(Action)。

这个控制器有一个idle状态,并且大部分时间处于这个状态,在状态转换的中途,发生这个事件,系统也将进入idle状态,进而重置模型,在Miss Grant的情况下,开门就是这样一个重置事件。

介绍重置事件的目的是说明这里的状态机不完全适应经典状态机模型【译者注:经典状态机没有转换或者事件重用的概念,而层次化状态机有此概念,当前的UML2.*中的状态机模型是层次化状态机】。

当然,类似重置事件可以采用另外一种方式来实现,对每个状态添加一个doorOpened事件的转换,目标状态都是idle状态。重置事件能简化状态机图。

第2节         状态机模型

一旦决定使用状态机来抽象表达控制器的工作,下一步将把抽象转变成软件,如果使用事件、状态、转换表达控制器,那么在软件代码中也使用同样的词汇来表达,这是领域驱动开发(DDD)的核心,形成一种程序员与领域专家共享的语言。

当使用Java时,通常使用状态机的领域模型来实现,

 

图2             状态机框架类图

控制器与设备之间的通信通过事件消息和命令消息,通信通道传递的是四个字母的代号,为了在控制器代码里使用象征意义的名字,创建EventCommand类,有namecode属性。

class AbstractEvent...

private String name, code;

public AbstractEvent(String name, String code) {

this.name = name;

this.code = code;

}

public String getCode() { return code;}

public String getName() { return name;}

public class Command extends AbstractEvent

public class Event extends AbstractEvent

State需要记录追踪发送的命令,和外部转换。

class State...

private String name;

private List<Command> actions = new ArrayList<Command>();

private Map<String, Transition> transitions = new HashMap<String, Transition>();

class State...

public void addTransition(Event event, State targetState) {

assert null != targetState;

transitions.put(event.getCode(), new Transition(this, event, targetState));

}

class Transition...

private final State source, target;

private final Event trigger;

public Transition(State source, Event trigger, State target) {

this.source = source;

this.target = target;

this.trigger = trigger;

}

public State getSource() {return source;}

public State getTarget() {return target;}

public Event getTrigger() {return trigger;}

public String getEventCode() {return trigger.getCode();}

状态机初始化在开始状态。

class StateMachine...

private State start;

public StateMachine(State start) {

this.start = start;

}  

状态机的其他状态都可以从起始状态到达。

class StateMachine...

public Collection<State> getStates() {

List<State> result = new ArrayList<State>();

collectStates(result, start);

return result;

}

private void collectStates(Collection<State> result, State s) {

if (result.contains(s)) return;

result.add(s);

for (State next : s.getAllTargets())

collectStates(result, next);

}

class State...

Collection<State> getAllTargets() {

List<State> result = new ArrayList<State>();

for (Transition t : transitions.values()) result.add(t.getTarget());

return result;

}

为了处理重置事件,需要在状态机中保存。

class StateMachine...

private List<Event> resetEvents = new ArrayList<Event>();

public void addResetEvents(Event... events) {

for (Event e : events) resetEvents.add(e);

}

不需要将重置事件独立为一个结构,可以像下面方式将重置事件看成一个外部转换。

class StateMachine...

private void addResetEvent_byAddingTransitions(Event e) {

for (State s : getStates())

if (!s.hasTransition(e.getCode())) s.addTransition(e, start);

}

我更喜欢详细描述状态机的重置事件,因为可以更好的表达意图,虽然使状态变复杂了点,但是可以清楚表达一般状态机是如何工作。回过头来,关注一下行为,像前面提到过的,它确实很简单,控制器有一个响应设备事件的处理方法。

class Controller...

private State currentState;

private StateMachine machine;

public CommandChannel getCommandChannel() {

return commandsChannel;

}

private CommandChannel commandsChannel;

public void handle(String eventCode) {

if (currentState.hasTransition(eventCode))

transitionTo(currentState.targetState(eventCode));

else if (machine.isResetEvent(eventCode))

transitionTo(machine.getStart());

// ignore unknown events

}

private void transitionTo(State target) {

currentState = target;

currentState.executeActions(commandsChannel);

}

class State...

public boolean hasTransition(String eventCode) {

return transitions.containsKey(eventCode);

}

public State targetState(String eventCode) {

return transitions.get(eventCode).getTarget();

}

public void executeActions(CommandChannel commandsChannel) {

for (Command c : actions) commandsChannel.send(c.getCode());

}

class StateMachine...

public boolean isResetEvent(String eventCode) {

return resetEventCodes().contains(eventCode);

}

private List<String> resetEventCodes() {

List<String> result = new ArrayList<String>();

for (Event e : resetEvents) result.add(e.getCode());

return result;

}

忽略任何没有注册到状态的事件,如果识别了事件,则转换到目标状态,并且目标状态执行所有的commands

第3节         Miss Grant's控制器编程

现在实现状态机模型:

Event doorClosed = new Event("doorClosed", "D1CL");

Event drawerOpened = new Event("drawerOpened", "D2OP");

Event lightOn = new Event("lightOn", "L1ON");

Event doorOpened = new Event("doorOpened", "D1OP");

Event panelClosed = new Event("panelClosed", "PNCL");

Command unlockPanelCmd = new Command("unlockPanel", "PNUL");

Command lockPanelCmd = new Command("lockPanel", "PNLK");

Command lockDoorCmd = new Command("lockDoor", "D1LK");

Command unlockDoorCmd = new Command("unlockDoor", "D1UL");

State idle = new State("idle");

State activeState = new State("active");

State waitingForLightState = new State("waitingForLight");

State waitingForDrawerState = new State("waitingForDrawer");

State unlockedPanelState = new State("unlockedPanel");

StateMachine machine = new StateMachine(idle);

idle.addTransition(doorClosed, activeState);

idle.addAction(unlockDoorCmd);

idle.addAction(lockPanelCmd);

activeState.addTransition(drawerOpened, waitingForLightState);

activeState.addTransition(lightOn, waitingForDrawerState);

waitingForLightState.addTransition(lightOn, unlockedPanelState);

waitingForDrawerState.addTransition(drawerOpened, unlockedPanelState);

unlockedPanelState.addAction(unlockPanelCmd);

unlockedPanelState.addAction(lockDoorCmd);

unlockedPanelState.addTransition(panelClosed, idle);

machine.addResetEvents(doorOpened);

后半部分的代码与前半部分的代码有很大的不同,之前的代码描述了如何建立一个状态机模型,后半部分的代码是一个特定控制器的配置,可以这样考虑,一个是库、框架或者构件实现代码,另一个是构件组装代码,本质上,可以将共同代码与变化代码独立,将共同代码结构化成一系列构件,可以应用于不同的配置。

 

图3             一个简单库的多个配置

下面是另一种配置方式:

<stateMachine start = "idle">

<event name="doorClosed" code="D1CL"/>

<event name="drawerOpened" code="D2OP"/>

<event name="lightOn" code="L1ON"/>

<event name="doorOpened" code="D1OP"/>

<event name="panelClosed" code="PNCL"/>

<command name="unlockPanel" code="PNUL"/>

<command name="lockPanel" code="PNLK"/>

<command name="lockDoor" code="D1LK"/>

<command name="unlockDoor" code="D1UL"/>

<state name="idle">

<transition event="doorClosed" target="active"/>

<action command="unlockDoor"/>

<action command="lockPanel"/>

</state>

<state name="active">

<transition event="drawerOpened" target="waitingForLight"/>

<transition event="lightOn" target="waitingForDrawer"/>

</state>

<state name="waitingForLight">

<transition event="lightOn" target="unlockedPanel"/>

</state>

<state name="waitingForDrawer">

<transition event="drawerOpened" target="unlockedPanel"/>

</state>

<state name="unlockedPanel">

<action command="unlockPanel"/>

<action command="lockDoor"/>

<transition event="panelClosed" target="idle"/>

</state>

<resetEvent name = "doorOpened"/>

</stateMachine>

这种表示形式对大部分读者应该比较熟悉,用XML文件,有一些好处:

1)一个明显的好处是我们不必针对每个控制器编写Java程序,只需要编译状态机构件和一个合适的解析器,当状态机启动时,可以读取解析XML文件。改变控制器的行为不必需要创建一个新的JAR。付出的代价是配置句法错误直到运行时才能检测到,虽然XML schema可以部分解决这个问题,

2)第二个好处是这个文件自身的表达,不需要关心变量间的关系,取而代之,是一个声明式方式,更清楚,用这样的文件表达配置有一些限制,但是这些限制可以减少构件组装时出错的机会。

通常采用命令式模型,即命令计算机按步骤执行。声明式是一个不太清楚的术语,当经常是从命令模型移除变化的方法。将变量移除,使用XML元素表示动作和转换。【译者注:声明式编程是被动式,由框架调度程序执行,通常没有main函数,而命令式是主动式,由自身调度程序执行,通常有main函数】

下面是配置文件的另一个版本:

events

doorClosed D1CL

drawerOpened D2OP

lightOn L1ON

doorOpened D1OP

panelClosed PNCL

end

resetEvents

doorOpened

end

commands

unlockPanel PNUL

lockPanel PNLK

lockDoor D1LK

unlockDoor D1UL

end

state idle

actions {unlockDoor lockPanel}

doorClosed => active

end

state active

drawerOpened => waitingForLight

lightOn => waitingForDrawer

end

state waitingForLight

lightOn => unlockedPanel

end

state waitingForDrawer

drawerOpened => unlockedPanel

end

state unlockedPanel

actions {unlockPanel lockDoor}

panelClosed => idle

end

虽然这种方式的句法可能不熟悉,实际上,这是一种惯用句法,我想这种句法比XML更容易写和读。可能你没有使用这种方式,但是有一点,你需要构造一种团队都熟悉的句法,可以在运行时加载,但不是必须,也可以在编译时加载。

这种语言是一种DSL,首先,它仅仅适用于很小范围,除了这种特别的状态机,不能做其他事情,所以DSL很小,没有控制结构和其他的东西,用这种语言不能完成一个完整的应用,仅仅可以描述一个应用的某个方面,所以DSL必须与其他语言配合工作,同时DSL的“小”意味着容易编辑和处理。

这种方便使创建控制软件更容易,不仅局限于开发人员,一个不熟悉控制器Java核心代码的人,通过读这种代码也可以理解软件是如何工作的,仅仅读DSL,就可以与开发人员进行有效率的交流。

创建一个领域专家和业务分析人员能交流的DSL有许多困难,由于有利于沟通,DSL值得。

现在,再回头看看XML表示,这是DSL?下面阐述一下,它应用XML句法,但还是一个DSL。引出一个问题:一个客户DSL句法和XML句法哪个更好?XML句法更容易解析,确实大部分XML配置文件本质上都是DSL

看看下面代码,针对这个问题,这是一个DSL吗?

event :doorClosed, "D1CL"

event :drawerOpened, "D2OP"

event :lightOn, "L1ON"

event :doorOpened, "D1OP"

event :panelClosed, "PNCL"

command :unlockPanel, "PNUL"

command :lockPanel, "PNLK"

command :lockDoor, "D1LK"

command :unlockDoor, "D1UL"

resetEvents :doorOpened

state :idle do

actions :unlockDoor, :lockPanel

transitions :doorClosed => :active

end

state :active do

transitions :drawerOpened => :waitingForLight,

:lightOn => :waitingForDrawer

end

state :waitingForLight do

transitions :lightOn => :unlockedPanel

end

state :waitingForDrawer do

transitions :drawerOpened => :unlockedPanel

end

state :unlockedPanel do

actions :unlockPanel, :lockDoor

transitions :panelClosed => :idle

end

跟前一个客户语言比起来,它更难懂,但是还算比较清楚,这种方式像RubyRuby有许多句法选择可以使代码易读,可以使Ruby很像客户语言。

Ruby开发者认为这段代码是一个DSL,使用Ruby的部分句法表达了和XML或者客户句法一样的意思。本质上,我使用Ruby的子集作为句法,将DSL嵌在Ruby里。通过DSL的视角审视Ruby,这是个传统的观点,Lisp编程者经常在Lisp创建DSL

有两种形式的文本DSL:外部DSL和内部DSL,一个外部DSL独立于宿主语言【译者注:即不使用宿主语言的句法】,这种语言使用一个客户句法,一个内部DSL使用一般意义语言的句法【译者注:即使用宿主语言的句法】。

嵌入式DSL等同于内部DSL,虽然这种说法广泛,但是嵌入式语言可能与脚本语言混淆,所以避免使用这种术语。例如Excel里的VBAGimp里的Schema

现在回头想想Java配置代码,是DSL吗?我认为不是。这种代码感觉与API绑定,然后上面提到过的Ruby代码更像声明式语言。

这意味着不能使用Java创建内部DSL?例如下面方式:

public class BasicStateMachine extends StateMachineBuilder {

Events doorClosed, drawerOpened, lightOn, panelClosed;

Commands unlockPanel, lockPanel, lockDoor, unlockDoor;

States idle, active, waitingForLight, waitingForDrawer, unlockedPanel;

ResetEvents doorOpened;

protected void defineStateMachine() {

doorClosed. code("D1CL");

drawerOpened. code("D2OP");

lightOn. code("L1ON");

panelClosed.code("PNCL");

doorOpened. code("D1OP");

unlockPanel.code("PNUL");

lockPanel. code("PNLK");

lockDoor. code("D1LK");

unlockDoor. code("D1UL");

idle

.actions(unlockDoor, lockPanel)

.transition(doorClosed).to(active)

;

active

.transition(drawerOpened).to(waitingForLight)

.transition(lightOn). to(waitingForDrawer)

;

waitingForLight

.transition(lightOn).to(unlockedPanel)

;

waitingForDrawer

.transition(drawerOpened).to(unlockedPanel)

;

unlockedPanel

.actions(unlockPanel, lockDoor)

.transition(panelClosed).to(idle)

;

}

}

它的形式古怪,但确实是Java,这叫做DSL,虽然比Ruby DSL更丑陋,但有声明式特征。

什么使得内部DSL不同于一般API?这个比较难于回答,后面将花更多时间讨论。

另一个术语,内部DSL是一个流畅接口(fluent interface),这个术语强调内部DSL仅仅是特殊的API,设计流畅。作为区分,有一个非流畅(nonfluentAPI,通常叫做命令查询API

第4节         语言和语义模型

从这个例子开始,讨论了建立一个状态机模型。模型表示,模型与DSL关系是很重要的问题。在这个例子中,DSL的角色是寄生在状态机模型,因此,我解析客户句法:

events

doorClosed D1CL

创建新的事件对象new Event("doorClosed", "D1CL"),因为有转换doorClosed => active,可以使用addTransition将其添加到转换中。这个模型是状态机行为的引擎。不同于命令查询API,所有的DSL都提供一种易读方式表达模型。【译者注:语义模型,无论使用何种表述方式,其表达的意义相同】

 

图4             解析一个寄生在语义模型上的DSL

DSL的视角,审视语义模型,当讨论编程语言时,通常讨论句法和语义,句法是程序合法的表达式。程序语义表示执行时的行为,领域模型和语义模型很像。

一个观点:语义模型是DSL的核心,一些DSL使用语义模型而有些不使用,但应该总是使用语义模型。

主张语义模型的原因是,它提供清晰解析语言与结果语义的关注点分离,可以说明状态机如何工作,可以增强和调试状态机而不用担心语言问题,可以使用命令查询接口测试状态机模型,可以将状态机模型和DSL分离,可以在使用语言实现之前,添加新的特征。最重要的一点是可以独立于语言测试模型,上面所有的DSL实例都建立在同样的语义模型上,并且建立了一致准确的对象配置。

这个例子中,语义模型是一个对象模型,语义模型也可以是其他的形式,可以使用抽象的数据结构包括所有行为,DSL仅仅提供了模型如何配置的表达机制。使用模型而不是DSL的好处是通过模型属性可以容易配置一个新的客户状态机,而不是DSL。事实上,这种模型的特征是在运行时改变控制器,而不需要编译,DSL不行。通过模型属性可以重用多个控制器,DSL不行,因此DSL仅仅是模型的一个剖面。

模型提供了DSL所没有的一些优点,模型全程可用,在软件中,创建模型,建立抽象可以使得编程更快,好的模型不论是作为库还是框架或者代码发布,都可以很好的工作,不需要DSL

尽管如此,一个DSL可以加强模型的能力,正确的DSL状态机更容易理解,一些DSL可以在运行时配置模型。

DSL的优点是:是一种特殊的模型,更加有效率进行系统编程。如果想改变状态机的行为,可以改变模型对象和它们之间的关系,这种模型称为自适应模型,结果是,为了了解状态机的行为,不需要看代码。

自适应模型很强大,但同时很难使用,因为不能看到任何的特定行为代码。DSL提供一种准确的方式表达代码,因此DSL是有价值的。

自适应模型是一个可选的计算模型,正规的编程语言提供了一种标准的方式来编程状态,自适应模型大部分情况工作的很好,但有时需要不同的方法,例如状态机、产生式规则系统、依赖网络。自适应模型提供一种好的计算模型,DSL比模型更容易编程,本书的后面,将描述一些可选的计算模型,看看这些模型到底是什么样?怎么实现。可能经常听到别人认为DSL是声明式编程。

在讨论这个例子时,先建立模型,然后基于模型创建DSL。因为,这容易理解DSL如何适合软件开发。虽然先建模是一般套路,但是不是仅有套路。例如,通过使用状态机的方法与领域专家讨论,然后创建一个领域专家可以明白的DSL,在这种情况下,几乎同时创建DSL和模型。

第5节         使用代码生成

截止到目前,创建寄生在语义模型的DSL,然后执行语义模型来提供控制器的行为,解析文本立即生成结果。

在语言界,解析型的替代方式是编译型,编译程序文本并生成中间结果,独立于行为。在DSL里,编译方法通常是代码生成。

使用状态机的例子很难解释他们之间的区别,使用另外一个小例子,保险行业对人有一些规则,21岁到40岁之间,这个规则可以是一个DSL,检测申请人资格。

 

图5             解析器单独解析文本然后生成结果

解析情况下,资格处理器解析规则然后生成执行语义模型,当检测申请人资格时,运行语义模型生成结果。

解析这些规则,加载语义模型,执行生成结果。

 

图6             编译器解析文本并且生成中间代码然后再另外的过程执行

在编译的情况下,解析器加载语义模型作为资格处理器创建过程的一部分。在构建过程中,DSL处理器生成一些可以编译的代码,打包,组成资格处理器,也许像某些共享库,然后运行得到结果。

状态机实例说明:在运行时解析配置代码,绑定语义模型,同时可以使用代码生成的方式,避免使用解析器。

代码生成通常不灵活,通常需要额外的编译步骤。为了构建程序,必须首先编译状态机框架和解析器,然后运行解析器生成源码,然后编译源码。这使得构建过程更复杂。

当使用没有工具支撑的DSL时,代码生成有必要,如果要在C环境运行“安全系统”,可以使用代码生成,生成C代码,然后编译运行。

许多DSL文章关注代码生成,演示的首要目的是代码生成,因此,一些文章和书都称赞代码生成的价值。依我看来,代码生成仅仅是一种实现机制,大部分情况下并不需要。

当不使用语义模型时,可能会用到代码生成,解析输入文本然后直接生成代码。虽然使用代码生成DSL是常见的方式,但不建议使用,除了很简单的情况。使用语义模型,可以独立解析、执行语义、代码生成,使得使用简单。语义模型可以转变思路,可以将内部DSL转变成外部DSL,而不用改变代码生成规则,同时,可以容易产生多种输出,而不需要复杂的解析器。可以使用解析模型和代码生成绑定同样的语义模型。

所以在本书,将语义模型作为DSL的核心。

通常使用两种代码生成样式:

(1)一次性代码,使用模板然后手动修改;

(2)无人工干预的代码生成。

我更喜欢后者,因为它可以随意生成代码。在DSL里尤其是这样的,因为DSL首先要表示DSL逻辑,不管什么时候,都能转换DSL。所以必须确信代码生成没有人工干预。

第6节         使用Language Workbench

目前为止介绍了两种DSL(内部DSL和外部DSL),这是对DSL的传统认识,这可能没有得到广泛认可,但是确实有很长历史和大的应用范围,这本书的剩余部分关注于使用成熟和容易获取的工具来使用这种方法。

这个工具叫做Language WorkbenchLanguage Workbench是一个高效创建DSL的工具环境。

使用外部DSL的好处是不受工具限制。大部分人接受高亮句法文本编辑器,Language Workbench不仅仅是解析器,而且是一个编辑环境。

Language Workbench使得DSL设计者超越传统基于文本的编辑,最明显的例子是图形语言,直接使用状态机图规格化保密柜面板状态机。

 

图7             MetaEdit Language Workbench中的保密柜面板状态机

这样的工具不仅可以定义图形语言,也可以从不同的视图查看DSL脚本,在上图中展现一个图形,还展现了状态机和事件列表。

这种多面板可视化编辑环境在很多工具中使用过,但是自己建立一个还是很费劲得,Language Workbench的承诺是可以容易创建这样的编辑环境,使用MetaEdit可以迅速创建类似上图的工具,这个工具允许定义状态机语义模型、图形和列表编辑器、语义模型代码生成器。这个工具确实好,但许多开发者怀疑这种工具。【译者注:在Eclipse社区的GMF有点这个的意思。】

这种工具的好处是可以让非程序员编程,spreadsheets为非程序员提供了编程环境,可能非程序员并没有认为自己在编程。

许多人不认为spreadsheets是编程环境,但是我认为spreadsheets是当前我知道的最成功的编程环境,作为一个编程环境spreadsheets有许多有意思的特性:

1)工具和语言绑定在一起;

2)叫做解说编程(illustrative programming),解说编程不是一个概念,而只是为了说明问题。它也有缺点,缺乏程序结构导致大量的复制粘贴。

Language Workbench是支持开发类似工具的编程平台。

如果完全完成Language Workbench的话,那么对软件开发将是一次变革,现在Language Workbench只是刚开始,只有将来能完全实现,我将用一章讨论Language Workbench

第7节         可视化

Language Workbench的最大好处是能表达更大范围的DSL,尤其是图形化表示,就像在上图看到的那样,你可能发现,上图并不漂亮,这是因为我没有画图,这是从语义模型自动生成的。状态机类不仅能执行,而且可以使用DOT语言响应自己。【译者注:DOT语言是一种文本图形描述语言。它提供了一种简单的描述图形的方法,并且可以为人类和计算机程序所理解。DOT语言文件通常是具有.gv或是.dot的文件扩展名】呈现。

DOT语言是Graphviz包的一部分,Graphviz是一个开源的工具,能描述数学图形结构,然后自动画出数学图形,只需要告诉它点、边、形状和其他的一些东西,它就可以画出图形,使用GraphvizDSL很有用,因为给出一种展现方式。可视化展现如同DSL一样帮助人们理解模型。

可视化不一定是图形,当我写一个解析器时使用文本可视化调试它,有人使用Excel可视化与领域专家沟通,一旦创建了语义模型,添加可视化是比较容易的,注意可视化来源于模型,不用DSL绑定模型时也可以基于模型创建可视化。

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值