Heading 1 - SJSU



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 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:

[pic]

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:

[pic]

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:

[pic]

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:

[pic]

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:

[pic]

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

#include

using namespace std;

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:

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.

/*

* 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 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.

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(p))->dropBombs();

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(p))->dropBombs(); // ???

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(p))

   mp->dropBombs();

else

   cout 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);

   }

}

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, list, set, and multiset container templates as well as iterators for accessing their stored elements:[4]

class Fleet

{

public:

   typedef set::iterator iterator;

   void add(Airplane* a) { members.insert(a); }

   void rem(Airplane* a) { members.erase(a); }

   iterator begin() { return members.begin(); }

   iterator end() { return members.end(); }

private:

   set members;

};

Alternatively, we can make Fleet a subclass that privately inherits the features of its set super-class:

class Fleet: set

{

public:

   void add(Airplane* a) { insert(a); }

   void rem(Airplane* a) { erase(a); }

   iterator begin() { return set::begin(); }

   iterator end() { return set::end(); }

};

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:

class Fleet: set

{

public:

   void add(Airplane* a) { if (test(a)) insert(a); }

   // etc.

private:

   bool test(Airplane* a);

};

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 base class:

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:

[pic]

Our idealized code generator generates adds to Date member variables to the Airplane class:

class Airplane

{

   Date inspected, flown;

   // etc.

};

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.

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:

[pic]

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.

[pic]

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:

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:

[pic]

Commonly used UML class stereotypes include:

= Instances represent subclasses of another class

= Instances represent other classes

= Instances own their own thread of control

= Instances can be saved to secondary memory

= Instance represent system control objects

= Instance represent system interface objects

= Instances represent application domain entities

= Instances represent external systems or users

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:

[pic]

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:

[pic]

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:

[pic]

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 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:

[pic]

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.

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:

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:

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:

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:

[pic]

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:

[pic]

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.

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;

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:

[pic]

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:

[pic]

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, 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:

[pic]

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 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.

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;

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:

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:

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:

[pic]

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:

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:

[pic]

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 subsidiaries;

   Affiliation* parent;

   // etc.

};

An affiliation maintains pointers to the two organizations it links as well as a type pointer:

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.

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)

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] Throughout the text we will use ellipsis "..." to indicate unseen code.

[2] Dynamic casting is discussed in chapter 5 and in Appendix 1.

[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.

[4] The standard library container templates are discussed in Appendix 1.

[5] C++ namespaces are discussed in Appendix 1.

[6] We are using a simplified version of extended Bachus-Naur form (EBNF) to describe syntax rules. (See [PEA] for my EBNF conventions.)

................
................

In order to avoid copyright disputes, this page is only a partial summary.

Google Online Preview   Download