Domain-driven design (DDD) is
an approach to software design that’s based on the premise that the
primary focus should be on the domain and the domain logic (as opposed
to focusing on technological concerns) and that complex domain designs
should be based on a model.
If you aren’t familiar with
DDD, you may want to skip this section, because it focuses specifically
on the use of DSLs in DDD applications.
1. Language-oriented programming in DDD
The reason for using
language-oriented programming is that humans are good at expressing
ideas using a spoken language. While spoken language is generally very
imprecise, people usually settle on a set of terms and phrases that have
specific meanings in a particular context.
Ubiquitous language
is a term used in DDD to describe the way we talk about the software.
The ubiquitous language is a spoken language that’s structured around
the domain model and is used by all members of the team when talking
about the domain.
A ubiquitous language isn’t a
DSL, and a DSL isn’t a ubiquitous language. A ubiquitous language is
used to make communication clearer. Terms from the ubiquitous language
are then used in the code of the system.
A DSL, on the other hand,
can be seen as taking the ubiquitous language and turning it into an
executable language. A DSL isn’t always about a business domain, but
when it is, and when you’re practicing DDD, it’s almost certain that
your DSL will reflect the ubiquitous language closely.
In short, the ubiquitous
language is the language of communication inside a team, whereas a DSL
is a way to express intent. The two can (and hopefully will) be merged
in many scenarios.
|
In some fields, the domain terms
are very explicit. In a Sarbanes-Oxley tracking system, the domain terms
are defined in the law itself. In many fields, some of the terms are
well defined (such as in accounting) but other terms are often more
loosely defined and can vary in different businesses or even different
departments. The term customer
is probably the quintessential example of a loosely defined term. I
once sat in a meeting with two department heads, watching them fight for
3 hours over how the system would define a customer, without any
satisfactory result.
When you’re building
software, you usually need to talk to the domain experts. They can help
clarify what the domain terms are, and from there you can build the
ubiquitous language that you’ll use in the project.
Once you have the
ubiquitous language, you can start looking at what you want to express
in the DSL, and how you can use the ubiquitous language to express that.
2. Applying a DSL in a DDD application
It seems natural, when thinking about DSLs, to add DDD to the mix, doesn’t it?
Figure 1
shows a set of DSLs in a domain-driven application. In most
applications, you’ll have a set of DSLs, each of them targeted at one
specific goal. You’ll also usually have a DSL facade of some kind that
will translate the code-driven API to a more language-oriented API.
There
are quite a few domains where DDD doesn’t make sense. In fact, most of
the DSLs that I use daily aren’t tied to a DDD implementation. They’re
technical DSLs, used for such things as templating, configuration, ETL
(extract, transform, and load), and so on.
Technical DSLs rarely require a
full-fledged domain model or a ubiquitous language because the model
used is usually implicit in the assumptions that we have as software
developers. A templating DSL doesn’t need anything beyond
text-processing instructions, for example. A configuration DSL needs
little beyond knowing what it configures.
But when it comes to business
DSLs, we’re in a much more interesting position. Assuming that we have a CLR application (written in C#, VB.NET, or Boo)
and assuming we’re writing the DSL in Boo, we have immediate and
unlimited access to the domain. This means that, by default, our DSL can
immediately take advantage of all the work that went into building the
ubiquitous language and the domain model.
All the ideas about the
domain model and ubiquitous language are directly applicable and exposed
to the DSL.
Listing 1. A DSL that uses an existing DDD-based domain model
when User.IsPreferred and Order.TotalCost > 1000:
AddDiscountPercentage 5
ApplyFreeShipping
when User.IsNotPreferred and Order.TotalCost > 500:
ApplyFreeShipping
|
Notice that we’re using both IsPreferred and IsNotPreferred—having
both of them means that you get better readability. But consider the
actions that are being performed when the condition specified in the when clause is matched. We aren’t modifying state, like this:
Order.TotalCost = Order.TotalCost - (Order.TotalCost * 0.05) #apply discount
That would probably work, but
it’s a bad way to do it. It’s completely opaque, for one thing. The code
is clear about what it does, but there is no hint about the business
logic and reasoning behind it. There is a distinct difference between
applying a discount for a particular sale offer and applying a discount
because of a coupon, for example, and this code doesn’t explain that.
It’s also probably wrong from the domain perspective, because you will
almost certainly want to keep track of your discounts.
In the domain, we probably would have something like this:
Order.ApplyDiscountPercentage(5)
That would be valid code
that we could put into action as well. But in the DSL, because we
already know what the applicable operations are, we can make it even
more explicit by specifying the discount as an operation with a known
context. This makes those operations into part of the language that we
use when writing functionality with the DSL.