Real Time UML: Advances in the UML for Real-Time Systems (3rd Edition)

Most classes, objects, and interfaces are small things. To do anything system-wide, many of these small things need to work together. And to work together, they must relate in some way. The UML provides three primary kinds of relations association, generalization, and dependency to link together your model elements so that they may collaborate, share information, and so on. The most fundamental of these is association, because associations enable collaborations of objects to invoke services, and will be considered first.

2.3.1 Associations

The most fundamental kind of relation between classes in the UML is the association. An association is a design-time relation between classes that specifies that, at runtime, instance of those classes may have a link and be able to request services of one another.

The UML defines three distinct kinds of associations: association, aggregation, and composition. An association between classes means simply that at some time during the execution of the system objects instantiated from those classes may have a link that enables them to call or somehow invoke services of the other. Nothing is stated about how that is accomplished, or even whether it is a synchronous method call (although this is most common) or some kind of distributed or asynchronous message transfer. Think of associations as conduits that allow objects to find each other at runtime and exchange messages. Associations are shown as lines connecting classes on class diagrams.

The direction of navigability of the association may be specified by adding an (open) arrowhead. A normal line with no arrowheads (or showing an arrowhead at both ends) means that the association is bi-directional; that is, an object at either end of the association may send a message to an object at the other end. If only one of the objects can send a message to the other and not vice versa, then we add an open arrowhead (we'll see later that the type of arrowhead matters) pointing in the direction of the message flow. Thus we see that an AlarmingObject object can send a message to an AlarmManager object, but not vice versa. This does not imply that the AlarmingObject object cannot retrieve a value from an AlarmManager object because it can call a method that returns such a value. It means, however, that an object of type Alarming Manager cannot spontaneously send a message to an AlarmingObject object because it doesn't know, by design, how to find it.

Every association has two or more ends; these ends may have role names. These name the instances with respect to the class at the other end of the association. It is common practice to give the role name on the opposite end of the association to the pointer that points to that instance. For example, in Figure 2-6, the Alarm class might contain a pointer named myView to an instance of type TextView. To invoke a service on the linked instance of TextView an action in an operation in the Alarm class would deference the pointer, as in

myView->setText("Help!")

Figure 2-6. Simple Association

Although somewhat less common, association labels may also be used, such as between the AlarmingClass and AlarmManager classes. The label is normally used to help explain why the association exists between the two classes. In this case, the label "Creates alarms for" indicates that is how the AlarmingClass intends to use the AlarmManager. To get the directionality of the label you can add an arrowhead next to the label to show the speaking perspective. The UML specification calls out an arrowhead with no associated arrow (as shown between the Alarm Manager and ListView classes), but more commonly tools use the characters "^", ">", "<", and "V" in the label itself to indicate speaking perspective. Note also that the speaking perspective of the label is unrelated to the association direction. In the association between Alarm Manager and ListView messages flow from the AlarmManager only, but the association label speaks from the ListView perspective.

The multiplicity is probably the most important property of an association end. The multiplicity of an association end indicates the possible numbers of instances that can participate in the association role at runtime. This may be

  • A fixed number, such as "1" or "3"

  • A comma separated list, such as "0,1" or "3,5,7"

  • A range, such as "1..10"

  • A combination of a list and a range, such as "1..10, 25", which means "one to 10, inclusive, or 25"

  • An asterisk, which means "zero or more"

  • An asterisk with an endpoint, such as "1..*" which means "one or more"

In Figure 2-6, we see multiplicities on all the associations. Multiplicity is shown at the role end of the class to which it applies. Thus each Alarm object associates with zero or one TextView objects, and each ListView object associates with exactly zero or more TextView objects. The AlarmManager uses instance multiplicity to show that there is exactly one instance in the context of the shown collaboration. Since there is only one, it isn't necessary to show a 1 on all the association ends that attach to it. All of these adornments, except for perhaps multiplicity, are optional and may be added as desired to further clarify the relation between the respective classes.

An association between classes means that at some point during the lifecycle of instances of the associated classes, there may be a link that enables them to exchange messages. Nothing is stated or implied about which of these objects comes into existence first, which other object creates them, or how the link is formed.

In UML 1.x, an associative class was a special kind of class that was used when the association itself had features of interest. Perhaps the most common example is the association between a Man class and a Woman class called "Marriage." The marriage has attributes, such as date, duration, and location, and operations such as createPrenuptual Agreement(), which are not really features of the collaborating objects themselves. In UML 2.0, associative classes were discarded, and all associations may have these features, as desired. Figure 2-7 shows such an associative class notation where the Charging association has attributes of the connection between the Battery and Charger.

Figure 2-7. Association, Aggregation, and Composition

2.3.2 Aggregation

An aggregation is a specialized kind of association that indicates a "whole-part" relation exists between two classes. The "whole" end is marked with a white diamond, as in Figure 2-7. For example, consider, the classes Message List and Message. The Message List class is clearly a "whole" that aggregates possibly many Message elements. The diamond on the aggregation relation shows that the Message List is the "whole." The asterisk (*) on the myMsg association end indicates that the list may contain zero or more Message elements. If we desired to constrain this to be no more than 100 messages we could have made the multiplicity "0..100."

Since aggregation is a specialized form of association, all of the properties and adornments that apply to associations also apply to aggregations, including navigation, multiplicity, role names, and association labels.

Aggregation is a relatively weak form of "whole-part," as we'll see in a moment. No statement is made about lifecycle dependency or creation/destruction responsibility. Indeed, aggregation is normally treated in design and implementation identically to association. Nevertheless, it can be useful to aid in understanding the model structure and the relations among the conceptual elements from the problem domain.

2.3.3 Composition

Composition is a strong form of aggregation in which the "whole" (also known as the "composite") has the explicit responsibility for the creation and destruction of the part objects. Because of this, the composite exists before the parts come into existence and continues to exist (although sometimes not for very long) after they are destroyed. If the parts have a fixed multiplicity with respect to the composite, then it is common for the composite to create those parts in its constructor and destroy them in its destructor. With nonfixed multiplicities, the composite dynamically creates and destroys the part objects during its execution. Because the composite has creation and destruction responsibility, each part object can only be owned by a single composite object, although the part objects may participate in other association and aggregation relations. Composition is also a kind of association so it can likewise have all of the adornments available to ordinary associations.

Composition has two common presentations: nested class boxes and a filled-in diamond. Figure 2-7 shows both forms. The Power Subsystem, for example, is a composite class that contains parts of type Charger, Battery, and Switch. The Button class is also a composite that contains a single Light part. These parts are not objects exactly they are object roles. These roles are played by objects executing in the system at runtime but may be played by different objects at different times. Similarly, the lines connecting the parts are not links (which occur between objects) but connectors that occur between roles.

With the containment presentation, there is an issue as to how to show the multiplicity of the part (by definition, the multiplicity on the whole end of a composition is exactly 1). Since there is no line on which to place the multiplicity, it is common to put the multiplicity in one of the upper corners of the part class or in square brackets after the part name. This is called instance multiplicity. We see that the Power Subsystem contains either one or two objects of type Switch, zero or more objects of type Charger, and zero to two objects of type Battery. The Display Subsystem has exactly one Knob but an unspecified number of Messages.

The two forms of composition are subtly different. The filled-in diamond shows relations between classes, but when these classes are nested within the composite class they need to be objects, or more precisely, object roles (called "parts"). In addition to making the classes into parts, the composite class environment may add additional constraints, often in terms of more precisely specifying multiplicity. Figure 2-8 shows an example of this, a set of composition relations with the filled diamond at the top and a representation of the very same system with the nested notation below. Notice that the composition role names in the upper part of the figure become the part names in the lower part. Also note that the multiplicity of the parts is, in this case, more precisely specified for example, "*" Windows becomes "2" window parts in the lower figure. The refinement of multiplicities is optional "*" could have remained "*" had we desired.

Figure 2-8. Composition and Parts

The most common implementation of an association, as we will see later in Code Listing 2-4, is an object pointer (in C++) or an object reference (in Java). This is true regardless of which kind of association it is an ordinary association, an aggregation, or a composition. There are many other ways of implementing an association including nested class declaration, object identifier reference (as in a MS Windows handle or a CORBA object ID), an operating system task ID, and so on but using a pointer is the most common.

Composition plays a very important role in the scalability of the UML by enabling objects to be defined in terms of parts, which are defined to be roles played by objects in the running system. These (part) objects themselves are typed by their class, of course, and those classes may themselves have parts. Thus, in UML we have the ability nest composite structures arbitrarily deeply a crucial feature for scalability of the UML to very large systems. This important topic will be discussed soon in Section 2.4.2.

2.3.3.1 Stereotypes

In a couple of places in Figure 2-7 notice that a class has a special adornment called a stereotype. Stereotypes are used in several ways. In one sense, a stereotype simply "metatypes" the role of a commonly used icon, such as the rectangle in Figure 2-7. A subsystem is special kind of class but we still use a class box to show it. To indicate that we mean a subsystem and not an ordinary class, we add the stereotypes. Subsystems are discussed later in this chapter, in Section 2.4.

The other usage of a stereotype is to tailor the UML to meet a specific need or purpose. It is part of the lightweight extension mechanism defined within the UML. A stereotype is a user-defined kind of element that is based on some already-defined element in the UML, such as Class, Operation, Association, and so on. You can create your own stereotypes if you wish, adding your problem-domain vocabulary to the UML. Stereotypes must always "subtype" an existing metaclass, such as Class, Component, Package, Association, and so on already defined in the UML specification. Stereotypes are usually shown by attaching the stereotype name in guillemots with the stereotyped element or shown using a user-defined icon.

2.3.4 Generalization

The generalization relation in the UML means that one class defines a set of features, which are either specialized or extended in another. Generalization may be thought of as an "is a type of" relation and therefore only as having a design-time impact, rather than a runtime impact.

Generalization has many uses in class models. First, generalization is used as a means to ensure interface compliance, much in the same way that interfaces are used. Indeed, it is the most common way to implement interfaces in languages that do not have interfaces as a native concept, such as in C++. Also, generalization can simplify your class models because you can abstract a set of features common to a number of classes into a single superclass, rather than redefining the same structure independently in many different classes. In addition, generalization allows for different realizations to be used interchangeably; for example, one realization subclass might optimize worst-case performance while another optimizes memory size and yet another optimizes reliability because of internal redundancy.

Generalization in the UML means two things. First, it means inheritance that subclasses have (at least) the same attributes, operations, methods, and relations as the superclasses they specialize. Of course, if the subclasses were identical with their superclasses, that would be boring, so subclasses can differ from their superclasses in either or both of two ways by specialization or by extension.

Subclasses can specialize operations or state machines of their superclasses. Specializing means that the same operation (or action list on the statechart) is implemented differently than in the superclass. This is commonly called polymorphism. In order for this to work, when a class has an association with another which is a superclass, at runtime an instance of the first can invoke an operation declared in the second, and if the link is actually to an subclass instance, the operation of the subclass is invoked rather than that of the superclass.

This is much easier to see in the example presented in Figure 2-9. The class MsgQueue is a superclass and defines standard queue-like behavior, storing Message objects in a FIFO fashion with operations such as insert() and remove(). CachedQueue specializes and extends MsgQueue (the closed arrowhead on the generalization line points to the more general class). The Communicator class associates with the base class MsgQueue. If it needs to store only a few messages, a standard in-memory queue, that is, an instance of MsgQueue, works fine. But what if some particular instance of Communicator needs to store millions of messages? In that case, the instance can link to an instance of the Cached Queue subclass. Whether Communicator actually links to an instance of MsgQueue or one of its subclasses is unknown to the instance of Communicator. It calls the insert() or remove() operations as necessary. If the connected instance is of class MsgQueue, then the correct operations for that class are called; if the connected instance is of class CachedQueue, then the operations for that class are invoked instead, but the client of the queue doesn't know (or care) which is invoked.

Figure 2-9. Generalization

It is common not to show methods in the subclass unless they override (redefine) methods inherited from the superclass, but this is merely a stylistic convention. Remember that a CachedQueue is a MsgQueue, so that everything that is true about the latter is also true of the former, including the attributes, operations, and relations. For example, CachedQueue aggregates zero or more Message objects and has a composition relation to the class Semaphore because its superclass does. However, in this case, the operations for insert and remove are likely to work differently.

For example, MsgQueue::insert() might be written as shown in Code Listing 2-1:

Code Listing 2-1. MsgQueue::insert() Operation

void MsgQueue::insert(Message m) { if (isFull()) throw OVERFLOW; else { head = (head + 1) % size; list[head] = m; }; };

However, the code for the insert operation in the subclass must be more complex. First, note that the subclass contains (via composition) two MsgQueues, one for input buffering and one for output buffering. The CachedQueue::insert() operation only uses the MsgQueue instance playing the inputQueue role. If this is full, then it must write the buffer out to disk and zero out the buffer. The code to do this is shown in Code Listing 2-2.

Code Listing 2-2. CachedQueue::insert() Operation

void CachedQueue::insert(Message m) { if (inputQueue->isFull()) { // flush the full queue to disk and then // clear it flush(); inputQueue->clear(); }; inputQueue->insert(m); };

Similarly, the operations for remove(), getSize(), clear(), isEmpty(), and isFull() need to be overridden as well to take into account the use of two internal queues and a disk file.

Note that in the UML attributes cannot be specialized. If the superclass defines an attribute of time sensedValue and it has a type int, then all subclasses also have that attribute and it is of the same type. If you need to change the type of an attribute, you should use the «bind» stereotype of dependency, a topic discussed in Section 2.3.5. Subclasses can also extend the superclass that is, they can have new attributes, operations, states, transitions, relations, and so on. In Figure 2-9, CachedQueue extends its base class by adding attributes filename and f and by adding operations flush() and load().

The other thing that generalization means in the UML is substitutability; this means that any place an instance of the superclass was used, an instance of the subclass can also be used without breaking the system in any overt way. Substitutability is what makes generalization immensely useful in designs.

2.3.5 Dependency

Association, in its various forms, and generalization are the key relations defined within the UML. Nevertheless, there are several more relations that are useful. They are put under the umbrella of dependency. The UML defines four different primary kinds of dependency abstraction, binding, usage, and permission, each of which may be further stereotyped. For example, «refine» and «realize» are both stereotypes of the abstraction relationship, and «friend» is a stereotype of permission. All of these special forms of dependency are shown as a stereotyped dependency (a dashed line with an open arrowhead).

Arguably, the most useful stereotypes of dependency are «bind», «usage», and «friend». Certainly, they are the most commonly seen, but there are others. The reader is referred to [2] for the complete list of "official" stereotypes.

The «bind» stereotype binds a set of actual parameters to a formal parameter list. This is used to specify parameterized classes (templates in C++-speak or generics in Ada-speak). This is particularly important in patterns because patterns themselves are parameterized collaborations, and they are often defined in terms of parameterized classes.

A parameterized class is a class that is defined in terms of more primitive elements, which are referred to symbolically without the inclusion of the actual element that will be used. The symbolic name is called a formal parameter and the actual element, when bound, is called an actual parameter. In Figure 2-10, Queue is a parameterized class that is defined in terms of two symbolic elements a class called Element and an int called Size. Because the exact elements that these parameters refer to are not provided in the definition of Queue, Queue is not an instantiable class those undefined elements must be given definitions before an instance can be created. The «bind» dependency does exactly that, binding a list of actual elements to the formal parameter list. In the case of MsgQueue, Element is replaced by the class Message and the int Size is replaced by the literal constant 1000. Now that the actual parameters are specified and bound, MsgQueue is an instantiable class, meaning that we can create objects of this class at runtime.

Figure 2-10. Dependency

The diagram shows three common forms for the «bind» dependency. Form 1 is the most common, but the other forms are prevalent as well.

The usage relation indicates some element requires the presence of another for its correct operation. The UML provides a number of specific forms such as «call» (between two operations), «create» (between classifiers, e.g. classes), «instantiate» (between classifiers), and «send» (between an operation and a signal). Of these, «call» is common, as well as an unspecified «usage» between components, indicating that one component needs another because some of the services in one invoke some set of services in the other.

The permission relation grants permission for a model element to access elements in another. The «friend» stereotype is a common one between classes, modeling the friend keyword in C++. «access» is similar to Ada's use keyword, granting access of a namespace of one Ada package to another. The «import» relation adds the public elements of one namespace (such as a UML package) into another.

2.3.6 Structural Diagrams

UML is a graphical modeling language, although, perhaps surprisingly, the notation is less important than the semantics of the language. Nevertheless, there is a common set of graphical icons and idioms for creating these views of the underlying model. We call these views "diagrams." UML has been unjustly criticized for having too many diagram types class diagrams, package diagrams, object diagrams, component diagrams, and so on. The fact is that these are all really the same diagram type a structural diagram. Each of these diagrams emphasizes a different aspect of the model but they may each contain all of the elements in the others. A package diagram may contain classes and a class diagram may contain objects, while a component diagram might have objects, classes, and packages. In truth, the UML has one structural diagram, which we call by different names to indicate the primary purpose of the diagram.

We use diagrams for a number of purposes: as a data entry mechanism, as a means to understand the contents of the model, and as a means to discuss and review the model. The model itself is the totality of the concepts in your system and their relations to one another. When we use diagrams as a data entry mechanism, we add, modify, or remove elements to the underlying model as we draw and manipulate the diagrams.

The most common diagrams you'll draw are the class diagrams. These diagrams emphasize the organization of classes and their relations. The other aspects are used as needed, but class diagrams provide the primary structural view.

In real systems, you cannot draw the entire system in a single diagram, even if you use E-size plotter paper and a 4-point font. As a practical matter, you must divide up your system into different structural views (behavioral views will be described later). How, then, can we effectively do this? What criteria should we use to decide how many diagrams we need and what should go on them?

In the ROPES process, we use a simple criterion for decomposing the views of the system into multiple diagrams. The ROPES process introduces the concept of a mission of an artifact its "purpose for existence." For diagrams, the mission is straightforward each diagram should show a single important concept. This might be to show the elements in a collaboration of objects or classes realizing a use case, or a generalization taxonomy, or the contents of a package. Usually, every element of your model appears in some diagram somewhere, but it is perfectly reasonable for it to appear in several diagrams. For example, a class might be involved in the realization of three use cases (resulting in three different diagrams), be a part of a generalization taxonomy, and also be contained in a package of your model. In this case, you might expect it to appear in five different diagrams. It is also not necessary for all aspects of the class to be shown in all views. For example, in the class diagrams showing collaborations, only the operations and attributes directly involved in the mission of that collaboration would be shown; in a diagram showing generalization, only the features added or modified by that class would be shown; in a diagram showing the contents of the package that owns the class, you probably wouldn't show any attributes or operations.

Which of the views is right? The answer is all of them. Just because a feature of a class or some other element isn't shown doesn't mean or imply that the feature doesn't exist or is wrong. The semantics of the class or model element is the sum of the semantic statements made in all diagrams in which it appears. Indeed, you can define model elements without explicitly drawing them on diagrams at all but instead adding them to the model repository directly. One of the most valuable things that modeling tools provide, over simple drawing tools, is the maintenance of the semantic information about the structure and behavior of your system.

Normally, you don't draw object diagrams directly. Most often, classes and class relations are drawn and these imply the possible sets of objects and their relations. If for some reason you want to depict particular configurations of the runtime system the object diagrams are the appropriate venue. A case in which you do is the composite structure diagram. This is a diagram that shows a composite class and its parts (as object roles). This diagram is used as the primary view for showing the hierarchical structure of a system, as we will see in Section 2.4.2.

2.3.7 Mapping Objects to Code

Of course, the UML model must ultimately map to source code. In Java and C++, the mapping is straightforward. The source code for such the class diagram in Figure 2-4 (in Java) is the most straightforward because Java contains interfaces as a native concept. The Java source code would look like Code Listing 2-3.

Code Listing 2-3. Class Diagram in Java

public class SensorClient { protected myISensor iSensor; public void displayValue(void) { int sensedValue = iSensor.getValue(); System.out.println(value); }; }; // end class SensorClient interface iSensor { int acquire(void); int getValue(void); void setCalibrationConstant(long newCalibrationConstant); }; // end interface iSensor public class Sensor implements iSensor { protected iFilter myIFilter; int value; long calibrationConstant; public int acquire(void){ /* method here */ }; public int getValue(void) { return myIFilter.filter(value); }; public void setCalibrationConstant(long newCalibrationConstant) { calibrationConstant = newCalibrationConstant; }; }; // end class Sensor interface iFilter { public int filterValue(int value); }; // end interface iFilter public class Filter implements iFilter { int lowPass; int highPass; public int filtervalue(int value) { /* method here */ }; public setFilterParameters(int newLowPass, int newHighPass) { lowPass = newLowPass; highPass = newHighPass; }; }; // end class Filter

In C++, the code is almost as straightforward as the Java code, but not quite, because an interface is not a native concept in C++. There are two common approaches to implement interfaces in C++. The first, shown in Code Listing 2-4, is to create an abstract base class by declaring the interface operations as pure virtual. The other common approach is to use the Interface or Façade pattern. This involves creating the interface class as an instantiable class that associates to a separate implementation class.

Code Listing 2-4. Class Diagram in C++

class SensorClient { protected: iSensor* myISensor; public: void displayValue(void) { int sensedValue = iSensor.getValue(); cout << value << endl; }; }; class iSensor { // abstract class public : virtual int acquire(void)=0; // pure virtual virtual int getValue(void)=0; // pure virtual virtual void setCalibrationConstant(long newCalibrationConstant)=0; }; class Sensor : public iSensor { protected : iFilter* myIFilter; int value; long calibrationConstant; public : int acquire(void); int getValue(void){ return myIFilter->filterValue(value); }; void setCalibrationConstant(long newCalibrationConstant) { calibrationConstant = newCalibrationConstant; }; }; class iFilter { public : virtual int filterValue(int value)=0; // pure virtual }; class Filter : public iFilter { public : int filterValue(int value) { lowPass = newLowPass; highPass = newHighPass; }; };

In summary, an object is one of many possible instances of a class. A class has two notable features attributes (which store data values) and methods (which provide services to clients of the class). Interfaces are named collections of operations that are realized by classes. Interfaces need not be explicitly modeled. Many useful systems have been designed solely with classes, but there are times when the addition level of abstraction is useful, particularly when more than a single implementation of an interface will be provided.

Категории