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.
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:
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.
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.
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.
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:
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:
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.
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.
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:
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.
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.
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.
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:
Representing transaction boundaries using textual annotations.
A sequence diagram showing explicit messages to start and stop transactions.
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.
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.
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.
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:
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.