Task: Use-Case Design
This task defines how to refine the products of Use-Case Analysis by developing Design-level Use-Case Realizations.
Disciplines: Analysis & Design
Purpose
  • To refine use-case realizations in terms of interactions
  • To refine requirements on the operations of design classes
  • To refine requirements on the operations of design subsystems and/or their interfaces
  • To refine requirements on the operations of capsules
Relationships
Main Description

The behavior of a system can be described using a number of techniques - collaborations or interactions. This task 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. Asynchronous messages play an important role in real-time or reactive systems, and are used for communication between instances of  Artifact: Capsule.

 UML 1.x Representation

You can use a proxy class to represent the subsystem on sequence diagrams. This proxy class is contained within the subsystem and is used to represent the subsystem in diagrams which do not support the direct use of packages and subsystems as behavioral elements. Use the proxy class in cases where you want to show that a specific subsystem responds to a message. In this case, you can show messages being sent from the subsystem proxy to other objects.

Refer to Differences Between UML 1.x and UML 2.0 for more information.

Steps
Create Use-Case Realizations

The Artifact: Use-Case Realization provides a way to trace behavior in the Design Model back to the Use-Case Model, and it organizes collaborations in the Design Model around the Use-Case concept.

Create a Design Use-Case Realization in the Design Model for each Use Case to be designed. The name for the Design Use-Case Realization should be the same as the associated Use Case, and a "realizes" relationship should be established from the use-case realization to its associated use case.

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 Task: Use-Case Analysis.  Such "analysis versions" of the use-case realizations describe interactions between analysis classes.  They need to be evolved to describe interactions between design elements.

Updating the sequence diagrams involves the following steps:

  • Identify each object that participates in the flow of the use case. This is done by instantiating the design classes and subsystems identified in the Task: Identify Design Elements. In real-time systems, you will also be identifying the capsule instances that participate 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. In order to represent the design subsystems you have some choices:
    • You can show instances of the subsystem on the sequence diagram.
    • You can use the interfaces realized by the subsystem. This is preferred in cases where you'd like to show that any model element which realizes the same interface might 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.
    • You can use the component to represent the subsystem on sequence diagrams. Use the component in cases where you want to show that a specific subsystem responds to a message. In this case, you can show messages being sent from the component to other objects.

    Note that these are system-level sequence diagrams, which show how instances of top level design elements (typically subsystems and subsystem interfaces) interact. Sequence diagrams showing the internal design of subsystems are produced separately, as part of Task: Subsystem Design.

  • Note that active object interactions are typically described using specification collaborations and state machines. They would be used here to show how messages can be sent to active objects by other elements in the system in a larger use-case realization. In typical usage, active objects are encapsulated within subsystems for the purpose of this task, such that the use-case realization consists of a set of interacting subsystems. The interactions define the responsibilities and interfaces of the subsystems. Within the subsystems, active objects represent concurrent threads of execution. The subsystems allow work to be divided between development teams, with the interfaces serving as the formal contracts between the teams. For real-time systems, you will use  Artifact: Capsules to represent the active objects.

    A minor note on showing messages emanating from subsystems: restricting messages only to interfaces reduces coupling between model elements and improves the resiliency of the design. Where possible, you should try to achieve this, and in cases where there are messages emanating from subsystems to non-interface model elements, you should look for opportunities to change these to messages to interfaces to improve decoupling in the model.

  • 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.

Diagram described in accompanying text.

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 might want to use a decentralized structure; in others, you might prefer a centralized structure. For definitions of these variants and recommendations on when to use the two types of structure, see Technique: Sequence Diagrams.

You might 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.

Incorporate Applicable Design Mechanisms

During Task: Architectural Analysis, analysis mechanisms were identified.  During Task: Identify Design Mechanisms, analysis mechanisms re refined into design mechanisms, the mapping from the analysis mechanisms to the design mechanisms is captured in the Software Architecture Document, and the design mechanisms are documented in the Project-Specific Guidelines.  

During this task, Use-Case Design, any applicable design mechanisms are incorporated into the use-case realizations.  The Designer surveys the available design mechanisms and determines those that apply to the use-case realization being developed, working within the recommendations and guidelines documented in the Software Architecture Document and the Design Guidelines.  
Note: Applicable design mechanism may have been identified in Task: Use-Case Analysis, during which analysis classes may have been "tagged" with a particular analysis mechanism, indicating that a particular piece of functionality needed to be handled in the design.  In such a case, the applicable design mechanisms are those associated with the analysis mechanisms that analysis classes participating in the use-case realization were tagged with

The Designer incorporates the applicable design mechanisms into the use-case realizations by including the necessary design elements and design element interactions into the use-case realizations following the rules of use documented in the Design Guidelines.

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 communication diagram as they tend to be easier to read when the diagram must contain the level of detail we typically want in 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. For more information, see Guideline: Statechart Diagram.

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 (for more information, see Task: Subsystem Design).

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 might 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 might make a good reusable component.
  • The sub-sequence occurs in only one use-case realization, but 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 replaceable component (see Concept: Component). In this case, a subsystem is the appropriate representation for the component within the design model.

Diagram described in accompanying text.

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; this is useful for example in parallel development environments (see "How to Work in Parallel").
  • 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:

Diagram described in accompanying text.

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:

Diagram described in accompanying text.

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:

Diagram described in accompanying text.

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:

Diagram described in accompanying text.

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 might 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.

How to Work in Parallel

In some cases it can be appropriate to develop a subsystem more or less independently and in parallel with the development of other subsystems. To achieve this, we must first find subsystem dependencies by identifying the interfaces between them.

The work can be done as follows:

  1. Concentrate on the requirements that affect the interfaces between the subsystems.
  2. Make outlines of the required interfaces, showing the messages that are going to pass over the subsystem borders.
  3. Draw sequence diagrams in terms of subsystems for each use case.
  4. Refine the interfaces needed to provide messages.
  5. Develop each subsystem in parallel, and use the interfaces as synchronization instruments between development teams.

You can also choose whether to arrange the sequence diagrams in term of subsystems or in terms of their interfaces only. In some projects, it might even be necessary to implement the classes providing the interfaces before you continue with the rest of the modeling.

Describe Persistence-Related Behavior

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 might 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:

Diagram described in accompanying text.

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.

Diagram described in accompanying text.

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 might 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 can 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.

Diagram described in accompanying text.

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:

Diagram described in accompanying text.

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 was 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 and 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.

Diagram described in accompanying text.

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 Task: 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 task have illustrated a number of different ways of annotating sequence diagrams.



Unify Design Classes and Subsystems

As use cases are realized, you need to unify the identified design classes and subsystems to ensure homogeneity and consistency in the Design 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 Design Model while you are working on it.

See especially Use-Case Realization in the Task: Review the Design.

More Information