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


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


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


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 eleganceor perversity; take your pickof UML that such a subtle difference in the arrowhead can have such a profound difference in the represented behavior.

Figure 18-11. Asynchronous message


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


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


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


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


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


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





Agile Principles, Patterns, and Practices in C#
Agile Principles, Patterns, and Practices in C#
ISBN: 0131857258
EAN: 2147483647
Year: 2006
Pages: 272

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net