习惯用语可以是技术性的也可以是领域性的 。 技术模式表示针对常见技术软件问题的解决方案,例如您如何在应用程序(或应用程序套件)中处理验证,安全性和事务数据。 先前的文章集中于使用元编程等技术来收集技术惯用模式。 域模式涉及如何抽象常见的业务问题。 尽管技术模式实际上出现在所有类型的软件中,但是您的域模式却不同,一个业务与另一个业务就不同。 但是,存在用于收集它们的丰富技术,这是本系列以及本系列后面几期的主题。
本文提供了使用DSL技术作为获取域模式的抽象样式的动机。 DSL提供了很多选择,包括它们自己的模式命名法。 Martin Fowler的最新著作是对DSL技术的深入研究(请参阅参考资料 )。 在开始展示特定技术时,在以后的文章中,我将使用他的许多模式名称以及他和我自己的示例的混合。
为什么要为了获得惯用模式而费心创建DSL? 正如我在“ 利用可重用代码,第2部分 ”中所指出的那样,将惯用模式与其余代码区分开来的最佳方法之一就是使其看起来与众不同。 这种视觉上的差异是您不需要使用常规API的直接提示。 同样,使用DSL的目标之一是编写看起来不太像源代码,而更像您要解决的问题的代码。 如果您可以实现此目标(或什至比现在更接近目标),则可以弥合大多数软件项目中的一个重要缺口:开发人员与业务利益相关者之间的沟通。 允许用户阅读您的代码是一个巨大的好处,因为它消除了将您的代码翻译成本地语言的需求,而这是容易出错的任务。 通过使您的代码对知道该软件应该做什么的非技术人员可读,您可以与他们进行更积极的对话。
作为使用这种技术的动力,我将从Fowler的DSL书中借用一个示例(请参阅参考资料 )。 假设我在一家制造软件控制的秘密隔间的公司工作(想像詹姆斯·邦德)。 我公司的一位客户H夫人希望我们在她的卧室里安装一个秘密隔间。 但是,我的公司使用了互联网泡沫破灭后遗留下来的Java™支持的烤面包机来运行该软件。 尽管烤面包机很便宜,但是刷新它们上的软件却很昂贵。 因此,我需要创建基本的密室代码并将其永久地放在烤面包机上,然后找出一种配置每个客户的密室需求的方法。 如您所知,这是现代软件世界中的一个普遍问题:通常不经常更改的行为,以及可以根据具体情况更改的配置。
H太太想要一个秘密小隔间,当您第一次关闭卧室门时,该小隔间会打开,然后打开她的梳妆台的第二个抽屉,最后打开床头灯。 这些活动必须按顺序进行,如果有什么中断顺序,则必须从头开始。 您可以想象将其秘密密室控制为状态机的软件,如图1所示:
底层状态机API很简单。 我创建了一个清单3所示的抽象事件类,它处理状态机中的事件和命令:
public 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;}
我可以用另一种称为简单类的状态机的状态进行建模States
,清单2所示:
public class States {
private State content;
private List transitions = new ArrayList();
private List commands = new ArrayList();
public States(String name, StateMachineBuilder builder) {
super(name, builder);
content = new State(name);
}
State getState() {
return content;
}
public States actions(Commands... identifiers) {
builder.definingState(this);
commands.addAll(Arrays.asList(identifiers));
return this;
}
public TransitionBuilder transition(Events identifier) {
builder.definingState(this);
return new TransitionBuilder(this, identifier);
}
void addTransition(TransitionBuilder arg) {
transitions.add(arg);
}
void produce() {
for (Commands c : commands)
content.addAction(c.getCommand());
for (TransitionBuilder t : transitions)
t.produce();
}
}
清单1和清单2仅在这里供参考。 要解决的有趣问题是状态机配置的表示。 此表示形式是用于安装秘密隔离专区的惯用模式。 清单3显示了状态机的基于Java的配置:
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突出显示了使用Java代码进行状态机配置的几个问题。 首先,通过阅读并不清楚这是状态机的配置。 像大多数Java API一样,它是一堆未经区分的代码。 第二,它是冗长和重复的。 例如,当我为状态机的每个部分设置越来越多的状态和转换时,一遍又一遍地使用变量名。 所有这些重复使代码更难阅读。 第三,此代码无法满足无需重新编译代码即可配置秘密隔离专区的最初目标。
实际上,在Java世界中,您几乎再也看不到这样的代码了,它倾向于使用XML作为配置代码。 用XML编写配置很简单,如清单4所示:
清单4中的代码比Java版本具有多个优势。 首先,我有较晚的绑定,这意味着我可以更改配置代码并将其放入烤面包机中,从而允许XML解析器读取新配置。 其次,对于这个特定问题,此代码更具表达性,因为XML包含了容器运输的概念:状态将其配置包含为子元素。 这有助于消除Java版本中出现的烦人的冗余。 第三,此代码本质上是声明性的。 通常,如果您只是在编写语句而不需要if
和while
语法,则声明性代码通常更易于阅读。
退后一会儿,意识到其中的含义。 外部化配置是现代Java世界中的一种常见模式,因此我们甚至不再将其视为独立的实体。 但是,它实际上是每个Java框架的功能。 配置是一种惯用模式,我们需要捕获它的方法,以将其与周围框架的一般行为区分开来。 通过使用XML进行配置,我可以在外部DSL中编写代码(语法为XML,语法由与此XML文档关联的架构定义),因此无需重新编译框架代码即可更改它。
我并不需要一直使用XML来获得XML的优势。 考虑清单5中所示的配置代码的版本:
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版本的许多优点:它是声明性的,具有容器性的且简洁的。 与XML和Java版本相比,它具有优势,因为它具有较少的噪声字符(例如<
和>
),而噪声字符是技术实现所需的,但会损害可读性。
此版本的配置代码是使用ANTLR编写的自定义外部DSL,ANTLR是一种开放源代码工具,可轻松编写自定义语言(请参阅参考资料 )。 那些仍然对大学的编译器类(包括Lex和YACC这样的经典工具)有噩梦的人会很高兴地知道这些工具已经变得更好。 这个示例来自Fowler的书,他说构建XML版本和构建自定义语言版本花费的时间差不多。
清单6包含另一种用Ruby编写的替代方法:
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
这是内部 DSL的一个很好的例子:使用基本语言语法的DSL,这意味着该DSL必须在语法上是合法的Ruby代码。 (由于它是用Ruby编写的,因此可以通过JRuby运行它,这意味着您的烤面包机所需的只是JRuby JAR文件。)
清单6具有许多与定制语言相同的优点。 请注意,大量使用Ruby块充当容器,这为您提供了与XML和自定义语言版本相同的容器形式语义。 与自定义语言相比,它确实使用了更多的噪音字符。 例如,Ruby中的:
前缀表示一个符号,在这种情况下,它基本上是用作标识符的不可变字符串。
在Ruby中实现这种DSL非常简单,如清单7所示:
class StateMachineBuilder
attr_reader :machine, :events, :states, :commands
def initialize
@events = {}
@states = {}
@state_blocks = {}
@commands = {}
end
def event name, code
@events[name] = Event.new(name.to_s, code)
end
def state name, &block
@states[name] = State.new(name.to_s)
@state_blocks[name] = block
@start_state ||= @states[name]
end
def command name, code
@commands[name] = Command.new(name.to_s, code)
end
Ruby具有关于语法的灵活规则,这使其非常适合此类DSL。 例如,在声明事件时,您不必强制在方法调用中包括括号。 在此版本中,您无需编写自己的语言或用尖括号将自己弄伤。 这有助于说明为什么这种方法在Ruby世界中如此流行。
DSL为捕获惯用模式提供了一种不错的替代语法。 根据Martin Fowler的定义,DSL具有五个关键特征。
要成为DSL,语言必须是计算机编程语言。 没有适当的限制,很容易在湿滑的斜坡上遇到的所有东西都是DSL。 如果您对术语DSL的定义过于宽泛,则所有上下文化的对话都将是DSL。 例如,我有一些板球爱好者。 当他们在谈论板球的时候我走过时,即使他们使用英语单词,我也听不懂他们在说什么。 我缺乏适当的上下文来理解他们使用这些词的方式。 因此,您可能会说板球和其他运动在其术语中包含DSL。 但是,将定义保留得如此宽泛,很难将其限制在有用的约束范围内,因此Fowler坚持将其限制为计算机编程语言。
Fowler对于DSL的第二个标准是它具有“语言性质”,这意味着您的DSL应该至少可以被非程序员理解。 这种语言的性质可以采取多种形式,在以后的文章中,我将继续向您展示许多形式,这是我继续研究使用DSL作为捕获惯用模式的一种方式。
要成为正确的DSL,必须将语言狭义地集中在特定的问题域上。 尝试创建DSL的危害之一就是使其范围太广。 DSL是一种抽象机制,创建过于广泛的抽象会降低抽象的好处。
DSL具有有限的表现力也是很典型的。 例如,很难找到包含控制结构(例如循环和决策)的DSL。 DSL应该特别且仅专注于它试图描述的域。 结果,相当多的DSL是声明性的而不是命令性的。
前面的两个条件表明了此特征,但是我将在这里对其进行形式化。 您的DSL不应是Turing完整的(请参阅参考资料 )。 实际上,DSL意外地成为图灵完整被认为是DSL的反模式。 例如,经典的UNIX® sendmail
配置文件被意外图灵完整。 如果您愿意并且有太多空闲时间,可以将操作系统写入sendmail
配置文件。
意外地使图灵完整变得非常容易。 一些熟悉的基础架构工具意外地实现了这种过渡-例如XSLT。 一种语言是否为DSL的确定有时取决于使用该语言的环境。 使用XSLT将一个文本版本转换为另一文本版本时,您将其用作DSL。 如果使用XSLT解决河内塔的问题,则将其用作图灵完整的语言(并且您可能应该尝试寻找新的爱好)。
在本期中,我为使用DSL作为提取惯用模式的提取机制奠定了基础。 DSL为此非常好用,因为它们很容易与常规API区别开来,它们本质上是声明性的,并且改善了项目开发人员和非开发人员之间的通信反馈回路。 在以后的文章中,我将研究构建DSL的多种技术。 在接下来的几期中,我将演示几种DSL技术,您可以利用它们在代码中进行发现和设计。
翻译自: https://www.ibm.com/developerworks/java/library/j-eaed13/index.html