- 1.1 Gothic Security
- 1.2 The State Machine Model
- 1.3 Programming Miss Grants Controller
- 1.4 Languages and Semantic Model
- 1.5 Using Code Generation
- 1.6 Using Language Workbenches
- 1.7 Visualization
1.3 Programming Miss Grant’s Controller
Now that I’ve implemented the state machine model, I can program Miss Grant’s controller like this:
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);
I look at this last bit of code as quite different in nature from the previous pieces. The earlier code described how to build the state machine model; this last bit of code is about configuring that model for one particular controller. You often see divisions like this. On the one hand is the library, framework, or component implementation code; on the other is configuration or component assembly code. Essentially, it is the separation of common code from variable code. We structure the common code in a set of components that we then configure for different purposes.
Figure 1.3 A single library used with multiple configurations
Here is another way of representing that configuration code:
<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>
This style of representation should look familiar to most readers; I’ve expressed it as an XML file. There are several advantages to doing it this way. One obvious advantage is that now we don’t have to compile a separate Java program for each controller we put into the field—instead, we can just compile the state machine components plus an appropriate parser into a common JAR, and ship the XML file to be read when the machine starts up. Any changes to the behavior of the controller can be done without having to distribute a new JAR. We do, of course, pay for this in that many mistakes in the syntax of the configuration can only be detected at runtime, although various XML schema systems can help with this a bit. I’m also a big fan of extensive testing, which catches most of the errors with compile-time checking, together with other faults that type checking can’t spot. With this kind of testing in place, I worry much less about moving error detection to runtime.
A second advantage is in the expressiveness of the file itself. We no longer need to worry about the details of making connections through variables. Instead, we have a declarative approach that in many ways reads much more clearly. We’re also limited in that we can only express configuration in this file—limitations like this are often helpful because they can reduce the chances of people making mistakes in the component assembly code.
You often hear people talk about this kind of thing as declarative programming. Our usual model is the imperative model, where we command the computer by a sequence of steps. “Declarative” is a very cloudy term, but it generally applies to approaches that move away from the imperative model. Here we take a step in that direction: We move away from variable shuffling and represent the actions and transitions within a state by subelements in XML.
These advantages are why so many frameworks in Java and C# are configured with XML configuration files. These days, it sometimes feels like you’re doing more programming with XML than with your main programming language.
Here’s another version of the configuration code:
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
This is code, although not in a syntax that’s familiar to you. In fact, it’s a custom syntax that I made up for this example. I think it’s a syntax that’s easier to write and, above all, easier to read than the XML syntax. It’s terser and avoids a lot of the quoting and noise characters that the XML suffers from. You probably wouldn’t have done it exactly the same way, but the point is that you can construct whatever syntax you and your team prefer. You can still load it in at runtime (like the XML) but you don’t have to (as you don’t with the XML) if you want it at compile time.
This language is a domain-specific language that shares many of the characteristics of DSLs. First, it’s suitable only for a very narrow purpose—it can’t do anything other than configure this particular kind of state machine. As a result, the DSL is very simple—there’s no facility for control structures or anything else. It’s not even Turing-complete. You couldn’t write a whole application in this language; all you can do is describe one small aspect of an application. As a result, the DSL has to be combined with other languages to get anything done. But the simplicity of the DSL means it’s easy to edit and process.
This simplicity makes it easier for those who write the controller software to understand it—but also may make the behavior visible beyond the developers themselves. The people who set up the system may be able to look at this code and understand how it’s supposed to work, even though they don’t understand the core Java code in the controller itself. Even if they only read the DSL, that may be enough to spot errors or to communicate effectively with the Java developers. While there are many practical difficulties in building a DSL that acts as a communication medium with domain experts and business analysts like this, the benefit of bridging the most difficult communication gap in software development is usually worth the attempt.
Now look again at the XML representation. Is this a DSL? I would argue that it is. It’s wrapped in an XML carrier syntax—but it’s still a DSL. This example thus raises a design issue: Is it better to have a custom syntax for a DSL or an XML syntax? The XML syntax can be easier to parse since people are so familiar with parsing XML. (However, it took me about the same amount of time to write the parser for the custom syntax as it did for the XML.) I’d contend that the custom syntax is much easier to read, at least in this case. But however you view this choice, the core tradeoffs of DSLs are the same. Indeed, you can argue that most XML configuration files are essentially DSLs.
Now look at this code. Does this look like a DSL for this problem?
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
It’s a bit noisier than the custom language earlier, but still pretty clear. Readers whose language likings are similar to mine will probably recognize it as Ruby. Ruby gives me a lot of syntactic options that make for more readable code, so I can make it look very similar to the custom language.
Ruby developers would consider this code to be a DSL. I use a subset of the capabilities of Ruby and capture the same ideas as with our XML and custom syntax. Essentially I’m embedding the DSL into Ruby, using a subset of Ruby as my syntax. To an extent, this is more a matter of attitude than of anything else. I’m choosing to look at the Ruby code through DSL glasses. But it’s a point of view with a long tradition—Lisp programmers often think of creating DSLs inside Lisp.
This brings me to pointing out that there are two kinds of textual DSLs which I call external and internal DSLs. An external DSL is a domain-specific language represented in a separate language to the main programming language it’s working with. This language may use a custom syntax, or it may follow the syntax of another representation such as XML. An internal DSL is a DSL represented within the syntax of a general-purpose language. It’s a stylized use of that language for a domain-specific purpose.
You may also hear the term embedded DSL as a synonym for internal DSL. Although it is fairly widely used, I avoid this term because “embedded language” may also apply to scripting languages embedded within applications, such as VBA in Excel or Scheme in the Gimp.
Now think again about the original Java configuration code. Is this a DSL? I would argue that it isn’t. That code feels like stitching together with an API, while the Ruby code above has more of the feel of a declarative language. Does this mean you can’t do an internal DSL in Java? How about this:
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) ; } }
It’s formatted oddly, and uses some unusual programming conventions, but it is valid Java. This I would call a DSL; although it’s more messy than the Ruby DSL, it still has that declarative flow that a DSL needs.
What makes an internal DSL different from a normal API? This is a tough question that I’ll spend more time on later (“Fluent and Command-Query APIs”), but it comes down to the rather fuzzy notion of a language-like flow.
Another term you may come across for an internal DSL is a fluent interface. This term emphasizes the fact that an internal DSL is really just a particular kind of API, designed with this elusive quality of fluency. Given this distinction, it’s useful to have a name for a nonfluent API—I’ll use the term command-query API.