3. Sequence Diagrams
Sequence
diagrams illustrate any interactions within a group of objects that
implement a given scenario. A sequence diagram is aimed at showing the
exact flow of control that one could observe within a system. With a sequence diagram, any observer knows exactly how the system implements a given use case.
3.1. A Look at the Notation
In a
sequence diagram, participating objects are rendered with rectangles
containing the object’s name. The full UML syntax dictates that you use
the notation instance : class to name an object. More commonly, though, you use an informal name (for example, an Order)
that indicates the nature and role of the object in the sequence. The
name is usually underlined. (Note that we are not taking the UML
standard literally here. Ours is a loose interpretation that is much
more practical in the real world.)
A vertical and dashed line departs from each object that participates in the sequence. This line is called the lifeline
and represents the passing of time and, contextually, the life span of
the object. An interaction between two objects in the sequence is
rendered by drawing a solid line ending with a filled arrow between the
lifelines of the objects. The solid line starts from the lifeline of the
caller and ends at the lifeline of the callee. The line is also
decorated with text that commonly indicates the method invoked or the
data returned.
Note
Generally, the
line is decorated with the name of the message sent or received.
However, it is common that the message corresponds to a method.
Therefore, the name of the message matches the name of the method. If
you want to be rigorous, you should detail the signature of the method
being called.
You indicate that an object is active and gains control of the operation by showing what is known as an activation bar. An activation bar is a part of the lifeline that is rendered using a narrow rectangle. (See Figure 13.)
The sequence in Figure 13
illustrates the retrieval of a little piece of information—the price of
an ordered item. As you can see, four different objects participate in
the operation—Order, Order Item, Customer, and Product. It all begins
when the GetPrice method is invoked on an Order object.
Internally, the Order object figures out the ordered item and attempts to retrieve the price. In turn, the Order Item finds a reference to the ordered product and gets the related price. At some point, when the activation bar of the Product object terminates, the Order Item
knows about the standard price of the product. However, it also needs
to check any applicable discount for the customer who actually ordered
it. So the GetDiscountRate method is invoked on a Customer object. At the end, the price of the ordered item is carried back to the original caller.
When
the invoked method returns a value, you typically draw a return line
(dashed this time) that connects callee and caller. This line is
decorated with the logical name assigned to the returned value. This
optional item increases the readability of the diagram and also gives
you a chance to express conditions by using interaction frames. (We’ll
say more about this in a moment.) Return values are optional, however.
Note
An object can invoke a method on the same instance—for example, an Order
object invokes a method on itself. This situation is represented by
drawing an arrow (in a semicircular shape) that starts and ends on the
same activation bar.
3.2. Life and Death of an Object
The lifeline
notation in a UML sequence diagram merely shows the passing of time, but
it says nothing about the creation and destruction of objects. A
special notation exists to indicate explicitly the lifetime of an
object. (See Figure 14.)
To indicate the creation of an object, the creator sends a message labeled with a new
keyword. The message moves from the activation bar of the creator up to
the box of the created object. The destruction of an object is
represented with an X symbol placed at the end of the lifeline at the
precise moment you want the object to be removed from memory.
What the expression
"remove the object from memory" exactly means depends on the language
you use. In C++, the developer needs to personally take care of the
destruction of any previously created objects. In this case, the X symbol indicates an explicit call to the object’s destructor using the C++ delete
keyword. In Java and .NET managed languages, conversely, objects are
removed automatically from memory when they go out of scope (garbage
collection). In this case, the X symbol on the lifeline simply indicates when the object is expected to be no longer available for use.
When
an external object is responsible for destroying a given object, and
the object supports a deterministic disposal model, you place a delete message that ends at the X on the lifeline of the object being destroyed.
Note
In particular, in the .NET Framework an object that implements the IDisposable
interface supports a more deterministic model for destruction. Through
the interface, the object exposes a method for callers to explicity
clean up the internal state and mark the instance for future deletion
care of the garbage collector.
In general, it is
acceptable that as an architect you just don’t bother with also
modeling the destruction of objects. It is reasonable that you leave the
burden of this to the development team and trust their skills.
3.3. Asynchronous Messages
So far, we
haven’t distinguished between synchronous and asynchronous messages in a
sequence diagram. All the messages you have seen so far—those
represented with a solid line ending in a filled arrow—are synchronous
messages. In the case of a synchronous message, the caller waits until
the operation terminates. For example, the caller waits until a given
routine ends. An asynchronous message indicates that the processing
continues without the caller needing to stop and wait for a response.
How does UML represent an asynchronous message?
In UML 2.x, a filled arrowhead shows a synchronous message, whereas a stick arrowhead shows an asynchronous message, as you can see in Figure 15.
Figure 15 updates Figure 14
by using an asynchronous UML message to indicate that after a new
customer has been approved an e-mail message should be sent to the
customer through the mailing system. The CustomerServices
object, though, doesn’t have to wait until the e-mail is delivered. You
typically use asynchronous messages for all interactions with hardware
devices and shared subsystems.
You should pay
attention to the subtle difference existing between the notation used
for synchronous and asynchronous messages. The difference is all in the
arrowhead: a filled one for synchronous messages, and a stick one for
asynchronous messages. Also note that this is the latest notation used
with UML 2.x. In earlier versions of UML, the notation for asynchronous messages was different: a half-stick arrowhead. Table 5 summarizes the notations.
Table 5. Notations for Messages in UML Sequence Diagrams
Notation | Version | Meaning |
---|
| All | Synchronous message |
| Up to UML 1.3 | Asynchronous message |
| Starting with UML 1.4 | Asynchronous message |
3.4. Standing in the Way of Control
Sequence
diagrams are essentially a tool to render how objects interact. They do
not lend themselves very well to model control logic. However, if in a
sequence you need to put some conditions, you can use interaction frames. Interaction frames are a new feature of UML 2.x aimed at expressing conditions and controlling flow in a sequence. Let’s start with the C# code shown here:
class OrderServices
{
public void Create(Order order)
{
// Calculate the total of all orders received from this customer
decimal total = order.Customer.GetAllOrdersAmount();
// Ask the system to suggest a discount rate for existing customers
if (total > 0)
{
string customerID = order.Customer.ID;
CustomerServices svc = new CustomerServices();
decimal rate = svc.SuggestDiscountRate(customerID);
order.SetDiscountRate(rate);
}
// Get the updated total for the order and proceed
decimal orderTotal = order.GetOrderTotal();
.
.
.
// Save the order back to storage
using(DataContext ctx = new DataContext())
{
ctx.Save(order);
}
}
}
The requirement behind
this piece of code is that before processing an order, you check whether
the customer who placed it is a known customer. If the customer has
already placed orders in the past, it is eligible for a discount. The
system suggests an appropriate discount rate that is set within the Order
object. Next, the order is processed by the system and then saved back
to storage. How would you render this logic in UML? In particular, how
would you render the condition? (See Figure 16.)
An interaction
frame delimits a region of a sequence diagram and marks it with an
operator and a Boolean guard. The operator indicates the behavior you
expect, whereas the guard indicates whether or not the sequence in the
frame executes or not. Table 6 lists the most commonly used operators for interaction frames.
Table 6. Common Operators for Interaction Frames
Operator | Meaning |
---|
alt | Indicates
alternative branches. The frame hosts multiple sequences, but only
those with a true Boolean guard execute. Multiple sequences in a frame
are separated with a dashed line. |
opt | The sequence in the frame will execute only if the guard is true. |
par | The frame hosts multiple sequences; all sequences will run in parallel. |
loop | The sequence in the frame will be executed multiple times, based on the guard expression. |
region | Indicates a critical section, and requires that only one thread executes it at a time. |
Finally,
note that interaction frames can be nested if this is really necessary.
The point here is that nested frames augment the complexity, and
decrease the readability, of the resulting diagram. If you have really
complex control logic to express, we suggest that you opt for an
activity diagram—essentially, an object-oriented version of
flowcharts—or even code snippets. The best you can do, though, is break
the sequence down into smaller pieces so that a standard sequence
diagram with simple interaction frames is readable enough.
UML is a
general-purpose modeling language with incorporated concepts and tools
that can be easily applied to a variety of contexts. Adapting the model
to a particular domain might require additional and more specific tools
and elements. For this reason, UML 2.x defines a standard mechanism, known as profiles,
to extend the language. When you create a custom UML profile, you
essentially extend the base metamodel by adding new elements with
specific properties suitable to a given domain.
However, a UML profile
is still a piece of a fixed model. A domain-specific language (DSL) is
something different. A DSL is a domain-specific programming language
created to solve problems in a particular domain. A DSL can be a visual
language or a textual language. In general, it falls somewhere in
between a small programming language and a scripting language. A DSL
doesn’t usually compile to binary code; rather, it is rendered to some
intermediate language. Although it was not created to be a DSL, all in
all SQL can be considered a realistic example of a DSL—it is a language
specific to a domain (database access), and it is invoked by other
applications.
To model your
domain effectively, should you use UML extensions or switch to a DSL?
In our way of looking at things, UML is a general-purpose language,
meaning that it is good at describing any sort of domain, but only
approximately. Or, put another way, it can’t be used to describe any
domain precisely. In the imperfect world where we live, no universal
language can be used to solve all sorts of problems with the same level
of effectiveness. Enter DSLs. A DSL is a language for just one domain
and scenario: it’s a made-to-measure, straight-to-the-point, and ad hoc
language. Its elements come from the domain, not from a fixed metamodel.
By extending UML with a
profile, you transform a general language into something specific and,
in doing so, you use the set of tools the general language makes
available. Sure, it works. But is it effective? By using DSLs, you
create your own language using ad hoc DSL tools, such as those
integrated in Visual Studio. (See http://msdn.microsoft.com/en-us/library/bb126235.aspx.)
This language might not be reusable outside the domain, but it is
direct. And, by the way, are you sure that your profile is reusable as
well?
Because a DSL language
is tailor-made for a domain, generating compile code out of it is
easier. And the DSL tool does it for you. We see the future of
model-driven development passing through DSL rather than using UML as
the modeling tool.