6.7 Class Diagrams


Class diagrams are the single most important diagrams in object-oriented analysis and design. They show the structure of the system in terms of classes and objects, including how the objects and classes relate to each other. Class diagrams are the primary road map of the system and its object-oriented decomposition. They are similar to object diagrams except they show primarily classes rather than instances. Object diagrams are inherently snapshots of the system at some point in time, showing the objects and the links among them that exist at that instant in time. Class diagrams represent, in an important sense, all possible such snapshots and are therefore more "universal." For this reason, almost all structural modeling of systems is done with class diagrams rather than object diagrams.

So far in this book, we've already seen plenty of class diagrams. One of the disadvantages of building real systems (as opposed to constructing examples for books) is that in real systems you always come up with more classes than you can conveniently put on a single diagram even if you go to 2-point font and E-size plotter paper. Developers need criteria for dividing up their class structures into different diagrams that make the system understandable, accessible, and modifiable in the various ways that the stakeholders need. Different methodologies have different solutions for this. The ROPES process has a simple process rule: Every diagram should have a single important concept it is trying to show. This is called the mission of the diagram. Common missions for a class diagram include these:

  • Show the context of an architectural class or object (a context diagram)

  • Show the architectural elements within a system and how they relate (a subsystem diagram or component diagram)

  • Show the parts within a structured class (a class structure diagram)

  • Show the architectural elements that work together to provide redundancy, replication, or fault tolerance (a structure reliability or structure safety diagram)

  • Show how the architectural or collaborating elements distribute across multiple address spaces (a deployment diagram)

  • Show how instances distribute themselves across multiple address spaces with respect to their communication (a distribution diagram)

  • Show the classes within a collaboration realizing a use case at an object-domain level of abstraction (a essential object diagram or essential class diagram)

  • Show the classes within a collaboration, including the design refactoring and inclusion of design elements (a class design diagram)

  • Show the information flow among types or instances (a data flow or information flow diagram

  • Show the classes within a package or domain

  • Show the package organization of the model (a package diagram)

  • Show the classes within a single generalization taxonomy (an inheritance diagram)

  • Show the classes related to the concurrency or resource model (a task diagram)

  • Show the classes related to access of a single resource of particular significance (a resource diagram)

  • Show a view of structural elements bound together by a coupled set of constraints (a constraint diagram)

  • Show a set of instances and their links for a typical or exception condition or state of the system (an object diagram)

  • Show the abstraction of a set of design elements into a design pattern (a design pattern diagram)

With a little thought, I'm sure you can come up with even more specific missions for class diagrams. Note that many of these missions are common enough to be given specific names, such as task diagram, but this doesn't mean that the diagram type is different, only its usage. The mission of a diagram focuses on why we want to construct a particular diagram and how it helps us, but the same set of elements (class, object, package, component, node) may all appear on the same diagram type. So these aren't fundamentally different diagrams. Systems are organized this way to help us understand all their relevant aspects. Organizing the diagrams by missions makes sense aids comprehension and defect-identification. A common complaint about the UML is that there are too many different diagrams, but really there are only a handful. There are, however, just a few diagram types, but many missions or purposes to which these few diagrams may be applied.

6.7.1 Associative Classes

In the UML 1.x, an associative class clarified associations that had interesting structure or behavior. In the UML 2.0, all associations are classifiers and so may be shown as a stereotyped class where useful. In distributed systems, the classic example of an associative class is a bus message class. In addition to the information being sent from one object to another, the message object may contain additional information specific to the relationship and even the link instance. For example:

  • Message priority

  • Message route

  • Session identifiers

  • Sequence numbers

  • Flow control information

  • Data format information

  • Data packaging information

  • Time-to-live information for the message

  • Protocol revision number

  • Data integrity check information

In a transaction-oriented system, associative classes also contain information specific to the transaction.

An associative class is used when information does not seem to belong to either object in the association or belongs to both equally. Marriage is an association between two people objects. Where do the following attributes belong?

  • Date of marriage

  • Location of marriage

  • Prenuptial agreement

Clearly, these are attributes of the marriage and not the participants.

Figure 6-13 shows a case of an associative class. In this distributed system model, one subsystem contains the sensor class and acts as a server for the data from the sensors. The client subsystem displays this data to the user. The association between the measurement server and measurement client is of interest here.

Figure 6-13. Session Associative Class

graphics/06fig13.gif

One way to implement the association is to have the client explicitly ask whenever it wants data to display. Another is to have the server provide the data whenever it becomes available. However, neither option may be the best use of a finite-bandwidth bus. Additionally, what if another client wants to be added to the recipient list for the data? Some buses provide the ability to broadcast such data, but most target a specific recipient.

The associative class solution provided here uses a session. A session is a negotiated agreement between two communicating objects. Sessions save bandwidth because they negotiate some information up front so that it need not be passed within each message. Nonsessioned (connectionless) communications are similar to postcards. Each and every time a postcard is mailed, it must contain the complete destination and source addresses. A session is more like a phone call. Once the destination is dialed and the connection established, the communication can be any length or complexity without resending or reconnecting.

In this case, the session contains two important pieces of information: the source and the target addresses. Two session subtypes are defined based on the update policy. The update policy can be either episodic or periodic. Episodic, in this context, means that the server sends data when it changes that is, when an episode occurs. Periodic means that the server must send the data at a fixed interval. Episodic sessions in this example have a maximum update rate to make sure that a bursty system does not overload the bus. Periodic sessions have a defined update rate.

The measurement server contains two operations to assist in session management. The first is Register Client. This operation accepts the registration of a new client and participates in the negotiated construction of a session object. The Deregister Client operation removes a registered client.

The measurement client contains two operations to support its role in negotiating the session: Accept Server and Break Connection.

Who owns the session? Arguments can be made for both the server and the client. The server must track information about the session so that it knows when and how to issue updates. The client initiates the session and must also know about update policy and rates. The solution shown in Figure 6-13 is that the session is an associative class and contains attributes about the relationship between the two primary classes. Because this is a distributed system, the implementation would involve creating two coordinating session objects (or a single distributed object), one on each processor node, providing session information to the local client or server.

6.7.2 Generalization Relationships

Generalization is a taxonomic relationship between classes. The class higher in the taxonomic hierarchy is sometimes called the parent, generalized, base, or superclass. The class inheriting properties from the base class is called the child, specialized, derived, or subclass. Derived classes have all the properties of their parents, but may extend and specialize them.

Using Aristotelian logic and standard set theory, generalizing along a single characteristic guarantees that the classes are disjoint.[10] When the set of subclasses enumerates all possible subclasses along the characteristic, the subclassing is said to be complete.

[10] By disjoint we mean that the classes represent nonoverlapping or orthogonal alternatives. Male and female, for example, are disjoint sets. Fuzzy sets are inherently nondisjoint and allow partial membership. Object subclasses are assumed to be crisp sets (all-or-none membership) rather than fuzzy.

Consider our elevator example. An elevator has many buttons ones to go to a floor, one to open the door, one to close the door, one to send an alarm, and so on. An elevator might have 30 different button instances in it, but how many classes do we have? Are the buttons different in terms of their structure or behavior? No they all work the same way you push them and they report a press event; when you release them, they report a release event; when selected, they will be backlit, and so on. They only differ in their usage not in their type, and should be modeled as different instances of a single class. In other environments, there may be buttons that differ in their structure and behavior.

The button subclasses in Figure 6-14 are specialized along the lines of behavior. Simple buttons issue an event message when pressed, but have no state memory. Toggle buttons jump back and forth between two states on sequential depressions. Multistate buttons run through a (possibly elaborate) state machine with each depression. Group buttons deselect all other buttons within the group when depressed.

Figure 6-14. Button Subclasses

graphics/06fig14.gif

In the UML, generalization implies two things: inheritance and substitutability. Inheritance means that (almost) everything that is true of a superclass is also true of all of its subclasses. This means that a subclass has all of its parent's attributes, operations, associations, and dependencies. If the superclass has a statechart to define its behavior, then the subclass will inherit that statechart. The subclass is free to both specialize and extend the inherited properties. Specialization means that the subclass may polymorphically redefine an operation (or statechart) to be more semantically appropriate for the subclass. Extension means that the subclass may add new attributes, operations, associations, and so on.

Substitutability means that an instance of a subclass may always be substituted for an instance of its superclass without breaking the semantics of the model. This is known as the Liskov Substitution Principle (LSP) [3]. The LSP states that a subclass must obey polymorphic rules in exactly the same manner as its superclass. For example,

 class Animal { public:     virtual void speak(void) = 0;   // virtual base        class }; class dog: public Animal { public:     void speak(void) { cout << "Arf!" << endl; }; }; class fish: public Animal { public:     void speak(void) { cout << "Blub! " << endl; }; }; class cat: public Animal { public:     void speak(void) {         cout << "<Aloof disdain>" << endl; }; }; void main(void) {     Animal *A;     dog d;     fish f;     cat c;     A = &d;     A->speak();     A = &c;     A->speak();     A = &f;     A->speak(); }; 

The base class (Animal) accesses the three subtypes through a pointer (A). Nonetheless, the access to the speak() method for each is identical, satisfying the LSP.

For the LSP to work, the relationship between the superclass and subclass must be one of specialization or extension. Whatever is true of the superclass must also be true of the subclass because the subclass is a type of its superclass. A dog is an animal, so all of the things true about all animals are also true about dogs. All of the behaviors common to all animals can also be performed by dogs. If the abstraction animal had an attribute or behavior that was not true of dogs (such as being able to produce free oxygen via photosynthesis), a dog would not be a type of animal.

That being said, a subclass is free to specialize the behavior of a superclass. The speak() method defined within animal is an example. Each subclass does a different thing while still meeting the requirement of supporting the behavior speak(). Locomote() is another example. Dogs, cats, fish, and birds all locomote, but they implement it differently. This is what is meant by specialization.

Subclasses are also free to extend their inherited structure by adding new behaviors or attributes. We can add some new behaviors to the dog and cat classes that are not true of animals in general.

 class dog: public Animal { public:     void speak(void) { cout << "Arf!" << endl; };     void slobber(float slobberIndex);     void AttackJogger(int fearLevel); }; class cat: public Animal { public:     void speak(void) {         cout << "<Aloof disdain>" << endl; };     void ClawFurniture(long ClawLength);     void SnubOwner(void); }; 

Now dogs can slobber() and AttackJogger() while cats can ClawFurniture() and SnubOwner(). Animals in general (and fish for that matter) can perform none of these charming behaviors.

Frequently, base classes cannot be instantiated without being first specialized. Animal might not do anything interesting, but dogs, cats, and fish do. When a class is not directly instantiated, it is called an abstract class. C++ classes are made abstract by the inclusion of a pure virtual function. A C++ virtual function is a class method that is denoted by the keyword virtual. It need only be so indicated in one class in the inheritance tree, and it will be virtual for all derived classes. A virtual function is made pure virtual with the peculiar syntax of assigning the function declaration the value zero. For example,

 class widget { public:     virtual void doSomething() = 0; }; 

Note that in C++, constructors and destructors cannot be made pure virtual. Furthermore, if a C++ class contains a virtual function, it is a logical error not to define the destructor as virtual.[11] C++ does not allow virtual constructors.

[11] Just one of many such opportunities provided by the C++ language.

6.7.2.1 Positioning Features in the Inheritance Tree

Generalization relationships form class hierarchies with the most general classes at the top and the most specialized classes at the bottom. Structuring these hierarchies is done by using three complementary approaches:

  • Extension: Derived classes extend the capabilities of the parent class (top-down).

  • Specialization: Derived classes specialize the capabilities of a parent (top-down).

  • Bubbling up: Attributes and behaviors that are common in peer children become attributes and behaviors of the parent class (bottom up).[12]

    [12] Note that restriction of the parent class is not included. All popular object-oriented languages allow the augmentation or extension of a class, but not restriction (removal of a behavior or attribute). To do so breaks the fundamental tenant of inheritance that the child is a type of its parent and therefore violates the LSP.

The first strategy, extension, means that a subclass can add behaviors and attributes to those it inherits from its parents. This is an example of the Open-Closed Principle (OCP) [4]. The OCP states that for maximum reusability, a class should be open for extension but not modification. The focus of the OCP is that changes in a well-designed class hierarchy should be made by subclassing rather than modifying the hierarchy itself. Another term for this concept is programming by difference. The developer finds a class in the hierarchy that is close to what is needed, subclasses it, and extends the subclass to meet the particular needs. The OCP focuses on reuse, but it applies equally well to the construction of class hierarchies. Subclasses may extend the capabilities of their parents without modifying the parents.

LSP applies not only to features such as attributes, operations, and states. It also applies to constraints. Following the generalization principle of "any subclass must be at least able to do what the parent class can," constraints should be loosened in subclasses not tightened. This makes sense if you look at a feature or quality of service of a superclass and ask the question, "Can the subclass do this?" Put another way, a subclass inherits all of the features (and constraints) of the superclass, although it may be some additional ones. The rule is substitutability. That is, a constraint in a subclass must be consistent with constraints in the superclass. For example,

  • If an offered interface has a timing requirements, such as "worst-case execution time = 18 ms +/ 2 ms," then any subclass must meet this, but it may do better, such as "worst case execution time = 16 ms +/ 1.5ms"; but it cannot fail to meet those constraints without violating substitutability. For example, a subclass of the previously mentioned class can't use the constraint "25 ms +/ 1ms" or even "17ms +/ 5ms."

  • Required interfaces have similar restrictions. If a class B meets the required interface of the superclass, then it should also meet the required interface for any subclass.

  • Subranges for values being sent to the class realizing the interface may be extended in the subclass, but not further constrained; this is because every value sent to the superclass must be valid while being sent to the subclass.

  • Subranges for values being returned from the class realizing the interface may be constrained but not extended; this is because every value returned by the subclass in this case may also be returned by the superclass and so maintains substitutability. The fact that it doesn't return all possible values isn't a problem. However, if the subclass returned a value beyond what the superclass could do, this could break a client.

Figure 6-15 shows how constraints are properly propagated to subclasses. It is, perhaps, counterintuitive that an InfraredDopplerLight is the superclass and DopplerLight is the subclass, but the InfraredDopplerLight allows sensing wavelengths to be set only in the infrared range, while the DopplerLight can use a wider range of light wavelengths. In this example, this arrangement of generalization allows substitutability to preserved:

  • The iInfraDopplerLight interface provides an operation acquire() with a specified worst-case execution time of 20ms. The subclass interface iDoppler provides the same operation with a specified worst-case execution of 15ms. The subclass can be substituted for the superclass and meet the interface requirements

  • The iInfraDopplerLight interface has an operation get(), which returns a data sample in the range of 0..1500. The subclassed interface iDoppler::get() returns a data sample in the range of 0..500. This is likewise consistent since every value returnable from the subclass is valid for the superclass (and therefore for its clients) so any client usage of the iDopper in the role of an iInfraDoppler is correct.

  • The iInfraSensor interface has a setWavelength() operation that accepts parameters only in the infrared range. The subclass interface iSensor::setWavelength() allows all the parameters that can be passed to it from an iInfraSensor client as well as more, since it can also take wavelengths in the visible and ultraviolet range.

Figure 6-15. Generalization and Constraints

graphics/06fig15.gif

As an exercise, decide how to arrange the following in a generalization taxonomy: shape, square, rectangle. This simple problem has caused more debate than you might think!

Generalization is useful because of the substitutability of the subclass whenever a superclass is used. Consider Figure 6-16. A client needs to use the Queue superclass to store strings (OMString is a type provided by the Rhapsody tool and so is used here).[13] Sometimes, a client might need to store more elements that there is main memory available for storage, so a CachedQueue (a subclass of Queue) can be substituted, to cache elements out to disk as necessary.

[13] Note that in a properly defined container class we would probably use parameterized classes to isolate the container functionality from the type being contained, but that would only muddy the waters of this particular example so we use a specific type with a specific size for simplicity.

Figure 6-16. Extending and Specializing

graphics/06fig16.gif

Figure 6-16 shows a simple example of extending a subclass. The Queue superclass provides several methods insert(), remove(), nElements(), isFull(), and isEmpty() as well as some attributes head, tail, and size (all of type int). It also has a composition relation to OMString (the type of elements that it stores). The source code for this class is shown in Code Listing 6-1 (Queue.h, the c++ header file) and Code Listing 6-2 (Queue.cpp, the implementation file). This code is generated from the I-Logix Rhapsody tool, so it contains some macros (such as GUARD_OPERATION, which allocates a mutex to protect the Queue class from mutual exclusion problems) and other aspects that are autogenerated and may be pretty much be ignored. You can see by looking at the model in Figure 6-16 and the source code that the Queue class is fairly straightforward.

Code Listing 6-1. Queue.h
 /*****************************************************       Rhapsody    : 5.0       Login       : Bruce       Component   : DefaultComponent       Configuration     : TestCaching       Model Element     : Queue //!   Generated Date    : Wed, 18, Jun 2003       File Path   : DefaultComponent\TestCaching\Queue.h *****************************************************/ #ifndef Queue_H #define Queue_H //#[ ignore #define _OMFLAT_IMPLEMENTATION 1 //#] #include <oxf/oxf.h> #include "fstream.h" #include "stdio.h" #include "Default.h" #include <oxf/omreactive.h> #include <oxf/state.h> #include <oxf/event.h> //## package Default //---------------------------------------------------- // Queue.h //---------------------------------------------------- class Overflow; class Underflow; //## class Queue class Queue : public OMReactive { ////    Constructors and destructors    //// public :     //## operation Queue()     Queue(OMThread*  p_thread = OMDefaultThread);     //## auto_generated     virtual ~Queue(); ////    Operations    //// public :     //## operation clear()     void clear();     //## operation get(int)     OMString get(int  index);     //## operation insert(OMString*)     virtual void insert(OMString*  s);     //## operation isEmpty()     virtual OMBoolean isEmpty();     //## operation isFull()     virtual OMBoolean isFull();     //## operation nElements()     virtual int nElements();     //## operation remove()     virtual OMString* remove(); ////    Additional operations    //// public :     //## auto_generated     int getHead() const;     //## auto_generated     void setHead(int  p_head);     //## auto_generated     int getSize() const;     //## auto_generated     void setSize(int  p_size);     //## auto_generated     int getTail() const;     //## auto_generated     void setTail(int  p_tail);     //## auto_generated     Overflow* getItsOverflow() const;     //## auto_generated     void setItsOverflow(Overflow*  p_Overflow);     //## auto_generated     Underflow* getItsUnderflow() const;     //## auto_generated     void setItsUnderflow(Underflow*  p_Underflow); ////    Framework operations    //// public :     //## auto_generated     int getElement() const;     //## auto_generated     virtual OMBoolean startBehavior(); protected :     //## auto_generated     void cleanUpRelations(); ////    Attributes    //// protected :     int head;           //## attribute head     int size;           //## attribute size     int tail;           //## attribute tail ////    Relations and components    //// protected :     OMString* element[100];           //## link element     Overflow* itsOverflow;            //## link itsOverflow     Underflow* itsUnderflow;          //## link itsUnderflow }; #endif /****************************************************       File Path   : DefaultComponent\TestCaching\Queue.h *****************************************************/ 
Code Listing 6-2. Queue.cpp
 /*****************************************************       Rhapsody    : 5.0       Login       : Bruce       Component   : DefaultComponent       Configuration     : TestCaching       Model Element     : Queue //!   Generated Date    : Wed, 18, Jun 2003       File Path   : DefaultComponent\TestCaching\Queue.cpp *****************************************************/ #include <oxf/omthread.h> #include "Queue.h" #include "Overflow.h" #include "Underflow.h" //## package Default //---------------------------------------------------- // Queue.cpp //---------------------------------------------------- //## class Queue Queue::Queue(OMThread*  p_thread) {     setThread(p_thread, FALSE);     {         for(int pos=0;pos<100;pos++)element[pos]=NULL;     }     itsOverflow = NULL;     itsUnderflow = NULL;     //#[ operation Queue()     size=100;     head=tail=0;     //#] } Queue::~Queue () {     cleanUpRelations(); } void Queue::clear() {     //#[ operation clear()     head=tail=0;     //#] } OMString Queue::get(int  index) {     //#[ operation get(int)     return *element[index];     //#] } void Queue::insert(OMString*  s) {     //#[ operation insert(OMString*)       if (isFull())         throw Overflow();       else {         element[head] = s;         head = (head+1) % size;         };     //#] } OMBoolean Queue::isEmpty() {     //#[ operation isEmpty()      if ( head == tail) return TRUE;       else return FALSE;     //#] } OMBoolean Queue::isFull() {     //#[ operation isFull()       return ( (head+1) % size == tail);     //#] } int Queue::nElements() {     //#[ operation nElements()       if (head >= tail) return head-tail;       else return size-(tail-head)+1;     //#] } OMString* Queue::remove() {     //#[ operation remove()      OMString* s;       if (isEmpty())         throw Underflow();       else {         // cout << "element[" << tail << "]=";         s = element[tail];         // cout << *s << "??" << s->GetLength()         // << endl;         tail = (tail+1) % size;         return s;       }     //#] } int Queue::getHead() const {     return head; } void Queue::setHead(int  p_head) {     head = p_head; } int Queue::getSize() const {     return size; } void Queue::setSize(int  p_size) {     size = p_size; } int Queue::getTail() const {     return tail; } void Queue::setTail(int  p_tail) {     tail = p_tail; } int Queue::getElement() const {     int iter=0;     return iter; } Overflow* Queue::getItsOverflow() const {     return itsOverflow; } void Queue::setItsOverflow(Overflow*  p_Overflow) {     itsOverflow = p_Overflow; } Underflow* Queue::getItsUnderflow() const {     return itsUnderflow; } void Queue::setItsUnderflow(Underflow*  p_Underflow) {     itsUnderflow = p_Underflow; } void Queue::cleanUpRelations() {     if(itsOverflow != NULL)         {             itsOverflow = NULL;         }     if(itsUnderflow != NULL)         {             itsUnderflow = NULL;         } } OMBoolean Queue::startBehavior() {     OMBoolean done = FALSE;     done = OMReactive::startBehavior();     return done; } /*****************************************************       File Path   : DefaultComponent\TestCaching\Queue.cpp *****************************************************/ 

The Queue class is subclassed by the CachedQueue class, which inherits these features. The CachedQueue class adds two some new methods flushBlock(), loadBlock(), and removeLocal() and some new attributes as well. The child class extends the functionality of the parent by providing new behaviors.

Note that the CachedQueue subclass internally contains two normal queues one for the input cache and one for the output cache. The input cache is strongly aggregated by the CachedQueue already because it is a kind of Queue, and the Queue class has a strong aggregation to an array of OMStrings. The other is done by aggregating a separate Queue subclass with the role name outCache for storing things as they come off the disk. Thus, data is inserted by a client into the input cache (directly aggregated by CachedQueue), or data may be written out to disk when that cache gets full, or the oldest data may reside in the indirectly aggregated output cache via the composition relation to the Queue class (role name outCache).

Specializing the CachedQueue as a subclass of Queue redefines some of the class's (virtual) behaviors, particularly, insert() and remove(). The code for the insert() and remove() operations of the Queue class is relatively simple, but more complex than Queue::insert() and Queue:: remove, as shown in Code Listing 6-3 and 6-4.

In the simple Queue class, the insert() operation checks to see whether there is room and, if so, sticks it in, doing the necessary math on the head pointer. In the CachedQueue, if there isn't room, then the input cache must be flushed out to disk, the input cache cleared, and the data inserted.

Similarly, the Queue::remove() operation is very simple if there is any data, return it, otherwise throw an exception. The CachedQueue:: remove() operation is more complex: If the data is in the output cache, then get it from there; if the output cache is empty and there is data on disk, then load the next block from the disk and then get it from there. If there is no data in the output cache or on disk but there is data in the input cache, then get it from there. Lastly, if there is no data anywhere, throw an underflow exception.

Code Listing 6-3. CachedQueue.h
 /*****************************************************       Rhapsody    : 5.0       Login       : Bruce       Component   : DefaultComponent       Configuration     : TestCaching       Model Element     : CachedQueue //!   Generated Date    : Wed, 18, Jun 2003       File Path   : DefaultComponent\TestCaching\CachedQueue.h *****************************************************/ #ifndef CachedQueue_H #define CachedQueue_H //#[ ignore #define _OMFLAT_IMPLEMENTATION 1 //#] #include <oxf/oxf.h> #include "fstream.h" #include "stdio.h" #include "Default.h" #include <oxf/omprotected.h> #include "Queue.h" //## package Default //---------------------------------------------------- // CachedQueue.h //---------------------------------------------------- class Overflow; class Underflow; //## class CachedQueue class CachedQueue : public Queue {     OMDECLARE_GUARDED ////    Constructors and destructors    //// public :     //## operation CachedQueue()     CachedQueue(OMThread*  p_thread =       OMDefaultThread);     //## auto_generated     ~CachedQueue(); ////    Operations    //// public :     //## operation insert(OMString*)     void insert(OMString*  s);     //## operation remove()     OMString* remove();     //## operation removeLocal()     OMString* removeLocal(); protected :     // write out the input cache to disk. Writing will     // append at the end of the file     //## operation flushBlock()     void flushBlock();     // loadBlock reads the next block off disk -     // loading occurs at the FRONT of the file.     //## operation loadBlock()     void loadBlock(); ////    Additional operations    //// public :     //## auto_generated     OMString getFilename() const;     //## auto_generated     void setFilename(OMString  p_filename);     //## auto_generated     int getNLinesOnDisk() const;     //## auto_generated     void setNLinesOnDisk(int  p_nLinesOnDisk);     //## auto_generated     long getReadPos() const;     //## auto_generated     void setReadPos(long  p_readPos);     //## auto_generated     Queue* getOutCache() const;     //## auto_generated     Queue* newOutCache();     //## auto_generated     void deleteOutCache(); ////    Framework operations    //// public :     //## auto_generated     virtual OMBoolean startBehavior(); protected :     //## auto_generated     void initRelations();     //## auto_generated     void cleanUpRelations(); ////    Attributes    //// protected :     OMString filename;        //## attribute filename     int nLinesOnDisk;         //## attribute nLinesOnDisk     long readPos;       //## attribute readPos ////    Relations and components    //// protected :     Queue* outCache;          //## link outCache }; #endif /*****************************************************       File Path   : DefaultComponent\TestCaching\CachedQueue.h *****************************************************/ 
Code Listing 6-4. CachedQueue.cpp
 /*****************************************************       Rhapsody    : 5.0       Login       : Bruce       Component   : DefaultComponent       Configuration     : TestCaching       Model Element     : CachedQueue //!   Generated Date    : Wed, 18, Jun 2003       File Path   : DefaultComponent\TestCaching\CachedQueue.cpp *****************************************************/ #include <oxf/omthread.h> #include "CachedQueue.h" #include "Overflow.h" #include "Underflow.h" //## package Default //---------------------------------------------------- // CachedQueue.cpp //---------------------------------------------------- //## class CachedQueue CachedQueue::CachedQueue(OMThread*  p_thread) {     setThread(p_thread, FALSE);     initRelations();     //#[ operation CachedQueue()     // constructor must clear any debris in the     // cache file.     filename = "C:\\cachefile.txt";     nLinesOnDisk = 0;     readPos = 0; // start of file     // ios::trunc clears out the file when the     // CachedQueue is constructed     ofstream cacheFile(filename, ios::trunc);     //#] } CachedQueue::~CachedQueue() {     cleanUpRelations(); } void CachedQueue::flushBlock() {     GUARD_OPERATION     //#[ operation flushBlock()     // open file for writing, appending at the end     // write out the strings in the buffer     // then clear the local buffer     OMString* s;     ofstream cacheFile (filename, ios::in|ios::app);     cout << " -> OPENED " << filename << " for       Writing" << endl;     while (!isEmpty()) {       s = removeLocal();       // get the data from the input side       cacheFile << *s << endl;       // write it to disk       // cout << "W<- " << *s << endl;       // show it ++nLinesOnDisk;       };     cacheFile.close();     cout << "Write file closed" << endl;     clear();     //#] } void CachedQueue::insert(OMString*  s) {     GUARD_OPERATION     //#[ operation insert(OMString*)     // check if there is room in the local store.     // if not, then write the buffer to disk (appending)     // clear it, then insert the new string     // using the superclass's insert();     if (isFull())       flushBlock();     Queue::insert(s);     //#] } void CachedQueue::loadBlock() {     GUARD_OPERATION     //#[ operation loadBlock()     // preconditions, there is at least one block     // in the cacheFile     // (checked by examining nBlocksOnDisk     // prior to calling this     #define MAXLENGTH 100     char buf[MAXLENGTH]; // buffer for input     OMString* s;     int j=1;     ifstream cacheFile(filename);     cout << " -> OPENED " << filename << " for reading" << endl;     cacheFile.seekg(readPos);     while (!cacheFile.eof() && !outCache->isFull() && nLinesOnDisk>0) {       cacheFile.getline(buf, MAXLENGTH);       s = new OMString((char*) buf);       outCache->insert(s);       // cout << "Loading string < " << *s << endl;        nLinesOnDisk;       }; // end while     readPos = cacheFile.tellg();     cacheFile.close();     cout << "Read file closed" << endl;     /*     // check on the outcache     int n = outCache->nElements();     int t = outCache->getTail();     // cout << "LoadBlock done. OutCache now has "     // << n << " elements" << endl;     for (j=0; j < n; j++) {       cout << "[" << t << "]" << outCache->get(t)         << endl;       t = (t+1) % outCache->getSize();     };     cout << "done showing the elements" << endl;     */     //#] } OMString* CachedQueue::remove() {     GUARD_OPERATION     //#[ operation remove()     // if there is stuff in the output cache, return          that     // else if there's stuff on disk, pull it off and          return the first     // else if there's stuff in the input buffer,          then return that     // else through an exception OMString* s;     if (outCache->nElements()>0) {         //cout << "CachedQueue.remove path 1 "             << endl;       s = outCache->remove();       }     else if (nLinesOnDisk>0) {         //cout << "CachedQueue.remove path 2 "             << endl;       loadBlock();       s = outCache->remove();       }     else if (nElements()>0) {         //cout << "CachedQueue.remove path 3 " << endl;       s = removeLocal();       }     else {       //cout << "CachedQueue.remove path 4 " << endl;       throw Underflow();       }     // cout << "CachedQueue::remove got string: "          << *s << endl;     return s;     //#] } OMString* CachedQueue::removeLocal() {     GUARD_OPERATION     //#[ operation removeLocal()     return Queue::remove();     //#] } OMString CachedQueue::getFilename() const {     return filename; } void CachedQueue::setFilename(OMString  p_filename) {     filename = p_filename; } int CachedQueue::getNLinesOnDisk() const {     return nLinesOnDisk; } void CachedQueue::setNLinesOnDisk(int  p_nLinesOnDisk) {     nLinesOnDisk = p_nLinesOnDisk; } long CachedQueue::getReadPos() const {     return readPos; } void CachedQueue::setReadPos(long  p_readPos) {     readPos = p_readPos; } Queue* CachedQueue::getOutCache() const {     return outCache; } Queue* CachedQueue::newOutCache() {     outCache = new Queue(getThread());     return outCache; } void CachedQueue::deleteOutCache() {     delete outCache; } void CachedQueue::initRelations() {     outCache = newOutCache(); } void CachedQueue::cleanUpRelations() {     {         deleteOutCache();         outCache = NULL;     } } OMBoolean CachedQueue::startBehavior() {     OMBoolean done = FALSE;     done = Queue::startBehavior();     outCache->startBehavior();     return done; } /*****************************************************       File Path   : DefaultComponent\TestCaching\CachedQueue.cpp *****************************************************/ 

The last strategy for constructing generalization hierarchies works from the leaves of the inheritance tree. After structuring the hierarchy, siblings subclassed from the same parent are examined for attributes and behaviors in common. If all siblings have the same property, it belongs to the parent rather than being replicated in each sibling. If the characteristic is common to some, but not all, of the siblings, then it may indicate a class is needed between the parent and the similar siblings.

Figure 6-17 shows an inheritance tree for bus messages. Each child class inherits the attributes Priority, Source Address, and Destination Address. We see, however, that each message has a Contents attribute. Event Report and Sequenced Event Report messages must identify the event in their Contents field. ACKs and NAKs must identify the message to which they are responding in their Contents field. Since all siblings have the Contents attribute, it can be moved up into the parent class, Bus Message.

Figure 6-17. Positioning Attributes in the Generalization Hierarchy

graphics/06fig17.gif

Event Report and Sequenced Event Report messages share ACK Timeout and Number of Retries attributes. These do not appear in the ACK and NAK messages. Therefore, these may be abstracted into a class between Event Report and Bus Message and Sequenced Event Report and Bus Message. This is called Reliable Message because protocol mechanisms exist to support retransmission if no response to them is received. Figure 6-18 shows the resulting reorganization.

Figure 6-18. Repositioned Attributes

graphics/06fig18.gif

Of course, the astute analyst may mix all three strategies. It is possible to extend, specialize, and bubble up all at the same time.



Real Time UML. Advances in The UML for Real-Time Systems
Real Time UML: Advances in the UML for Real-Time Systems (3rd Edition)
ISBN: 0321160762
EAN: 2147483647
Year: 2003
Pages: 127

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