18. Sequence Diagrams

image

© Jennifer M. Kohnke

Sequence diagrams are the most common of the dynamic models drawn by UML users. As you might expect, UML provides lots and lots of goodies to help you draw truly incomprehensible diagrams. In this chapter, we describe those goodies and try to convince you to use them with great restraint.

I once consulted for a team that had decided to create sequence diagrams for every method of every class. Please don’t do this; it’s a terrible waste of time. Use sequence diagrams when you have an immediate need to explain to someone how a group of objects collaborate or when you want to visualize that collaboration for yourself. Use them as a tool that you use occasionally to hone your analytical skills rather than as necessary documentation.

The Basics

I first learned to draw sequence diagrams in 1978. James Grenning, a longtime friend and associate, showed them to me while we were working on a project that involved complex communication protocols between computers connected by modems. What I am going to show you here is very close to the simple notation he taught me then, and it should suffice for the vast majority of sequence diagrams that you will need to draw.

Objects, Lifelines, Messages, and Other Odds and Ends

Figure 18-1 shows a typical sequence diagram. The objects and classes involved in the collaboration are shown at the top. Objects have underlined names; classes do not. The stick figure (actor) at left represents an anonymous object. It is the source and sink of all the messages entering and leaving the collaboration. Not all sequence diagrams have such an anonymous actor, but many do.

Figure 18-1. Typical sequence diagram

image

The dashed lines hanging down from the objects and the actor are called lifelines. A message being sent from one object to another is shown as an arrow between the two lifelines. Each message is labeled with its name. Arguments appear either in the parentheses that follow the name or next to data tokens (the little arrows with the circles on the end). Time is in the vertical dimension, so the lower a message appears, the later it is sent.

The skinny little rectangle on the lifeline of the Page object is called an activation. Activations are optional; most diagrams don’t need them. Activations represent the time that a function executes. In this case, it shows how long the Login function runs. The two messages leaving the activation to the right were sent by the Login method. The unlabeled dashed arrow shows the Login function returning to the actor and passing back a return value.

Note the use of the e variable in the GetEmployee message. This signifies the value returned by GetEmployee. Note also that the Employee object is named e. You guessed it: They’re one and the same. The value that GetEmployee returns is a reference to the Employee object.

Finally, note that because EmployeeDB is a class, its name is not underlined. This can only mean that GetEmployee is a static method. Thus, we’d expect EmployeeDB to be coded as in Listing 18-1.


Listing 18-1. EmployeeDB.cs

public class EmployeeDB
{
  public static Employee GetEmployee(string empid)
  {
    ...
  }
  ...
}


Creation and Destruction

We can show the creation of an object on a sequence diagram by using the convention shown in Figure 18-2. An unlabeled message terminates on the object to be created, not on its lifeline. We would expect ShapeFactory to be implemented as shown in Figure 18-2.

Figure 18-2. Creating an object

image


Listing 18-2. ShapeFactory.cs

public class ShapeFactory
{
  public Shape MakeSquare()
  {
    return new Square();
  }
}


In C#, we don’t explicitly destroy objects. The garbage collector does all the explicit destruction for us. However, there are times when we want to make it clear that we are done with an object and that, as far as we are concerned, the garbage collector can have it.

Figure 18-3 shows how we denote this in UML. The lifeline of the object to be released comes to a premature end at a large X. The message arrow terminating on the X represents the act of releasing the object to the garbage collector.

Figure 18-3. Releasing an object to the garbage collector

image

Listing 18-3 shows the implementation we might expect from this diagram. Note that the Clear method sets the topNode variable to null. Since it is the only object that holds a reference to that TreeNode instance, the TreeMap will be released to the garbage collector.


Listing 18-3. TreeMap.cs

public class TreeMap
{
  private TreeNode topNode;
  public void Clear()
  {
    topNode = null;
  }
}


Simple Loops

You can draw a simple loop in a UML diagram by drawing a box around the messages that repeat. The loop condition is enclosed in brackets and is placed somewhere in the box, usually at the lower right. See Figure 18-4.

Figure 18-4. A simple loop

image

This is a useful notational convention. However, it is not wise to try to capture algorithms in sequence diagrams. Sequence diagrams should be used to expose the connections between objects, not the nitty-gritty details of an algorithm.

Cases and Scenarios

Don’t draw sequence diagrams like Figure 18-5, with lots of objects and scores of messages. Nobody can read them. Nobody will read them. They’re a huge waste of time. Rather, learn how to draw a few smaller sequence diagrams that capture the essence of what you are trying to do. Each sequence diagram should fit on a single page, with plenty of room left for explanatory text. You should not have to shrink the icons down to tiny sizes to get them to fit on the page.

Figure 18-5. An overly complex sequence diagram

image

Also, don’t draw dozens or hundreds of sequence diagrams. If you have too many, they won’t be read. Find out what’s common about all the scenarios and focus on that. In the world of UML diagrams, commonalities are much more important than differences. Use your diagrams to show common themes and common practices. Don’t use them to document every little detail. If you really need to draw a sequence diagram to describe the way messages flow, do them succinctly and sparingly. Draw as few of them as possible.

First, ask yourself whether the sequence diagram is even necessary. Code is often more communicative and economical. Listing 18-4, for example, shows what the code for the Payroll class might look like. This code is very expressive and stands on its own. We don’t need the sequence diagram to understand it, so there’s no need to draw the sequence diagram. When code can stand on its own, diagrams are redundant and wasteful.


Listing 18-4. Payroll.cs

public class Payroll
{
  private PayrollDB itsPayrollDB;
  private PaymentDisposition itsDisposition;
  public void DoPayroll()
  {
    ArrayList employeeList = itsPayrollDB.GetEmployeeList();
    foreach (Employee e in employeeList)
    {
      if (e.IsPayDay())
      {
        double pay = e.CalculatePay();
        double deductions = e.CalculateDeductions();
        itsDisposition.SendPayment(pay - deductions);
      }
    }
  }
}


Can code really be used to describe part of a system? In fact, this should be a goal of the developers and designers. The team should strive to create code that is expressive and readable. The more the code can describe itself, the fewer diagrams you will need, and the better off the whole project will be.

Second, if you feel that a sequence diagram is necessary, ask yourself whether there is a way to split it up into a small group of scenarios. For example, we could break the large sequence diagram in Figure 18-5 into several much smaller sequence diagrams that would be much easier to read. Consider how much easier the small scenario in Figure 18-6 is to understand.

Third, think about what you are trying to depict. Are you trying to show the details of a low-level operation, as in Figure 18-6, which shows how to calculate hourly pay? Or are you trying to show a high-level view of the overall flow of the system, as in Figure 18-7? In general, high-level diagrams are more useful than low-level ones. High-level diagrams help the reader tie the system together mentally. They expose commonalities more than differences.

Figure 18-6. One small scenario

image

Figure 18-7. A high-level view

image

Advanced Concepts

Loops and Conditions

It is possible to draw a sequence diagram that completely specifies an algorithm. Figure 18-8 shows the payroll algorithm, complete with well-specified loops and if statements.

Figure 18-8. Sequence diagram with loops and conditions

image

The payEmployee message is prefixed with a recurrence expression that looks like this:

*[foreach id in idList]

The star tells us that this is an iteration; the message will be sent repeatedly until the guard expression in the brackets is false. Although UML has a specific syntax for guard expressions, I find it more useful to use a C#-like pseudocode that suggests the use of an iterator or a foreach.

The payEmployee message terminates on an activation rectangle that is touching, but offset from, the first. This denotes that there are now two functions executing in the same object. Since the payEmployee message is recurrent, the second activation will also be recurrent, and so all the messages depending from it will be part of the loop.

Note the activation that is near the [payday] guard. This denotes an if statement. The second activation gets control only if the guard condition is true. Thus, if isPayDay returns true, calculatePay, calculateDeductions, and sendPayment will be executed; otherwise, they won’t be.

The fact that it is possible to capture all the details of an algorithm in a sequence diagram should not be construed as a license to capture all your algorithms in this manner. The depiction of algorithms in UML is clunky at best. Code such as Listing 18-4 is a much better way of expressing an algorithm.

Messages That Take Time

Usually, we don’t consider the time it takes to send a message from one object to another. In most OO languages, that time is virtually instantaneous. That’s why we draw the message lines horizontally: They don’t take any time. In some cases, however, messages do take time to send. We could be trying to send a message across a network boundary or in a system where the thread of control can break between the invocation and execution of a method. When this is possible, we can denote it by using angled lines, as shown in Figure 18-9.

Figure 18-9. Normal phone call

image

This figure shows a phone call being made. This sequence diagram has three objects. The caller is the person making the call. The callee is the person being called. The telco is the telephone company.

Lifting the phone from the receiver sends the off-hook message to the telco, which responds with a dial tone. Having received the dial tone, the caller dials the phone number of the callee. The telco responds by ringing the callee and playing a ringback tone to the caller. The callee picks up the phone in response to the ring. The telco makes the connection. The callee says “Hello,” and the phone call has succeeded.

However, there is another possibility, which demonstrates the usefulness of these kinds of diagrams. Look carefully at Figure 18-10. Note that the diagram starts exactly the same. However, just before the phone rings, the callee picks it up to make a call. The caller is now connected to the callee, but neither party knows it. The caller is waiting for a “Hello,” and the callee is waiting for a dial tone. The callee eventually hangs up in frustration, and the caller hears a dial tone.

Figure 18-10. Failed phone call

image

The crossing of the two arrows in Figure 18-10 is called a race condition. Race conditions occur when two asynchronous entities can simultaneously invoke incompatible operations. In our case, the telco invoked the ring operation, and the callee went off hook. At this point, the parties all had a different notion of the state of the system. The caller was waiting for “Hello,” the telco thought its job was done, and the callee was waiting for a dial tone.

Race conditions in software systems can be remarkably difficult to discover and debug. These diagrams can be helpful in finding and diagnosing them. Mostly, they are useful in explaining them to others, once discovered.

Asynchronous Messages

When you send a message to an object, you usually don’t expect to get control back until the receiving object has finished executing. Messages that behave this way are called synchronous messages. However, in distributed or multithreaded systems, it is possible for the sending object to get control back immediately and for the receiving object to execute in another thread of control. Such messages are called asynchronous messages.

Figure 18-11 shows an asynchronous message. Note that the arrowhead is open instead of filled. Look back at all the other sequence diagrams in this chapter. They were all drawn with synchronous (filled arrowhead) messages. It is the elegance—or perversity; take your pick—of UML that such a subtle difference in the arrowhead can have such a profound difference in the represented behavior.

Figure 18-11. Asynchronous message

image

Previous versions of UML used half-arrowheads to denote asynchronous messages, as shown in Figure 18-12. This is much more visually distinctive. The reader’s eye is immediately drawn to the asymmetry of the arrowhead. Therefore, I continue to use this convention, even though it has been superseded in UML 2.0.

Figure 18-12. Older, better way to depict asynchronous messages

image

Listing 18-5 and 18-6 show code that could correspond to Figure 18-11. Listing 18-5 shows a unit test for the AsynchronousLogger class in Listing 18-6. Note that the LogMessage function returns immediately after queueing the message. Note also that the message is processed in a completely different thread that is started by the constructor. The TestLog class makes sure that the logMessage method behaves asynchronously by first checking whether the message was queued but not processed, then yielding the processor to other threads, and finally by verifying that the message was processed and removed from the queue.

This is just one possible implementation of an asynchronous message. Other implementations are possible. In general, we denote a message to be asynchronous if the caller can expect it to return before the desired operations are performed.


Listing 18-5. TestLog.cs

using System;
using System.Threading;
using NUnit.Framework;

namespace AsynchronousLogger
{
  [TestFixture]
  public class TestLog
  {
    private AsynchronousLogger logger;
    private int messagesLogged;

    [SetUp]
    protected void SetUp()
    {
      messagesLogged = 0;
      logger = new AsynchronousLogger(Console.Out);
      Pause();
    }

    [TearDown]
    protected void TearDown()
    {
      logger.Stop();
    }

    [Test]
    public void OneMessage()
    {
      logger.LogMessage("one message");
      CheckMessagesFlowToLog(1);
    }

    [Test]
    public void TwoConsecutiveMessages()
    {
      logger.LogMessage("another");
      logger.LogMessage("and another");
      CheckMessagesFlowToLog(2);
    }

    [Test]
    public void ManyMessages()
    {
      for (int i = 0; i < 10; i++)
      {
        logger.LogMessage(string.Format("message:{0}", i));
        CheckMessagesFlowToLog(1);
      }
    }

    private void CheckMessagesFlowToLog(int queued)
    {
      CheckQueuedAndLogged(queued, messagesLogged);
      Pause();
      messagesLogged += queued;
      CheckQueuedAndLogged(0, messagesLogged);
    }

    private void CheckQueuedAndLogged(int queued, int logged)
    {
      Assert.AreEqual(queued,
                       logger.MessagesInQueue(), "queued");
      Assert.AreEqual(logged,
                       logger.MessagesLogged(), "logged");
    }

    private void Pause()
    {
      Thread.Sleep(50);
    }
  }
}



Listing 18-6. AsynchronousLogger.cs

using System;
using System.Collections;
using System.IO;
using System.Threading;

namespace AsynchronousLogger
{
  public class AsynchronousLogger
  {
    private ArrayList messages =
      ArrayList.Synchronized(new ArrayList());
    private Thread t;
    private bool running;
    private int logged;
    private TextWriter logStream;

    public AsynchronousLogger(TextWriter stream)
    {
      logStream = stream;
      running = true;
      t = new Thread(new ThreadStart(MainLoggerLoop));
      t.Priority = ThreadPriority.Lowest;
      t.Start();
    }

    private void MainLoggerLoop()
    {

      while (running)
      {
        LogQueuedMessages();
        SleepTillMoreMessagesQueued();
        Thread.Sleep(10); // Remind me to explain this.
      }
    }

    private void LogQueuedMessages()
    {
      while (MessagesInQueue() > 0)
        LogOneMessage();
    }

    private void LogOneMessage()
    {
      string msg = (string) messages[0];
      messages.RemoveAt(0);
      logStream.WriteLine(msg);
      logged++;
    }

    private void SleepTillMoreMessagesQueued()
    {
      lock (messages)
      {
        Monitor.Wait(messages);
      }
    }

    public void LogMessage(String msg)
    {
      messages.Add(msg);
      WakeLoggerThread();
    }

    public int MessagesInQueue()
    {
      return messages.Count;
    }

    public int MessagesLogged()
    {
      return logged;
    }

    public void Stop()
    {
      running = false;
      WakeLoggerThread();
      t.Join();
    }

    private void WakeLoggerThread()
    {

      lock (messages)
      {
        Monitor.PulseAll(messages);
      }
    }
  }
}


Multiple Threads

Asynchronous messages imply multiple threads of control. We can show several different threads of control in a UML diagram by tagging the message name with a thread identifier, as shown in Figure 18-13.

Figure 18-13. Multiple threads of control

image

image

Note that the name of the message is prefixed with an identifier, such as T1, followed by a colon. This identifier names the thread that the message was sent from. In the diagram, the AsynchronousLogger object was created and manipulated by thread T1. The thread that does the message logging, running inside the Log object, is named T2.

As you can see, the thread identifiers don’t necessarily correspond to names in the code. Listing 18-6 does not name the logging thread T2. Rather, the thread identifiers are for the benefit of the diagram.

Active Objects

Sometimes, we want to denote that an object has a separate internal thread. Such objects are known as active objects. They are shown with a bold outline, as in Figure 18-14.

Figure 18-14. Active object

image

Active objects instantiate and control their own threads. There are no restrictions about their methods. Their methods may run in the object’s thread or in the caller’s thread.

Sending Messages to Interfaces

Our AsynchronousLogger class is one way to log messages. What if we wanted our application to be able to use many different kinds of loggers? We’d probably create a Logger interface that declared the LogMessage method and derive our AsynchronousLogger class and all the other implementations from that interface. See Figure 18-15.

Figure 18-15. Simple logger design

image

The application is going to be sending messages to the Logger interface. The application won’t know that the object is an AsychronousLogger. How can we depict this in a sequence diagram?

Figure 18-16 shows the obvious approach. You simply name the object for the interface and be done with it. This may seem to break the rules, since it’s impossible to have an instance of an interface. However, all we are saying here is that the logger object conforms to the Logger type. We aren’t saying that we somehow managed to instantiate a naked interface.

Figure 18-16. Sending to an interface

image

Sometimes, however, we know the type of the object and yet want to show the message being sent to an interface. For example, we might know that we have created an AsynchronousLogger, but we still want to show the application using only the Logger interface. Figure 18-17 shows how this is depicted. We use the interface lollipop on the lifeline of the object.

Figure 18-17. Sending to a derived type through an interface

image

Conclusion

As we have seen, sequence diagrams are a powerful way to communicate the flow of messages in an object-oriented application. We’ve also hinted that they are easy to abuse and easy to overdo.

An occasional sequence diagram on the whiteboard can be invaluable. A very short paper with five or six sequence diagrams denoting the most common interactions in a subsystem can be worth its weight in gold. On the other hand, a document filled with a thousand sequence diagrams is not likely to be worth the paper it’s printed on.

One of the great fallacies of software development in the 1990s was the notion that developers should draw sequence diagrams for all methods before writing the code. This always proves to be a very expensive waste of time. Don’t do it.

Instead, use sequence diagrams as the tool they were intended to be. Use them at a whiteboard to communicate with others in real time. Use them in a terse document to capture the core salient collaborations of the system.

As far as sequence diagrams are concerned, too few is better than too many. You can always draw one later if you find you need it.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
34.229.62.45