Saturday, May 06, 2006

Executing an internal DSL in multiple contexts

Creating an internal DSL (or embedded DSL) is challenging, but it does provide many advantages. One advantage to expressing your business rules in an internal DSL is the ability to execute them in various contexts. By executing the DSL in various contexts you can generate multiple behaviors from the same business rule. When the rule changes over time, all parts of the system that reference the rule will also be changed.

For example assume you work for a casino and you have been tasked with designing a system that will notify the poker room employees when a new table needs to be opened or when you are looking to open a new table. The rules for opening a table vary based on the stakes of the table and the length of the waiting list. For example, you need more people waiting for a no limit game because people go broke more quickly and you don't want the table to be short-handed shortly after you open it. The rules would be expressed in your DSL like this:
if the '$5-$10 Limit' list is more than 12 then notify the floor to open
if the '$1-$2 No Limit' list is more than 15 then notify the floor to open
if the '$5-$10 Limit' list is more than 8 then notify the brush to announce
if the '$1-$2 No Limit' list is more than 10 then notify the brush to announce
The first context in which I will execute the DSL is the context that notifies the employees.
class ContextOne < DslContext

bubble :than, :is, :list, :the, :to

def more(value)
'> ' + value.to_s
end

def method_missing(sym, *args)
@stakes = sym
eval "List.size_for(sym) #{args.first}"
end

def floor(value)
__position(value, :floor)
end

def brush(value)
__position(value, :brush)
end

def open
__action(:open)
end

def announce
__action(:announce)
end

def __action(to)
{ :action => to }
end

def __position(value, title)
value[:position] = title
value
end

def notify(value)
[@stakes, value]
end

end
ContextOne uses the DSL to check the List for the size per stakes and sends notifications when necessary. This is of course sample code and my List object is just a stub to verify that everything works correctly. I'll add a link to the sample code at the end of the post.

Based on this same script you could execute a second context that returns a list of the different games that are currently being spread.
class ContextTwo < DslContext

bubble :than, :is, :list, :the, :to, :more, :notify, :floor, :open, :brush

def announce
@stakes
end

alias open announce

def method_missing(sym, *args)
@stakes = sym
end

end
As you can see, adding additional contexts is very easy. Another could be added to display all positions that are set up to receive notices.
class ContextThree < DslContext

bubble :than, :is, :list, :the, :to, :more, :notify, :announce, :open, :open

def announce; end
def open; end

def brush(value)
:brush
end

def floor(value)
:floor
end

def method_missing(sym, *args)
true
end

end
Executing a DSL script in multiple contexts begins to blur the line between code and data. The script 'code' can also be executed to do things such as generate reports (i.e. A report of which employees are contacted by the system). The script could also be executed in a context that will show how long before a table will be opened (i.e. the rule states that 15 are needed, the system knows 10 are on the list so it displays the message '5 more people needed before the game can start').

A note on implementation: In my experience it is much easier to create a class per context and execute in the scope of that object. An alternative is to execute the script in the scope of one object that creates a generic object graph. The problem with this approach is finding an object graph that is generic enough to be useful in several situations. Clearly, I prefer the first approach.

Sample Code: http://www.jayfields.com/src/dslcontext.txt
Post a Comment