The
behavior of a system can be described using a number of techniques -
collaborations or interactions. This activity describes the use of interactions,
specifically sequence diagrams, to describe the behavior of the system. Sequence
diagrams are most useful where the behavior of the system or subsystem can be
primarily described by synchronous messaging. Asynchronous messaging, especially
in event-driven systems, is often more easily described in terms of state
machines and collaborations, allowing a compact way of defining possible
interactions between objects.
Describe Interactions
Between Design Objects
For each use-case realization, you should illustrate the interactions between
its participating design objects by creating one or more sequence diagrams.
Early versions of these may have been created during Activity:
Use-Case Analysis.
Updating the sequence diagrams involves the following steps:
Identify each object that participates in the flow of the use case.
Represent each participating object in a sequence diagram. Make a lifeline
for each participating object in the sequence diagram:
You may use the interfaces realized by the subsystem. This is
preferred in cases where you wish to show that any model element which
realizes the same interface may be used in place of the interface. If
you choose to show interfaces on the sequence diagram, be aware that you
will want to ensure that no messages are sent from the interface to
other objects. The reason for this is that interfaces completely
encapsulate the internal realization of their operations. Therefore, we
cannot be certain that all model elements which realize the interface
will in fact actually be designed the same way. So on sequence diagrams
no messages should be shown being sent from interfaces.
Represent the interaction that takes place with actors. Represent each
actor instance and external object that the participating objects interacts
with by a lifeline in the sequence diagram.
Illustrate the message sending between participating objects. The flow of
events begins at the top of the diagram and continues downward, indicating a
vertical chronological axis. Illustrate the message sending between objects
by creating messages (arrows) between the lifelines. The name of a message
should be the name of the operation invoked by the message. In the early
stages of design, not many operations will be assigned to the objects, so
you may have to leave this information out and give the message a temporary
name; such messages are said to be "unassigned." Later, when you
have found more of the participating objects' operations, you should update
the sequence diagram by "assigning" the messages with these
operations.
Describe what an object does when it receives a message. This is done by
attaching a script to the corresponding message. Place these scripts in the
margin of the diagram. Use either structured text or pseudocode. If you use
pseudocode, be sure to use constructs in the implementation language so that
the implementation of the corresponding operations will be easier. When the
person responsible for an object's class assigns and defines its operations,
the object's scripts will provide a basis for that work.
You document the use-case behavior performed by the
objects in a sequence diagram.
When you have distributed behavior among the objects, you should consider how
the flow will be controlled. You found the objects by assuming they would
interact a certain way in the use-case realization, and have a certain role. As
you distribute behavior, you can begin to test those assumptions. In some parts
of the flow, you may want to use a decentralized structure; in others, you may
prefer a centralized structure.
You may need new objects at this point, for example if you are using a
centralized structure and need a new object to control the flow. Remember that
any object you add to the design model must fulfill the requirements made on the
object model.
Handle All Variants of the Flow of Events
You should describe each flow variant in a separate sequence diagram.
Sequence diagrams are generally preferable to collaboration diagrams as they tend
to be easier to read when the diagram must contain the level of detail we
typically want when designing the system.
Start with describing the basic flow, which is the most common or most
important flow of events. Then describe variants such as exceptional flows. You
do not have to describe all the flows of events, as long as you employ and
exemplify all operations of the participating objects. Given this, very trivial
flows can be omitted, such as those that concern only one object.
Study the use case to see if there are flow variants other than those already
described in requirements capture and analysis, for example, those that depend
on implementation. As you identify new flows, describe each one in a sequence
diagram. Examples of exceptional flows include the following.
Error handling. If an interface reports that an error has
occurred in its communication with some external system, for example, the
use case should deal with this. A possible solution is to open a new
communication route.
Time-out handling. If the user does not reply within a
certain period, the use case should take some special measures.
Handling of erroneous input to the objects that participate in the
use case. Errors like this might stem from incorrect user input.
Handle Optional Parts of the Use Case
You can describe an alternative path of a flow as an optional flow instead of
as a variant. The following list includes two examples of optional flows.
By sending a signal, the actor decides-from a number of options-what the
use case is to do next. The use case has asked the actor to answer yes or no
to a question, for example, or provided the actor with a variety of
functions the system can perform in the use case's current state.
The flow path varies depending on the value of stored attributes or
relationships. The subsequent flow of events depends on the type of data to
be processed.
If you want an optional flow, or any complex sub-flow, to be especially
noticeable, use a separate sequence diagram. Each separate sequence diagram
should be referred to from the sequence diagram for the main flow of events
using scripts, margin text or notes to indicate where the optional or sub-flow
behavior occurs.
In cases where the optional or exceptional flow behavior could occur
anywhere, for example behavior which executes when a particular event occurs,
the sequence diagram for the main flow of events should be annotated to indicate
that when the event occurs, the behavior described in the optional/exceptional
sequence diagram will be executed. Alternately, if there is significant
event-driven behavior, consider using statechart diagrams to describe the
behavior of the system.
Simplify
Sequence Diagrams using Subsystems (optional)
When a use case is realized, the flow of events is usually described in terms
of the executing objects, i.e. as interaction between design objects. To
simplify diagrams and to identify re-usable behavior, there may be a need to
encapsulate a sub-flow of events within a subsystem. When this is done, large
subsections of the sequence diagram are replaced with a single message to the
subsystem. Within the subsystem, a separate sequence diagram may illustrate the
internal interactions within the subsystem that provide the required behavior
.
Sub-sequences of messages within sequence diagrams should be encapsulated
within a subsystem when:
The sub-sequence occurs repeatedly in different use-case realizations;
that is, the same (or similar) messages are sent to the same (or similar)
objects, providing the same end result. The phrase 'similar' is used because
some design work may be needed to make the behavior reusable.
The sub-sequence occurs in only one use-case realization, but it is
expected to be performed repeatedly in future iterations, or in similar
systems in the future. The behavior may make a good reusable component.
The sub-sequence occurs in only one use-case realization, it is complex
but easily encapsulated, needs to be the responsibility of one person or a
team, and provides a well-defined result. In these kinds of situations, the
complex behavior usually requires special technical knowledge, or special
domain knowledge, and as a result is well-suited to encapsulating it within
a subsystem.
The sub-sequence is determined to be encapsulated within a component in
the implementation model. In this case, a subsystem is the appropriate
representation for the component within the design model.
A use-case realization can be described, if necessary, at
several levels in the subsystem hierarchy. The lifelines in the middle diagram
represent subsystems; the interactions in the circles represent the internal
interaction of subsystem members in response to the message.
The advantages of this approach are:
Use-case realizations become less cluttered, especially if the internal
design of some subsystems is complex.
Use-case realizations can be created before the internal designs of
subsystems are created.
Use-case realizations become more generic and easy to change, especially
if a subsystem needs to be substituted with another subsystem.
Example:
Consider the following sequence diagram, which is part of a
realization of the Local Call use case:
In this diagram, the gray classes belong to a Network
Handling subsystem; the other classes belong to a Subscriber Handling subsystem.
This implies that this is a multi-subsystem sequence diagram, i.e. a diagram
where all the objects that participate in the flow of events are included,
regardless of whether their classes lie in different subsystems or not.
As an alternative, we can show invocation of behavior on the
Network Handling subsystem, and the exercise of a particular interface on that
subsystem. Let's assume that the Network Handling subsystem provides an
ICoordinator interface, which is used by the Subscriber Handling subsystem:
The ICoordinator interface is realized by the Coordinator
class within Network Handling. Given this, we can use the Network Handling
subsystem itself and its ICoordinator interface in the sequence diagram, instead
of instances of classes within Network Handling:
Note that the Coordinator, Digit Information, and Network
class instances are substituted by their containing subsystem. All calls to the
subsystem are instead done via the ICoordinator interface.
Showing Interfaces on Lifelines
In order to achieve true substitutability of subsystems realizing the same
interface, only their interface should be visible in interactions (and in
diagrams in general); otherwise the interactions (or diagrams) need to be
changed when subsystems are substituted with each other.
Example:
We can include only the ICoordinator interface,
but not its providing subsystem, in a sequence diagram:
Sending a message to an interface lifeline means that any
subsystem which realizes the interface can be substituted for the interface in
the diagram. Note that the ICoordinator interface lifeline does not have
messages going out from it, since different subsystems realizing the interface
may send different messages. However, if you want to describe what messages
should be sent (or are allowed to be sent) from any subsystem realizing the
interface, such messages can go out from the interface lifeline.
Describe Persistence-Related Behaviors
The whole goal of the object-oriented paradigm is to encapsulate implementation
details. Therefore, with respect to persistence, we would like to have a
persistent object look just like a transient object. We should
not have to be aware that the object is persistent, or treat it any differently
than we would any other object. At least thatâs the goal.
In practice, there may be times when the application needs to control various
aspects of persistence:
when persistent objects are read and written
when persistent objects are deleted
how transactions are managed
how locking and concurrency control is achieved
Writing Persistent Objects
There are two cases to be concerned with here: the initial time the object is
written to the persistent object store, and subsequent times when the
application wants to update the persistent object store with a change to the
object.
In either case, the specific mechanism depends on the operations supported by
the persistence framework. Generally, the mechanism used is to send a message to
the persistence framework to create the persistent object. Once an object is
persistent, the persistence framework is smart enough to detect subsequent
changes to the persistent object and write them to the persistent object store
when necessary (usually when a transaction is committed).
An example of a persistent object being created is shown below:
The object PersistenceMgr is an instance
of VBOS, a persistence framework. The OrderCoordinator creates a persistent
Order by sending it as the argument to a 'createPersistentObject' message to the
PersistenceMgr.
It is generally not necessary to explicitly model this
unless it is important to know that the object is being explicitly stored at a
specific point in some sequence of events. If subsequent operations need to
query the object, the object must exist in the database, and therefore it is
important to know that the object will exist there.
Reading Persistent Objects
Retrieval of objects from the persistent object store is necessary before the
application can send messages to that object. Recall that work in an
object-oriented system is performed by sending messages to objects. But if the
object that you want to send a message to is in the database but not yet in
memory, you have a problem: you cannot send a message to something which does
not yet exist!
In short, you need to send a message to an object that knows how to query the
database, retrieve the correct object, and instantiate it. Then, and only then,
can you send the original message you originally intended. The object that
instantiates a persistent object is sometimes called a factory object.
A factory object is responsible for creating instances of
objects, including persistent objects. Given a query, the factory
could be designed to return a set of one or more objects which match the query.
Generally objects are richly connected to one another through their
associations, so it is usually only necessary to retrieve the root
object in an object graph; the rest are essentially transparently âpulledâ
out of the database by their associations with the root object. (A good
persistence mechanism is smart about this: it only retrieves objects when they
are needed; otherwise, we might end up trying to instantiate a large number of
objects needlessly. Retrieving objects before they are needed is one of the main
performance problems caused by simplistic persistence mechanisms.)
The following example shows how object retrieval from the persistent object
store can be modeled. In an actual sequence diagram, the DBMS would not be
shown, as this should be encapsulated in the factory object.
Deleting Persistent Objects
The problem with persistent objects is, well, they persist! Unlike transient
objects which simply disappear when the process that created them dies,
persistent objects exist until they are explicitly deleted. So itâs important
to delete the object when itâs no longer being used.
Trouble is, this is hard to determine. Just because one application is done
with an object does not mean that all applications, present and future, are
done. And because objects can and do have associations that even they donât
know about, it is not always easy to figure out if it is okay to delete an
object.
In design, this can be represented semantically using state charts:
when the object reaches the end state, it can be said to be released.
Developers responsible for implementing persistent classes can then use the
state chart information to invoke the appropriate persistence mechanism behavior
to release the object. The responsibility of the Designer of the use-case
realization is to invoke the appropriate operations to cause the object to reach
its end state when it is appropriate for the object to be
deleted.
If an object is richly connected to other objects, it may be difficult to
determine whether the object can be deleted. Since a factory
object knows about the structure of the object as well as the objects to which
it is connected, it is often useful to charge the factory object for a class
with the responsibility of determining whether a particular instance can be
deleted. The persistence framework may also provide support for this capability.
Modeling Transactions
Transactions define a set of operation invocations which are atomic;
they are either all performed, or none of them are performed. In the context of
persistence, a transaction defines a set of changes to a set of objects which
are either all performed or none are performed. Transactions provide
consistency, ensuring that sets of objects move from one consistent state to
another.
There are several options for showing transactions in Use Case Realizations:
Textually. Using scripts in the margin of the sequence
diagram, transaction boundaries can be documented as shown below. This
method is simple, and allows any number of mechanisms to be used to
implement the transaction.
Representing transaction boundaries using textual
annotations.
Using Explicit Messages. If the transaction management
mechanism being used uses explicit messages to begin and end transactions,
these messages can be shown explicitly in the sequence diagram, as shown
below:
A sequence diagram showing explicit messages to start and
stop transactions.
Handling Error Conditions
If all operations specified in a transaction cannot be performed (usually
because an error occurred), the transaction is aborted, and all
changes made during the transaction are reversed. Anticipated error conditions
often represent exceptional flows of events in use cases. In other cases, error
conditions occur because of some failure in the system. Error conditions should
be documented in interactions as well. Simple errors and exceptions can be
shown in the interaction where they occur; complex errors and exception may
require their own interactions.
Failure modes of specific objects can be shown on state charts. Conditional
flow of control handling of these failure modes can be shown in the interaction
in which the error or exception occurs.
Handling Concurrency Control
Concurrency describes the control of access to critical system resources in
the course of a transaction. In order to keep the system in a consistent state,
a transaction may require that it have exclusive access to certain key resources
in the system. The exclusivity may include the ability to read a set of objects,
write a set of objects, or both read and write a set of objects.
Letâs look at a simple example of why we might need to restrict access to a
set of objects. Letâs say we a running a simple order entry system. People
call-in to place orders, and in turn we process the orders and ship the orders.
We can view the order as a kind of transaction.
To illustrate the need for concurrency control, letâs say I call in to
order a new pair of hiking boots. When the order is entered into the system, it
checks to see if the hiking boots I want, in the correct size, are in inventory.
If they are, we want to reserve that pair, so that no one else
can purchase them before the order can be shipped out. Once the order is
shipped, the boots are removed from inventory.
During the period between when the order is placed and when it ships, the
boots are in a special state â they are in inventory, but they are
"committed" to my order. If my order gets canceled for some reason (I
change my mind, or my credit card has expired), the boots get returned to
inventory. Once the order is shipped, we will assume that our little company
does not want to keep a record that it once had the boots.
The goal of concurrency, like transactions, is to ensure that the system
moves from one consistent state to another. In addition, concurrency strives to
ensure that a transaction has all the resources it needs to complete its work.
Concurrency control may be implemented in a number of different ways, including
resource locking, semaphores, shared memory latches, and private workspaces.
In an object-oriented system, it is difficult to tell from just the message
patterns whether a particular message might cause a state change on an object.
Also, different implementations may obviate the need to restrict access to
certain types of resources; for example, some implementations provide each
transaction with its own view of the state of the system at the beginning of the
transaction. In this case, other processes may change the state of an object
without affecting the âviewâ of any other executing transactions.
To avoid constraining the implementation, in design we simply want to
indicate the resources to which the transaction must have exclusive access.
Using our earlier example, we want to indicate that we need exclusive access to
the boots that were ordered. A simple alternative is to annotate the description
of the message being sent, indicating that the application needs exclusive
access to the object. The Implementer then can use this information to determine
how best to implement the concurrency requirement. An example sequence diagram
showing annotation of which messages require exclusive access is shown below.
The assumption is that all locks are released when the transaction is completed.
An example showing annotated access control in a sequence
diagram.
The reason for not restricting access to all objects needed in a transaction
is that often only a few objects should have access restrictions; restricting
access to all objects participating in a transaction wastes valuable resources
and could create, rather than prevent, performance bottlenecks.
Refine the Flow of Events Description
In the flow of events of the use-case realization you may need to add
additional description to the sequence diagrams, in cases where the flow of
events is not fully clear from just examining the messages sent between
participating objects. Some examples of these cases include cases where timing
annotations, notes on conditional behavior, or clarification of operation
behavior is needed to make it easier for external observers to read the
diagrams.
The flow of events is initially outlined in the Activity:
Use-Case Analysis. In this step you refine the flow of events as needed to
clarify the sequence diagrams.
Often, the name of the operation is not sufficient to understand why the
operation is being performed. Textual notes or scripts in the margin of the
diagram may be needed to clarify the sequence diagram. Textual notes and scripts
may also be needed to represent control flow such as decision steps, looping,
and branching. In addition, textual tags may be needed to correlate extension
points in the use case with specific locations in sequence diagrams.
Previous examples within this activity have illustrated a number of different
ways of annotating sequence diagrams.
Unify Classes and Subsystems
As use cases are realized, you need to unify the identified classes and
subsystems to ensure homogeneity and consistency in the model.
Points to consider:
Names of model elements should describe their function.
Avoid similar names and synonyms because they make it difficult to
distinguish between model elements.
Merge model elements that define similar behavior, or that represent the
same phenomenon.
Merge entity classes that represent the same concept or have the same
attributes, even if their defined behavior is different.
Use inheritance to abstract model elements, which tends to make the model
more robust.
When updating a model element, also update the corresponding flow of
events description of the use-case realizations.
Evaluate Your Results
You should check the design model at this stage to verify that your work is
headed in the right direction. There is no need to review the model in detail,
but you should consider the Checkpoints for the
Design Model while you are working on it.
See especially checkpoints for use-case
realization in the Activity:
Review the Design.