1. Object-Oriented Modeling
Total Page:16
File Type:pdf, Size:1020Kb
1. Object-Oriented Modeling
Overview
A picture is worth a thousand words, or in our case, a thousand lines of code. Using UML class diagrams developers can get a quick overview of the structure of all or part of a program. Sometimes, even the quality of a design can be discerned from a class diagram.
To provide a context for modeling and design, the chapter begins with a brief overview of the iterative-incremental development process. A parallel introduction to UML class and object diagrams follows. The translation between these diagrams and C++ programs is a point of special emphasis. The chapter concludes with a discussion of a few design principles.
A full treatment of software engineering, UML, or design principles is beyond the scope of a single chapter and is tangential to our purposes. The interested reader should consult the references mentioned for details on these subjects.
Object-Oriented Development
A software project usually involves three participants: the client commissions the software, the developer builds the software, and the user uses the software. These roles may be played by individuals or organizations. In fact, all three roles may be played by the same Pattern Oriented Programming with C++/Pearce actor, but it will still be important for the actor to remember which role he is playing at any given moment. Just as a play is divided into carefully scripted acts and scenes, a software project follows a carefully scripted development process, which is usually divided into five phases:
Analysis: All three participants create a specification document that describes the application's requirements as well as the application domain's important terms, concepts, and relationships. Design: Using the specification document as a guide, the developer creates an architectural document that describes the application's important classes, together with their responsibilities and collaborations. Implementation: Using the architectural document as a guide, the developer implements the classes it describes. The developer may need to introduce supporting classes. Testing: Of course the developer tests the structure and function of each class he implements (unit testing), but the application must also be tested as a whole, first by the developer (integration testing), then by the users (acceptance testing). Integration testing is the riskiest phase. Maintenance: Maintenance involves fixing bugs (corrective maintenance), porting the application to new platforms (adaptive maintenance), and adding new features (perfective maintenance). Maintenance is the longest and costliest phase. Iterative-incremental development processes mitigate the risk of integration testing by allowing the developer to iterate through these phases many times. Each iteration produces an increment of the specification document, architectural document, and a tested implementation. Typically, high priority and high risk requirements are attacked during the early iterations. Thus, a decision to abort a project can be made early, before too much money has been spent:
1-2 1. Object-Oriented Modeling
Analysis
specification += increment
Design
architecture += increment
Implementation
implementation += increment
Testing iterate? abort? release?
Maintenance
retire?
In theory, a developer may be working on the specification near the end of the project and the implementation near the beginning. In practice, most of the specification is completed during the early iterations through the specification phase. Only finishing touches might be added late in the project. Administrative issues such as resolving file dependencies and implementing constructors and destructors are typical activities during the early iterations through the implementation phase, while implementing low-level supporting functions is more common during the late iterations. The following graph gives a rough idea of the maturity rates of the specification, architecture, implementation, and test plan as time passes:
increments
final
= specification = architecture = implementation
= testing
iterations
1-3 Pattern Oriented Programming with C++/Pearce
UML
The Universal Modeling Language (UML) is a family of diagram types that appear prominently in specification and architectural documents. UML was developed by Rational Software corporation [WWW-6] and was subsequently chosen by OMG, the Object Management Group [WWW-14], as the "industry standard" object-oriented modeling language. As such, UML replaces or incorporates several competing languages that preceded it.
Although UML describes many types of diagrams, we will only introduce and use a restricted subsets of class, package, object, and interaction (sequence) diagrams. For a more thorough treatment, the interested reader should consult [FOW-1] or any of the dozens of other books on UML that are currently available.
Objects and Classes
A class diagram shows an application's important classes and their relationships. Classes appear in these diagrams as class icons. A class icon is a box labeled with the name of the class it represents. Additional compartments show important attributes and operations:
Name Attributes Operations
One or both of these additional compartments may be suppressed when the extra information is unavailable, premature, or unnecessary.
For example, a flight simulation program will probably want to represent airplanes as instances of an airplane class. Suppose we learn that from the flight simulator's point of view, the important attributes of an airplane are its altitude and air speed, and the important operations are takeoff(), fly(), and land(). Here's the corresponding class icon:
1-4 1. Object-Oriented Modeling
Airplane altitude speed takeoff() fly() land()
Notice that we have suppressed information about the types, visibility, and initial values of the attributes, as well as the parameters, visibility, and return values of the operations. This is often done when such information is unavailable, unnecessary, or premature.
Of course UML allows us to add this information. For example, it probably makes sense for air speed and altitude to be doubles initialized to zero. (Airplanes are created on the ground and standing still.) Suppose we decide that Airplane may eventually serve as a base class for classes representing special types of airplanes such as military planes and passenger planes. In this case we may want to make altitude and air speed protected instead of private.
Assume we also learn that the takeoff(), fly(), and land() operations are indeed parameterless and have void return values. Of course these operations should be public; however, we find out that they all need to call a supporting function called flaps(), which raises and lowers the wing flaps by a specified integer angle with a default argument of 30 degrees. Because flaps() is only a supporting function, we decide to make it private.
Finally, suppose our unit testing regimen demands that every class provide a public, static operation called test() that creates a few objects, calls a few member functions, then returns true if no errors occurred and false otherwise. Here is the class icon showing all of this added information:
Airplane #altitude: double = 0.0 #speed: double = 0.0 +takeoff(): void +fly(): void +land(): void -flaps(d: int = 30): void +test(): bool
1-5 Pattern Oriented Programming with C++/Pearce
Note that UML indicates visibility using "+" for public, "#" for protected, and "-" for private. Static attributes and operations are underlined. (We use C++ types such as double, int, bool, and void for pre-defined primitive types.)
Some CASE tools (Computer Aided Software Engineering) can generate class declarations from a class diagram containing a sufficient amount of detail. Let's take a moment to consider how an idealized CASE tool might generate a C++ class declaration from our Airplane class icon.
The Airplane class should be declared inside of a header file called airplane.h, which may include some standard header files. The if-not-defined macro, #ifndef/#endif, is used to prevent airplane.h from being included multiple times within the same implementation file, which would result in a compiler error:
/* * File: airplane.h * Programmer: Pearce * Copyright (c): 2000, all rights reserved. */ #ifndef AIRPLANE_H #define AIRPLANE_H #include
class Airplane { // see below };
#endif
Naturally, C++ will interpret attributes as member variables and operations as member functions. If we zoom in on the class declaration we notice that the public member functions are divided into four groups: constructors and destructor, getters and setters, operations, and the test driver:
1-6 1. Object-Oriented Modeling
class Airplane { public: // constructors & destructor: Airplane(); virtual ~Airplane(); // getters & setters: double getAltitude() const { return altitude; } void setAltitude(double a) { altitude = a; } double getSpeed() const { return speed; } void setSpeed(double s) { speed = s; } // operations: void takeoff(); void fly(); void land(); // test driver: static bool test(); protected: double altitude; double speed; private: void flaps(int d = 30); };
The last two groups are self explanatory. A default constructor is needed to initialize the member variables. An empty virtual destructor is a common feature of a base class. For example, assume MilitaryPlane is derived from Airplane:
class MilitaryPlane: public Airplane { ... };
Deleting an airplane pointer that points to a military plane will automatically call the destructors for both classes:
Airplane* p = new MilitaryPlane(); // do stuff with p ... delete p; // calls ~Airplane() and ~MilitaryPlane()
By default, our idealized CASE tool automatically generates member functions that allow us to read (getters) and modify (setters) each member variable. In the case of airplanes, we may want to comment out the setters so that clients can only modify air speed and altitude using the takeoff(), fly(), and land() operations.
Of course our CASE tool also generates an implementation file called airplane.cpp. No class icon contains enough information to specify how its operations should be implemented, so these are left as stubs. Notice, however, that the constructor is implemented; it simply initializes the member variables to their specified initial values.
1-7 Pattern Oriented Programming with C++/Pearce
/* * File: airplane.cpp * Programmer: Pearce * Copyright (c): 2000, all rights reserved. */ #include "airplane.h"
Airplane::Airplane() { speed = 0.0; altitude = 0.0; } Airplane::~Airplane() {} void Airplane::takeoff() { /* stub */ } void Airplane::fly() { /* stub */ } void Airplane::land() { /* stub */ } void Airplane::flaps(int d /* = 30 */) { /* stub */ } bool Airplane::test() { return true; }
Since we are using an idealized CASE tool, it has thoughtfully provided a main() function in a file called main.cpp:
/* * File: main.cpp * Programmer: Pearce * Copyright (c): 2000, all rights reserved. */ #include "airplane.h"
int main(int argc, char* argv[]) { cout << "Airplane::test() = " << Airplane::test() << endl; return 0; }
Creating files and declarations such as these is often done during early iterations through the implementation phase. After files fill up with implementation details, it can be hard to sort out complex dependencies between files, and simple features such as the default constructor, the virtual destructors, and the getter and setter functions can be forgotten. Also, not all code generators are of the same quality as the one used by our idealized CASE tool, so programmers need to know how to translate class diagrams into declarations by hand.
Object Icons
A UML object diagram shows how an application's important objects are linked together in memory at a moment of time. Objects appear in these diagrams as boxes called object
1-8 1. Object-Oriented Modeling icons. Like a class icon, an object icon is also a labeled box, but there's no danger in confusing the two. The first line of an object icon is the name of the object, if it has one, followed by colon, followed by the name of the class, all underlined. Subsequent lines show each attribute— in the order it was declared— with its current value:
name: Class attribute1 = value1 attribute2 = value2 etc.
For example, assume two airplanes are created and both take off:
Airplane a, *b = new Airplane(); a.takeoff(); b->takeoff();
Assume the first has reached an altitude of 30,000 feet and an air speed of 600 mph, while the second has reached an altitude of 27,000 feet and an air speed of 580 mph. Here are the corresponding object icons:
a: Airplane :Airplane altitude = 30000 altitude = 27000 speed = 600 speed = 580
Notice that we show the name of a, but not b. This is because b isn't the name of an object, it's the name of a pointer to an object. In fact, the actual object b points to is anonymous.
Generalization
Object oriented languages allow us to define subclasses. Instances of a subclass inherit the features of the super class. In addition, subclasses can add new features or modify inherited features.
For example, a Polygon class in a graphics program provides member functions for computing area and perimeter. Triangle and Rectangle subclasses might redefine these functions using simpler formulas or add new formulas and attributes peculiar to triangles or rectangles. Obtuse and acute triangles are instances of subclasses of the Triangle class,
1-9 Pattern Oriented Programming with C++/Pearce while squares are instances of a subclass of Rectangle. In a class diagram we can express the relationship between a subclass and a super class by connecting the class icons with a generalization arrow:
Polygon perimiter() area()
Rectangle Triangle altitude
Square Obtuse Acute
In C++ we can create subclasses using derivation. In this context Polygon is called the base class while Triangle and Rectangle are called derived classes. Of course Triangle is the base class for its derived classes, Obtuse and Acute, while Rectangle is the base class for Square:1
class Polygon { ... }; class Rectangle: public Polygon { ... }; class Square: public Rectangle { ... }; class Triangle: public Polygon { ... }; class Obtuse: public Triangle { ... }; class Acute: public Triangle { ... };
Java syntax makes the relationship between the two classes clearer by declaring that the features of subclasses extend the features inherited from super-classes:
class Polygon { ... } class Rectangle extends Polygon { ... } class Square extends Rectangle { ... } class Triangle extends Polygon { ... } class Obtuse extends Triangle { ... } cloass Accute extends Triangle { ... }
As another example, assume a new release of our flight simulator program will make a distinction between military planes and passenger planes. Of course we will still need to
1 Throughout the text we will use ellipsis "..." to indicate unseen code.
1-10 1. Object-Oriented Modeling keep track of the altitude and speed of both types of planes, and both types of planes will still need to takeoff, fly, and land. In addition, a military plane can drop bombs and has a Boolean attribute indicating if it is flying at a supersonic speed, while a passenger plane can show movies to its passengers and has an integer attribute indicating how many passengers are on board.
We could replace the Airplane class with PassengerPlane and MilitaryPlane classes, but then we would need to re-implement the takeoff(), fly(), and land() operations. Worse yet, we would need to re-implement these operations twice, once for each new class. Instead, we make the PassengerPlane and MilitaryPlane subclasses of the Airplane class. Now each new class inherits the takeoff(), fly(), and land() operations as well as the altitude and speed attributes from the Airplane super class:
Airplane #altitude #speed +takeoff() includes +fly() private & +land() commercial
PassengerPlane MilitaryPlane -onBoard: int -superSonic: bool +showMovies() +dropBombs()
Our class diagram introduces two new features. First, instead of drawing two generalization arrows, we combined them into a single forked arrow. This makes the diagram easier to read and makes it easier to add new subclasses later. Second, we have attached a note to the PassengerPlane class icon. A note is a dog-eared box containing a comment. It has no impact on the implementation, but it can make the class diagram easier to understand.
Assume we create and takeoff in one of each type of airplane:
MilitaryPlane a; PassengerPlane b; a.takeoff(); b.takeoff();
1-11 Pattern Oriented Programming with C++/Pearce
Assume our military plane has reached an altitude of 50,000 feet and a speed of 700 miles per hour, while our passenger plane— which has 80 passengers on board— has reached an altitude of 28,000 feet and a speed of 550 miles per hour. The object diagram depicting this scenario clearly shows the altitude and speed attributes each plane inherits from the Airplane super class:
a: MilitaryPlane b: PassengerPlane altitude = 50000 altitude = 28000 speed = 700 speed = 550 superSonic = true onBoard = 80
Note that the inherited attributes are listed above the subclass attributes. In fact, this corresponds to the way these objects would be laid out in memory in Java and C++. Thus, instances of MilitaryPlane and PassengerPlane literally are instances of Airplane, but with additional fields hanging off the bottom.
This last observation suggests that it is possible to write generic algorithms that are indifferent to the types of airplanes they process. Such an algorithm can use an airplane pointer to point at both military and passenger planes:
Airplane* p = 0; // later: p = new PassengerPlane(); // implicit upcast p->takeoff(); p->fly(); p->land(); delete p; // and still later: p = new MilitaryPlane(); // implicit upcast p->takeoff(); p->fly(); p->dropBombs(); // this fails
Note that the C++ compiler automatically retypes MilitaryPlane and PassengerPlane pointers to Airplane pointers. This is called an implicit upcast. The term "implicit" means the operation is performed automatically; the programmer doesn't need to tell the compiler to do it. The term "upcast" indicates that we are retyping a subclass pointer as a super-class pointer. C++ is willing to perform upcasts because a pointer to a subclass instance literally is a pointer to a super-class instance.
1-12 1. Object-Oriented Modeling
However, the compiler rejects the last line:
p->dropBombs(); // this fails
This happens because the compiler doesn't know that p will point to a military plane at the moment this line is executed. Of course we can tell the compiler that this is the case by performing an explicit downcast:
((MilitaryPlane*)p)->dropBombs();
An alternate syntax uses the static_cast<> operator:
(static_cast
But what happens if we are wrong. For example, suppose control arrives at this line of code through some unanticipated route that bypasses the place where p is pointed at a military plane:
Airplane* p = new PassengerPlane(); p->takeoff(); p->fly(); p->land(); goto Later; delete p; p = new MilitaryPlane(); Later: p->takeoff(); p->fly(); (static_cast
If we are lucky, the program will simply crash, because p doesn't point at a military airplane. If we are unlucky, then the program won't crash; it will simply produce the wrong behavior (maybe it will rain suitcases in Iowa).
If we aren't sure what type of plane p will point at, then we can always use a dynamic cast, which returns 0, the null pointer, if the cast doesn't make sense:2
MilitaryPlane* mp = 0; if (mp = dynamic_cast
2 Dynamic casting is discussed in chapter 5 and in Appendix 1.
1-13 Pattern Oriented Programming with C++/Pearce
Links and Associations
In UML a relationship between two (or more) classes is called an association, and pairs (or tuples) of related objects are called links. For example, an on-line world atlas might represent the relationship "City X is in Country Y" and the relationship "City X is capitol of Country Y" as two associations between the class City and the class Country. In UML an association is a line segment connecting the icons of the related classes:
isIn > City Country capitolOf >
In this example we have labeled our associations with names and directions. This is the only way to distinguish an association from its inverse association in UML, although in most of our examples the distinction won't be important.
In an object diagram we represent the links "Washington D.C. is capitol of the USA," "Philadelphia is in the USA," and "Washington D.C. is in the USA" with line segments connecting object icons:
capitolOf > Washington DC: City USA: Country isIn >
isIn ^
Philadelphia: City
The relationship between links and associations is analogous to the relationship between objects and classes. Sometimes links are referred to as association instances.
Suppose we want to enhance our flight simulator program by allowing users to assemble and command entire fleets of airplanes. In addition, users can customize airplanes by changing the wings, engines, fuselage, and other parts. We begin by adding Fleet and Wing classes to our class diagram. We use associations to represent the relationship between a fleet and its members and the relationship between an airplane and its wings:
1-14 1. Object-Oriented Modeling
Fleet Airplane Wing add() flaps() remove()
MilitaryPlane PassengerPlane
Note that it would have been inappropriate to represent these relationships using generalization, because a wing is not a special type of airplane, and an airplane is not a special type of fleet.
Association Roles
The endpoints of an association are called roles. We can add information about an association by attaching navigation arrows, multiplicities, and names to its roles.
Multiplicity
An m-to-n relationship between classes A and B relates a single instance of A to n instances of B, and relates m instances of A to a single instance of B. For example, suppose every airplane in a fleet has exactly three pilots who are authorized to fly it, and suppose every pilot is authorized to fly exactly two planes in the fleet, then the relationship "Pilot X is authorized to fly airplane Y" is a 2-to-3 relationship. In UML we can indicate the multiplicity of a relationship by labeling each role with the multiplicity of its adjacent class:
3 Pilot 2 Airplane flies >
In a corresponding object diagram, we would expect to see every pilot object linked to two distinct airplane objects, and every airplane object linked to three distinct pilot objects:
1-15 Pattern Oriented Programming with C++/Pearce
p1: Pilot
a1: Airplane p2: Pilot a2: Airplane
p3: Pilot
p4: Pilot
a3: Airplane p5: Pilot a4: Airplane
p6: Pilot
If every airplane has 3 or 5 pilots who are authorized to fly it, then we can represent this multiplicity as a sequence:
3,5 Pilot 2 Airplane flies >
If every airplane has 3 to 5 pilots who are authorized to fly it, then we can represent this multiplicity as a range:
3..5 Pilot 2 Airplane flies >
Clearly, every wing is attached to exactly one plane, and every plane has exactly two wings, so the association between Airplane and Wing has 1-to-2 multiplicity. An airplane can only belong to one fleet at a time, but the number of airplanes that belongs to a fleet is zero or more. In UML we use an asterisk to indicate zero or more:
1 * 1 2 Fleet Airplane Wing
Navigation
An association between airplanes and wings doesn't necessarily imply that an airplane knows which wings hold it up or that a wing knows which airplane it is attached to.
1-16 1. Object-Oriented Modeling
However, an airplane will probably need to send signals to its wings and vice-versa, so it is important that an Airplane object can quickly determine which Wing objects are holding it up, and a Wing object we can quickly determine which Airplane object it is attached to. This property is called navigability. The association between Airplane and Wing has bi- directional navigability. We can represent this by drawing barbed arrowheads at each end of the association. (Don't get creative with arrowheads. They have very specific meanings in UML.)
Clearly an object representing a fleet of planes should know which planes belong to it, but planes move from one fleet to another, and which fleet a plane belongs to will make no difference in how the plane works, so the association between Fleet and Airplane has unidirectional navigability, which UML indicates by drawing an arrowhead only on the Airplane end of the association:
1 * 1 2 Fleet Airplane Wing
In this book an association without navigation arrows is assumed to have undetermined navigability.
Navigation arrowheads can be placed on links, too. For example, the following object diagram shows a fleet containing two airplanes. Airplane a is connected to two wings, w1 and w2:
w2: Wing
a: Airplane united: Fleet
w1: Wing b: Airplane
1-17 Pattern Oriented Programming with C++/Pearce
Of course the links between an airplane and its wings are bi-directional. In other words, if Airplane a is connected to Wing w1, then w1 is connected to a.
Implementing Associations
In most examples in this book we will represent links using C++ pointers. For example, our idealized CASE tool interprets the bi-directional, 1-to-2 association between Airplane and Wing by adding two Wing pointers to the Airplane class:
class Airplane { public: void setLeftWing(Wing* w); // see below void setRightWing(Wing* w); // see below Wing* getLeftWing() const { return leftWing; } Wing* getRightWing() const { return rightWing; } // etc. protected: Wing *leftWing, *rightWing; // etc. }; and an Airplane pointer to the Wing class:
class Wing { public: Wing(); virtual ~Wing() {} Airplane* getAirplane() const { return airplane; } void setAirplane(Airplane* p) { airplane = p; } private: Airplane* airplane; };
Of course the usual getter and setter functions are automatically provided, and the pointers are initialized to 0 by the default constructors:
Airplane::Airplane() { leftWing = 0; rightWing = 0; // etc. }
1-18 1. Object-Oriented Modeling
Wing::Wing() { airplane = 0; }
However, there are several constraints implicit in our class diagram that require some effort to enforce in C++. For example, the class diagram tells us that every airplane object is linked to two distinct wing objects, but there is nothing to prevent the left and right wing pointers of a C++ airplane from pointing to the same C++ wing. We should attempt to enforce this in setLeftWing() and setRightWing(). For example:
void Airplane::setLeftWing(Wing* w) { if (w != rightWing) leftWing = w; }
Another constraint implicit in our class diagram is that if airplane a is connected to wing w, then wing w is connected to airplane a, and not some other airplane. Conversely, if wing w is connected to airplane a, then airplane a is connected to wing w. We could enforce this in setLeftWing() and setRightWing(), too. For example, setLeftWing() can disconnect the plane from the old left wing, connect the new wing to the plane, then connect the plane to the new wing:
void Airplane::setLeftWing(Wing* w) { if (w != rightWing) { // disconnect this from old wing: if (leftWing) leftWing->setAirplane(0); // connect new wing to this: leftWing = w; // connect this to new wing: if (leftWing) leftWing->setAirplane(this); } }
Of course a fumbling user might directly connect a wing to the wrong airplane:
Airplane a, b; Wing w; a.setLeftWing(&w); // a connected to w, w connected to a w.setAirplane(&b); // a connected to w, w connected to b
This creates an inconsistency because the setAirplane() function simply sets the airplane pointer to a new airplane. Of course an improved setAirplane() function could attempt to:
1-19 Pattern Oriented Programming with C++/Pearce
1. disconnect this wing from the old airplane; 2. connect this wing to the new airplane; 3. connect the new airplane to this wing;
There are several problems with this approach. First, was this wing the left or right wing of the old plane, and is it to be the left or right wing of the new plane? Second, how will the wing be attached to the new plane? For example, will we call the setLeftWing() function to do this? If we are not careful, this could result in a non-terminating recursion.
Perhaps we are giving too much freedom to our users. Perhaps users should only be allowed to connect wings to airplanes, but not airplanes to wings. We can enforce this by making setAirplane() a private member function. But then Airplane can't call this function either. In C++ we can solve this problem by declaring the Airplane class to be a friend of the Wing class:
class Wing { public: Wing(); virtual ~Wing(); Airplane* getAirplane() const { return airplane; } private: friend class Airplane; void setAirplane(Airplane* p) { airplane = p; } Airplane* airplane; };
Resolving File Dependencies
Our CASE tool generates separate header and implementation files for each class icon. We can represent these files and the dependencies between them by drawing a dependency graph:
airplane.h wing.h
airplane.cpp wing.cpp
1-20 1. Object-Oriented Modeling
In this diagram (which is not a UML diagram), the dashed arrows indicate dependencies. For example, if the programmer makes changes to the wing.h file, for example if the setAirplane() is renamed setPlane(), then this could force changes to be made to wing.cpp, airplane.h, and by transitivity, to airplane.cpp.
Normally, dependencies between files are resolved using include directives. For example, airplane.cpp and wing.h can include airplane.h, and wing.cpp and airplane.h can include wing.h. However, some preprocessors will get confused by the fact that airplane.h includes wing.h and wing.h includes airplane.h.3 The preprocessor might get caught in an infinite loop. One common trick for solving this problem is to resolve the dependency from wing.h to airplane.h by using a forward reference:
// wing.h class Airplane; // forward reference
class Wing { public: Wing(); virtual ~Wing(); Airplane* getAirplane() const { return airplane; } private: friend class Airplane; void setAirplane(Airplane* p) { airplane = p; } Airplane* airplane; };
This works provided wing.h doesn't attempt to call any Airplane member functions. After all, the compiler only knows that Airplane is the name of a class. No other information is given. Calls to Airplane member functions must be moved to wing.cpp, which creates a direct dependency from wing.cpp to airplane.h. But this dependency can be resolved without circularities by including airplane.h in wing.cpp:
3 Actually, the circular includes won't pose a problem as long as they appear within the #ifndef/#endif directives we conventionally place in our header files. Programmers still need to be aware of other tricks for resolving circular dependencies.
1-21 Pattern Oriented Programming with C++/Pearce
#include airplane.h wing.h
class Airplane
#include #include #include
airplane.cpp wing.cpp
In summary, the simple bi-directional association between the Airplane and Wing class hides many unpleasant implementation details: How will the implicit constraints be enforced in C++? How will the bi-directional dependency between wing.h and airplane.h be resolved? Both of these issues need to be resolved by an expert programmer during early iterations through the implementation phase.
Naming Roles
Notice that our CASE tool cleverly selected the names leftWing and rightWing for the two Wing pointers encapsulated by an Airplane. Of course a real CASE tool wouldn't know how to differentiate between the two wing pointers, and so would probably choose to store them in a small array:
class Airplane { public: Airplane(); void setWing(Wing* w, int i); Wing* getWing(int i) const { return wing[i]; } // etc. private: Wing* wing[2]; // etc. };
Explicit Constraints
Suppose we add an attribute called side to our Wing class that distinguishes between left and right wings:
1-22 1. Object-Oriented Modeling
class Wing { public: enum Orientation { LEFT, RIGHT }; Orientation getSide() { return side; } void setSide(Orientation s) { side = s; } // etc. private: Orientation side; // etc. };
We can attach constraints to the Wing role of our Airplane-to-Wing associations to require that an airplane's left wing is a left-side wing and its right wing is a right-side wing. A constraint is a Boolean-valued condition bracketed by curly braces. Although OCL, the Object Constraint Language, is a proposed standard language for expressing constraints, we will often simply use Boolean-valued C++ expressions or even informal expressions.
{leftWing->side == LEFT && rightWing->side == RIGHT}
1 2 Wing Airplane side
Unlike notes, constraints do impact the implementation, even though the implementation may be ad hoc. For example, we might enforce the orientation constraints on wings in the Airplane's wing setter functions:
void Airplane::setLeftWing(Wing* w) { if (w->side == Wing::LEFT) { // disconnect this from old wing: if (leftWing) leftWing->setAirplane(0); // connect new wing to this: leftWing = w; // connect this to new wing: if (leftWing) leftWing->setAirplane(this); } }
1-23 Pattern Oriented Programming with C++/Pearce
Containers
How will our magic CASE tool represent the association between Fleet and Airplane? Of course the association is unidirectional, so we won't need to make any changes to our Airplane class. But instances of our Fleet class need to hold zero or more Airplane pointers. Placing a static array of Airplane pointers in the Fleet class will impose a maximum size on fleets and will be difficult to manage. Instead, a linked list, dynamic array, or set should be used. Several libraries include implementations of these data structures, including the standard C++ library, which provides vector
class Fleet { public: typedef set
Alternatively, we can make Fleet a subclass that privately inherits the features of its set
class Fleet: set
The need for private inheritance becomes clear if we want to place restrictions on the type of airplanes that can be added to a fleet. For example, assume all airplanes must pass a quality test before they can be added to a fleet:
4 The standard library container templates are discussed in Appendix 1.
1-24 1. Object-Oriented Modeling
class Fleet: set
Private inheritance prevents low quality planes from being added to the fleet through the "back door". For example, assume a particular plane doesn't pass the quality test for a particular fleet:
Fleet panAm; Airplane junkHeap; panAm.add(&junkHeap); // this fails
Thanks to private inheritance, Fleet clients can't call member functions inherited from the set
panAm.insert(&junkHeap); // this fails, too!
Composition
In UML we can indicate that instances of class A contain instances of class B rather than pointers to instances of class B by using composition. For example, assume each airplane instance contains two instances of a Date class representing the last time the plane was inspected and the last time the plane was flown. This can be specified in a class diagram by a composition "arrow" connecting the Airplane and Date classes:
Airplane 2 Date
Our idealized code generator generates adds to Date member variables to the Airplane class:
class Airplane { Date inspected, flown; // etc. };
1-25 Pattern Oriented Programming with C++/Pearce
Conceptually, composition is be used to represent the relationship between an assembly and its components, but we have to be careful with this. For example, if we represent the relationship between Airplane and Wing using composition:
class Airplane { public: Wing getLeftWing() { return leftWing; } Wing getRightWing() { return rightWing; } // etc. protected: Wing leftWing, rightWing; // etc. }; then executing the statements:
Airplane a; Wing w = a.getLeftWing(); assigns a copy of a.leftWing to w. At this point there are two objects in our application that represent the same wing in the application domain. This may not be a problem if modeling wings isn't an important requirement for our application. For example, our flight simulator program might have many objects representing the date "January 1, 2000" without causing any confusion, but it can lead confusion for important domain objects. For example, raising the flaps on the left wing of a real airplane is unambiguous, but raising the flaps on the virtual left wing of a virtual airplane is ambiguous if there are lots of copies of this wing floating around in memory. By contrast, if we were developing a program to help mechanics schedule airplane inspections, then having several objects representing the same date could be a source of confusion.
In Java associations and compositions are both represented using references:
class Airplane { protected Wing leftWing, rightWing; // etc. }
Although this appears to be composition, leftWing and rightWing are actually references to heap-based Wing objects.
1-26 1. Object-Oriented Modeling
Aggregation
UML also includes a weaker form of composition called aggregation. Conceptually, an aggregation relationship between classes A and B indicate that an instance of A is merely a collection of instances of B. The same instance of B might simultaneously belong to many collections. One of these collections may cease to exist without any effect on its members.
We might have represented the relationship between fleets and airplanes using aggregation, because the same airplane might simultaneously belong to several fleets, and a fleet might disband, but the airplanes that belonged to the fleet still exist. Aggregation is represented in a class diagram by an aggregation "arrow" connecting the aggregate class to its member class:
Fleet * Airplane
Although aggregation may have conceptual value (and even this is debatable), it doesn't seem to suggest anything about implementation beyond what is already suggested using an ordinary association.
Packages
A package is a named set of classes, functions, and sub-packages. Packages are useful for partitioning large programs and libraries into subsystems and sub-libraries. A package diagram shows an application's important packages and their dependencies. Packages appear in these diagrams as labeled folders called package icons. A dependency is a dashed arrow pointing from an importer package to an exporter package, and indicates that changes to the exporter package may force changes to the importer package.
For example, the following package diagram indicates that components in the Business Logic package import (use) components defined in the Database package, while components in the User Interface package import components defined in the Business Logic package. Of course some of these components may have been imported by the Business Logic package, so the dependency relationship is transitive.
1-27 Pattern Oriented Programming with C++/Pearce
User Interface
Business Logic
Database
Packages can be implemented in C++ using names spaces:5
namespace Database { class Query { ... }; class Table { ... }; // etc. }
namespace BusinessLogic { using namespace Database; class Transaction { ... }; class Customer { ... }; // etc. }
namespace UserInterface { using namespace BusinessLogic; class DialogBox { ... }; class Menu { ... }; // etc. }
Java has a similar packaging mechanism.
Stereotypes
UML can be extended using stereotypes. A stereotype is an existing UML icon with a stereotype label of the form:
<
5 C++ namespaces are discussed in Appendix 1.
1-28 1. Object-Oriented Modeling
A collaboration is a group of classes that work together to achieve a particular goal. Although a certain collaboration may have wide applicability, the actual classes that appear in this collaboration may have different names and meanings from one application to the next. In this case stereotypes can be used to indicate the role a class plays within the collaboration. For example, suppose Secretary class has a member function called type() that creates Report objects:
Report* Secretary::type(...) { return new Report(...); }
In Chapter 3 we will learn that this type of a member function is called a factory method. The class containing the factory method plays the role of a "factory", the return type of the factory method plays the role of a "product", and the association between the factory and product classes is that the factory class creates instances of the product class. We can suggest these associations by simply attaching stereotypes to the icons in our diagram:
<
Commonly used UML class stereotypes include:
<
<
<
<
<
<
<
<
In some cases a stereotype is so common that it earns its own icon. For example, in UML actors are sometimes represented by stick figures:
1-29 Pattern Oriented Programming with C++/Pearce
<
Interfaces
When it is time to upgrade or replace a chip in a computer, the old chip is simply popped out of the motherboard, and the new chip is plugged in. It doesn't matter if the new chip and old chip have the same manufacturer or the same internal circuitry, as long as they both "look" the same to the motherboard and the other chips in the computer. The same is true for car, television, and sewing machine components. Open architecture systems and "Pluggable" components allow customers to shop around for cheap, third-party generic components, or expensive, third-party high-performance components.
A software component is an object that is known to its clients only through the interfaces it implements. Often, the client of a component is called a container. If software components are analogous to pluggable computer chips, then containers are analogous to the motherboards that contain and connect these chips. For example, an electronic commerce server might be designed as a container that contains and connects pluggable inventory, billing, and shipping components. A control panel might be implemented as a container that contains and connects pluggable calculator, calendar, and address book components. Java Beans and ActiveX controls are familiar examples of software components.
Interfaces and Components in UML
Modelers can represent interfaces in UML class diagrams using class icons stereotyped as interfaces. The relationship between an interface and a class that realizes or implements it is indicated by a dashed generalization arrow:
1-30 1. Object-Oriented Modeling
<
<
XComponent YComponent ZComponent serviceA() serviceA() serviceA() serviceB() serviceB() serviceB() serviceC() serviceC() serviceC() etc. etc. etc.
Notice that the container doesn't know the type of components it uses. It only knows that its components realize or implement the IComponent interface.
For example, imagine that a pilot flies an aircraft by remote control from inside of a windowless hangar. The pilot holds a controller with three controls labeled: TAKEOFF, FLY, and LAND, but he has no idea what type of aircraft the controller controls. It could be an airplane, a blimp, a helicopter, perhaps it's a space ship. Although this scenario may sound implausible, the pilot's situation is analogous to the situation any container faces: it controls components blindly through interfaces, without knowing the types of the components. Here is the corresponding class diagram:
1 1 <
Helicopter Blimp Airplane takeoff() takeoff() takeoff() fly() fly() fly() land() land() land() hover() deflate() bank() etc. etc. etc.
Notice that all three realizations of the Aircraft interface support additional operations: airplanes can bank, helicopters can hover, and blimps can deflate. However, the pilot
1-31 Pattern Oriented Programming with C++/Pearce doesn't get to call these functions. The pilot only knows about the operations that are specifically declared in the Aircraft interface.
We can create new interfaces from existing interfaces using generalization. For example, the Airliner interface specializes the Aircraft and (Passenger) Carrier interfaces. The PassengerPlane class implements the Airliner interface, which means that it must implement the operations specified in the Aircraft and Carrier interfaces as well. Fortunately, it inherits implementations of the Aircraft interface from its Airplane super- class:
<
Airplane <
PassengerPlane add(Passenger p) rem(Passenger p) serveCocktails() etc.
An interface is closely related to the idea of an abstract data type (ADT). In addition to the operator prototypes, an ADT might also specify the pre- and post-conditions of these operators. For example, the pre-condition for the Aircraft interface's takeoff() operator might be that the aircraft's altitude and airspeed are zero, and the post-condition might be that the aircraft's altitude and airspeed are greater than zero.
1-32 1. Object-Oriented Modeling
Interfaces and Components in Java
Java allows programmers to explicitly declare interfaces:
interface Aircraft { public void takeoff(); public void fly(); public void land(); }
Notice that the interface declaration lacks private and protected members. There are no attributes, and no implementation information is provided.
A Pilot uses an Aircraft reference to control various types of aircraft:
class Pilot { private Aircraft myAircraft; public void fly() { myAircraft.takeoff(); myAircraft.fly(); myAircraft.land(); } public void setAircraft(Aircraft a) { myAircraft = a; } // etc. }
Java also allows programmers to explicitly declare that a class implements an interface:
class Airplane implements Aircraft { public void takeoff() { /* Airplane takeoff algorithm */ } public void fly() { /* Airplane fly algorithm */ } public void land() { /* Airplane land algorithm */ } public void bank(int degrees) { /* only airplanes can do this */ } // etc. }
The following code shows how a pilot flies a blimp and a helicopter:
Pilot p = new Pilot("Charlie"); p.setAircraft(new Blimp()); p.fly(); // Charlie flies a blimp! p.setAircraft(new Helicopter()); p.fly(); // now Charlie flies a helicopter!
It is important to realize that Aircraft is an interface, not a class. As such, it cannot be instantiated:
1-33 Pattern Oriented Programming with C++/Pearce
p.setAircraft(new Aircraft()); // error!
Java also allows programmers to create new interfaces from existing interfaces by extension:
interface Airliner extends Aircraft, Carrier { public void serveCocktails(); }
Although a Java class can only extend at most one class (multiple inheritance is forbidden in Java), a Java interface can extend multiple interfaces and a Java class can implement multiple interfaces. A Java class can even extend and implement at the same time:
class PassengerPlane extends Airplane implements Airliner { public void add(Passenger p) { ... } public void rem(Passenger p) { ... } public void serveCocktails() { ... } // etc. }
Interfaces and Components in C++
C++ is much older than Java, so it doesn't allow programmers to explicitly declare interfaces. Instead, we'll have to fake it using classes that only contain public, pure virtual functions:
class Aircraft // interface { public: virtual void takeoff() = 0; virtual void fly() = 0; virtual void land() = 0; };
A client references a component through an interface typed pointer:
1-34 1. Object-Oriented Modeling
class Pilot { public: void fly() { myAircraft->takeoff(); myAircraft->fly(); myAircraft->land(); } void setAircraft(Aircraft* a) { myAircraft = a; } // etc. private: Aircraft* myAircraft; };
Instead of explicitly declaring that a class implements an interface as one does in Java, C++ programmers must declare that a class is derived from an interface. Because all of the interface member functions are pure virtual, this will require the derived class to provide implementations:
class Airplane: public Aircraft { public: void takeoff() { /* Airplane takeoff algorithm */ } void fly() { /* Airplane fly algorithm */ } void land() { /* Airplane land algorithm */ } void bank(int degrees) { /* only airplanes can do this */ } // etc. };
In the following code snippet a pilot first flies a blimp, then a helicopter. Unlike Java, the question of deleting the blimp remains open:
Pilot p("Charlie"); p.setAircraft(new Blimp()); p.fly(); // Charlie flies a blimp! p.setAircraft(new Helicopter()); p.fly(); // now Charlie flies a helicopter!
Interfaces and be constructed from existing interfaces through the C++ derived class mechanism, too:
class Airliner: public Aircraft, public Carrier { public: virtual void serveCocktails() = 0; };
Here is a C++ implementation of the Airliner interface:
1-35 Pattern Oriented Programming with C++/Pearce
class PassengerPlane: public Airplane, public Airliner { public: void add(Passenger p) { ... } void rem(Passenger p) { ... } void serveCocktails() { ... } };
Abstract Classes
Classes only inherit obligations from the interfaces they implement—obligations to implement the specified member functions. By contrast, an abstract class is a partially defined class (these will be discussed in detail in Chapter 3). Classes derived from an abstract class inherit both features and obligations. For example, airplanes, blimps, and helicopters all have altitude and speed attributes. Why not declare these attributes, as well as their attending getter and setter functions, in the Aircraft base class:
class Aircraft { public: Aircraft(double a = 0.0, double s = 0.0) { altitude = a; speed = s; } virtual ~Aircraft() {} double getSpeed() const { return speed; } double getAltitude() const { return altitude; } void setSpeed(double s) { speed = s; } void setAltitude(double a) { altitude = a; } // these functions must be defined by derived classes: virtual void takeoff() = 0; virtual void fly() = 0; virtual void land() = 0; protected: double altitude, speed; };
Of course technically, Aircraft is no longer an interface, because it contains members other than public pure virtual functions. But takeoff(), fly(), land() will still be pure virtual functions, which means users still won't be allowed to instantiate the Aircraft class.
Names of abstract classes and virtual functions are italicized in UML:
1-36 1. Object-Oriented Modeling
1 1 Aircraft Pilot altitude speed takeoff() fly() land()
Helicopter Blimp Airplane takeoff() takeoff() takeoff() fly() fly() fly() land() land() land() hover() deflate() bank() etc. etc. etc.
Interaction Diagrams
By itself, an object diagram isn't very useful. It becomes much more useful when it shows typical interaction sequences between the objects. An interaction occurs when a client object sends a message or invokes a member function of a server object. The server may or may not return a value to the client.
UML provides two types of interaction diagrams: collaboration diagrams and sequence diagrams. In this book we will use sequence diagrams. At the top of a sequence diagram is a row of object icons. A life line hangs below each icon. If a and b are objects, and if a calls b.fun() at time t, then we would draw a horizontal arrow labeled "fun" that emanates from a's life line at time t, and terminates at b's life line. The exact location of time t on a life line isn't as important as its relative position. Time flows from the top of the diagram to the bottom.
For example, assume a point of sale terminal (POST) records a sale by:
1. Checking inventory to see if item is in stock 2. Debiting the customer's account. 3. Crediting the retailer's account. 4. Updating inventory. 5. Printing a receipt.
Here is the corresponding sequence diagram:
1-37 Pattern Oriented Programming with C++/Pearce
customer retailer :POST :Account :Account :Inventory
inStock?
withdraw
deposit
update
makeReceipt
Object-Oriented Design
The goal of every program is to be useful (solve the right problems), usable (easy to use), and modifiable (easy to maintain). Two important design principles that help developers achieve the last goal are modularity and abstraction:
The Modularity Principle Programs should be constructed out of cohesive, loosely coupled modules (classes). The Abstraction Principle The interface of a module (class) should be independent of its implementation. A cohesive class has a unified purpose, while loose coupling implies dependencies on other classes are minimal. Taken together, this makes a class easier to reuse, replace, and understand. The abstraction principle implies that the clients of class A (now defined as classes that depend on A) don't need to understand the implementation of A in order to use it. Conversely, the implementer of A is free to change implementation details without worrying about breaking the client's code.
Cohesion
The member functions of a cohesive class work together to achieve a common goal. Classes that try to do too many marginally related tasks are difficult to understand, reuse, and maintain.
1-38 1. Object-Oriented Modeling
Although there is no precise way to measure the cohesiveness of a class, we can identify several common "degrees" of cohesiveness. At the low end of our spectrum is coincidental cohesion. A class exhibits coincidental cohesion if the tasks its member functions perform are totally unrelated:
class MyFuns { public: void initPrinter(); double calcInterest(); Date getDate(); };
The next step up from coincidental cohesion is logical cohesion. A class exhibits logical cohesion if the tasks its member functions perform are conceptually related. For example, the member functions of the following class are related by the mathematical concept of area:
class AreaFuns { public: double circleArea(); double rectangleArea(); double triangleArea(); };
A logically cohesive class also exhibits temporal cohesion if the tasks its member functions perform are invoked at or near the same time. For example, the member functions of the following class are related by the device initialization concept, and they are all invoked at system boot time:
class InitFuns { public: void initDisk(); void initPrinter(); void initMonitor(); };
One reason why coincidental, logical, and temporal cohesion are at the low end of our cohesion scale is because instances of such classes are unrelated to objects in the application domain. For example, suppose x and y are instances of the InitFuns class:
InitFuns x, y;
1-39 Pattern Oriented Programming with C++/Pearce
How can we interpret x, and y? What do they represent? How are they different?
A class exhibits procedural cohesion, the next step up in our cohesion scale, if the tasks its member functions perform are steps in the same application domain process. For example, if the application domain is a kitchen, then cake making is an important application domain process. Each cake we bake is the product of an instance of a MakeCake class:
class MakeCake { public: void addIngredients(); void mix(); void bake(); };
A class exhibits informational cohesion if the tasks its member functions perform are services performed by application domain objects. Our Airplane class exhibits informational cohesion, because different instances represent different airplanes:
class Airplane { public: void takeoff(); void fly(); void land(); };
Note that the informational cohesion of this class is ruined if we add a member function for computing taxes or browsing web pages.
Coupling
An association from class A to class B implies a dependency of A on B. Changes to B could force changes to A. The question is, what type of changes to B are likely to force changes in A? If A and B are loosely coupled, only major changes to certain member functions of B should impact A. If A and B are tightly coupled, then small changes to B can have a dramatic impact on A.
Although there is no precise way to measure how tightly an association couples one class to another, we can identify several common coupling "degrees". For example, assume an E- commerce server keeps track of customers and the transactions they commit:
1-40 1. Object-Oriented Modeling
< commits Transaction Customer * 1
Normally, this would mean that the Transaction class has a member variable that points to a Customer:
class Transaction { Customer* customer; // etc. };
Some changes to the Customer class will impact the Transaction class, but some will not. For example, changing the private members of the Customer class should have no impact. This is the most common form of coupling. For lack of a better term, we will call this client coupling.
On the other hand, if the Transaction class is a friend of the Customer class:
class Customer { friend class Transaction; // etc. };
Then Transaction is content coupled to Customer. Changes to the private members of Customer could impact Transaction. Declaring one class to be the friend of another tightens the coupling between the two classes.
If Customer is an interface for corporate and individual customers:
< commits <
Corporate Individual
Then the Transaction class can't even be sure what type of object its customer pointer points at. There is no mention in the Transaction class of corporate or individual customers,
1-41 Pattern Oriented Programming with C++/Pearce only customers. Transactions can call public Corporate and Individual member functions that are explicitly declared in the Customer interface. Other public member functions such as Corporate::getCEO() or Individual::getSpouse() are not visible to transaction objects. Transaction exhibits interface coupling with the Corporate and Individual classes. Obviously interface coupling is looser than client coupling.
Message passing also helps to loosen the coupling between objects. For example, suppose an object representing an ATM machine mediates between transactions and customers:
Transaction Customer
ATM
In this case transactions and customers communicate by passing messages through the ATM machine, which means that the transaction doesn't even need to know the location of the customer. We shall call this message coupling. Short of totally uncoupled, we can achieve the loosest form of coupling by combining interface and message coupling.
Problems
Problem 1.1: Modeling Application Domains
An application domain is the real world context of an application: bank, warehouse, space ship, etc. Often, a specification document includes a UML class diagram that represents the application domain's important concepts and relationships as classes and associations respectively. In each of the following problems draw a UML class diagram that models the important concepts and their relationships in the application domain described. You may draw the diagram by hand, with a diagram editor, or by using a CASE tool.
Next, faithfully translate your class diagram into C++ class declarations. Be sure to include all supporting functions implied by your diagram. For example, each member variable requires initialization as well as setter and getter functions (getAAA(), setAAA()). Each
1-42 1. Object-Oriented Modeling container member should provide clients with functions for adding and removing elements as well as traversing the container. Do not invent new member variables or member functions. Each class should be declared in its own header file and should have its own source file (even if it's empty).
A scenario description follows each domain description. Draw an object diagram that instantiates your class diagram and models the scenario. Implement a main() function in main.cpp so that it creates the objects and links in your object diagram. Insert diagnostic messages in main() to prove your program compiles and runs.
Problem 1.1.1
Domain: A course is taught in a school by a teacher. There are two types of courses: seminars and lectures. Any number of students may take a course, but a student may take no more than five courses per term. Teachers teach from two to four courses per term.
Scenario: Bill Jones and Sue Smith are students at Cambridge University, where they both take a Physics seminar taught by Professor Newton.
Problem 1.1.2
Domain: A warehouse has any number of aisles, an aisle has any number of bins, a bin has any number of boxes, and a box contains any number of items.
Scenario: The AA Warehouse stores whiskey in bin 3 of aisle 6, beer in bin 6 of aisle 3, and wine in bin 2 of aisle 6.
Problem 1.1.3
Domain: A play has many characters. A play occurs on a stage and has three acts. Each act has three scenes. A character may be played by many different actors, and an actor may play different characters.
Scenario: Hamlet is being performed at the Globe theater. Mel Gibson plays Hamlet, and Drew Barrymore plays Ophelia.
Problem 1.1.4
Domain: A tennis tournament has many matches. Each match is between two players and consists of six or seven games. Each game consists of six or seven sets, and each set consists of five or more points.
1-43 Pattern Oriented Programming with C++/Pearce
Scenario: Venus Williams is playing her sister, Serena, in the championship match at the US Open. Venus wins the first game of the match: 5-0, 4-2, 2-4, 5-1, and 4-2.
Problem 1.1.5
Domain:
A C++ program is a sequence of declarations:
PROGRAM ::= DECLARATION ...6
Besides declarations, there are two other types of C++ statements: expressions and control structures:
STATEMENT ::= DECLARATION | EXPRESSION | CONTROL
There are four types of control structures: conditional (if-else, switch), iterative (for, do-while, and while), jump (break, continue, goto, return), and block.
CONTROL ::= CONDITIONAL | ITERATIVE | JUMP | BLOCK
A block is a sequence of statements between curly braces:
BLOCK ::= { STATEMENT ... }
An if-else statement consists of an expression (the condition), and one or two statements (the consequent and the alternative):
IF ::= if (EXPRESSION) STATEMENT [else STATEMENT]
A while or do-while statement consists of a condition (the loop condition) and a statement (the iterate):
WHILE ::= while (EXPRESSION) STATEMENT DO ::= do STATEMENT while (EXPRESSION);
Scenario:
if (x < y) x = 0; else x = y;
6 We are using a simplified version of extended Bachus-Naur form (EBNF) to describe syntax rules. (See [PEA] for my EBNF conventions.)
1-44 1. Object-Oriented Modeling
Problem 1.1.6
Domain: A hospital has many patients. Each patient has one doctor, although a doctor may have several patients. Tests are performed on each patient resulting in many measurements that must be recorded in a data base. In some cases the measurements can be complicated data structures. Examples of measurements include blood pressure, temperature, and pulse. It's important to know the time of a measurement.
Scenario: At 3:00 PM on July 4, in St. Yak's hospital, Dr. Gump measures patient Smith's blood pressure (120/80) and temperature (99).
Problem 1.1.7: Transaction Processing
Domain: A transaction processor creates and commits transactions. Each transaction represents the action of withdrawing funds from on account and depositing them in another. Besides a balance and a password, each account is associated with an owner. An owner has a name and a PIN number.
Scenario: Bill Smith transfers $50 from his savings account into his checking account.
Problem 1.2: Analysis Patterns and The Actor-Role Pattern
An analysis pattern is a reusable domain model. At first it may seem surprising that a domain model could be reused, but there are several catalogs filled with such models. (See [COAD] or [FOW-2] for examples.) Analysis patterns are the analysis phase analogs of the design patterns we will study in Chapter 2.
A simple example of an analysis pattern is the Actor-Role pattern:
1-45 Pattern Oriented Programming with C++/Pearce
Actor-Role [COAD], [FOW-2] Other names: Actors are also called participants or parties. Confusingly, roles are sometimes called actors. Problem The same person may play many roles in an enterprise: customer, employee, supplier, etc. Sometimes these roles are played by organizations, not people. Associating personal data such as name, address, and phone number with an object representing a role can lead to unnecessary duplication and to synchronization problems. (How many objects must be updated when an employee who also happens to be a customer moves?) Solution Store personal data in an actor object. There are two subclasses of actors: persons and organizations. Store role-specific information (e.g., salary, rank, business volume, etc.) in a role object that maintains a link to the actor who plays the role. Using the Actor-Role patter, draw a UML class diagram that models the following domain:
A department store's object-oriented database keeps a record of each sale. Each sale record includes the time of the sale, a list of line items representing the items purchased, the clerk, and the customer. (A line item is an object consisting of a quantity and an object representing an item, for example, "4 sweaters".) Of course clerk and customer are simply roles played by actors. The customer may even be a corporate or governmental customer.
Draw an object diagram that instantiates the previous class diagram and that models the following scenario:
Bill Smith is a clerk at Sears. On December 24, he sells four sweaters and three shirts to Sue Jones.
Problem 1.3: Power Types
Normally, the type of an object is simply identified with its class. This is fine provided that type doesn't change or doesn't need to be known at runtime. Otherwise, we can represent the type of an object as an instance of a power type class.
For example, platoons, battalions, companies, and regiments are all examples of organizations. We could simply define these to be subclasses of Organization, or we could introduce them as instances of an OrgType power class. In this case each instance of the Organization class is created by an instance of the OrgType class using a factory method that provides the organization with a pointer to its creator:
1-46 1. Object-Oriented Modeling
class OrgType { string name; // e.g., "Battalion", "Department", "Corporation" public: Organization* makeOrganization() // factory method { return new Organization(this); } // etc. };
Every organization retains a pointer to its type. This allows clients to query an organization object at runtime about its type:
class Organization { friend class OrgType; OrgType* type; Organization(OrgType* t = 0) { type = t; } // private constructor! public: OrgType* getType() { return type; } // etc. };
Note that the private constructor makes it difficult for users to create mistyped organizations. A fuller treatment of runtime type identification (RTTI) will be given in Chapter 5.
Building a flexible model of an organizational structure is tricky. The following model represents facts about the parent-subsidiary relationship as self-associations on the Organization class, and rules about the parent-subsidiary relationship as self-associations on the OrgType class:
subsidiary *
<
1 type <
*
Organization parent
1
subsidiary *
1-47 Pattern Oriented Programming with C++/Pearce
Draw an object diagram that instantiates the class diagram above and that models the following scenario:
Departments are subsidiaries of colleges, and colleges are subsidiaries of universities. In particular, at Tech University the Mathematics department and the Phisics deparement are subsidiaries of the College of Science.
Translate the class diagram into C++ class declarations and write a function that creates the scenario just described.
Problem 1.4: Association Classes
Normally, links are represented by pointers in C++. But what happens when the links themselves have attributes? Where should this information be stored? For example, suppose we want to add type information to links. More specifically, suppose we want to record that the link between a parent organization and a subsidiary organization is the type of link that exists between colleges and departments, regiments and battalions, or corporations and divisions. Where would we store this information in the organizational model developed in the previous problem?
The solution is to represent links between organizations as objects rather than pointers. Such objects are instances of association classes, which are represented in UML class diagrams as class icons connected to associations by dashed lines. These techniques are used in the Organization-Affiliation analysis pattern:
1-48 1. Object-Oriented Modeling
Organization-Affiliation [FOW-2] Problem Organizational structures vary widely from one enterprise to the next. Organizational structures within an enterprise are subject to change. Solution Represent organization types as instances of an OrgType power type. Represent affiliations as instances of an Affiliation association class. Represent organizational rules as instances of an AfffilType class that is both, an association class and a power type:
subsidiary <
1 type <
*
Organization parent * 1 Affiliation subsidiary *
In our organization model parent-subsidiary relationships, for example the fact that the Math department is a subsidiary of the college of Science, are represented by instances of the Affiliation class. Relationships between parent and subsidiary organization types, for example, the rule that departments are subsidiaries of colleges, are represented by instances of the AffilType class. To close the loop, AffilType is a power type for Affiliation.
In the C++ translation, each organization maintains pointers to parent and subsidiary affiliations rather than pointers to parent and subsidiary organizations:
class Organization { list
An affiliation maintains pointers to the two organizations it links as well as a type pointer:
1-49 Pattern Oriented Programming with C++/Pearce
class Affiliation { Organization *parent, *subsidiary; AffilType* type; // etc. };
Repeat the previous problem using association classes.
Problem 1.5: The UML Meta Model
While instances of a power type represent subclasses, instances of a meta class represent classes in general. In fact, meta classes can be used to represent any UML element.
Draw a UML class diagram that represents the important concepts and relationships of UML based on the following summary:
Two classes may be related by a dependency. There are two types of dependencies: generalization and association. Each endpoint of an association can have a name, multiplicity, and a navigation arrow. Composition and aggregation are two special types of associations.
Your diagram might be useful in a specification for a CASE tool.
Problem 1.6: Recursive Containers
A container contains components. Components of a recursive container may themselves by containers. In each of the following problems, draw a class diagram representing the domain. Translate your diagram into C++ class declarations. Prove your declarations work by writing a simple test harness.
Problem 1.6.1
A folder may contains files. These files may be documents, applications, or other folders.
Problem 1.6.2
A tree has two types of nodes: parents and leafs. A parent node has one or more nodes below it (called the child nodes). A child node may be a leaf or a parent. A leaf node has no children.
1-50 1. Object-Oriented Modeling
Problem 1.6.3
A simple programming language has three types of expressions: literals, symbols, and operations:
EXPRESSION ::= LITERAL | SYMBOL | OPERATION
There are two types of operations: infix and prefix:
OPEARATION ::= INFIX | PREFIX
An infix operation consists of two expressions separated by an operator symbol:
INFIX ::= EXPRESSION OPERATOR EXPRESSION
For example: 42 + x. A prefix expression consists of an operator followed by an expression:
PREFIX ::= OPERATOR EXPRESSION
Problem 1.7: Reverse Engineering
Assume the following C++ class declarations have been made:
class A { public: virtual void f() = 0; ... }; class B: public A { A* a; ... }; class C: public A { A* a; ... }; class D { list as; ... }; // list<> is an STL container class E: public B, public C { D* d; ... };
Draw a class diagram showing the relationship between A, B, C, D, and E.
Problem 1.8: The Proxy Pattern
A proxy is an object that implements the same interface as a server. A proxy performs some extra service such as security check, caching recent results, or maintaining usage statistics, then delegates the clients request to another object that implements the server's interface. This might be the server itself, or another proxy. (This is the Proxy design pattern, which is discussed in Chapter 7)
1-51 Pattern Oriented Programming with C++/Pearce
Problem 1.8.1
A foreign diplomat sends a message to an agent. Either the agent is a diplomat or a translator who translates the message from one language to another, then forwards the translated message to another agent.
Draw a class diagram showing the relationships between diplomats, agents, and translators.
Problem 1.8.2
An Indian diplomat sends a message in Hindi to an agent who translates the message to German and sends it to another agent who translates the message to Arabic. This agent sends the message to another agent who translates it to Spanish, then sends the message to a Mexican diplomat. The Mexican diplomat reads the message, then sends a reply back through the same chain of translators.
Draw a UML sequence diagram showing the sequence of events.
Problem 1.8.3
An application running on host A sends a message to an application running on Host B. The message is first sent from the application layer on Host A to the Transport layer, where the message is broken into packets. The transport layer sends the packets to the network layer, which determines the route the packets will take. The network layer sends the packets to the data link layer, which breaks the packets into frames to be sent to the first hop on the route selected by the network layer. The data link layer sends the frames to the physical layer, which actually sends the frames to the next hop. Assume the message from A to B will be routed through Host C.
Draw a UML sequence diagram showing the events that will occur.
1-52