Sunteți pe pagina 1din 27

Abstract Classes and Pure Virtual Functions

In this article we will be discussing abstract classes, and how they are implemented in C++. The first part of the article will define abstract classes and describe how to use them as tools for Object Oriented Design. The second half of the article will concern itself with their implementation in C++, and some of the quirks and pitfalls that surround their use. While it is true that all objects are represented by a class, the converse is not true. All classes do not necessarily represent objects. It is possible, and often desirable, for a class to be insufficient to completely represent an object. Such classes are called abstract classes. The illustrations used in this article conform to the Booch Notation for Object Oriented Design. This is a rich and expressive notation which is very useful for presenting OOD concepts. Where necessary I will digress to explain some of this notation. What is an Abstract Class? Simply stated, an abstract class is a class which does not fully represent an object. Instead, it represents a broad range of different classes of objects. However, this representation extends only to the features that those classes of objects have in common. Thus, an abstract class provides only a partial description of its objects. Because abstract classes do not fully represent an object, they cannot be instantiated. At first it may seem a little odd that a class is incapable of having any instances. But everyday life is full of such classes. We may describe a particular animal as belonging to the class of all Mammals. However, we will never see an instance of the class Mammal! At least not a pure instance. Every animal belonging to the class Mammal must also belong to a class which is subordinate to Mammal such as Mouse, Dog, Human, or Platypus. This is because the class Mammal does not fully represent any animal. In Object Oriented Design, we will never see an instance of an abstract class, unless it is also an instance of a subordinate class. This is because the abstract class does not fully represent any object. As Booch says: An abstract class is written with the expectation that its subclasses will add to its structure and behavior...1

can support 0 instances, its is abstract. According to Rumbaugh: Abstract classes organize features common to several classes.2 What are the common features mentioned by Rumbaugh? They are features of the classs interface. For example, consider the classes Window and Door. At first glance these classes may not seem to have anything to do with each other. But both share some interesting common features. They are both holes in a wall. They both have particular locations and sizes with respect to the wall. They both may exists in one of three states: {OPEN, CLOSED, LOCKED}. Finally, they can be sent similar messages: {open, close, lock}. Thus, we can describe both classes as inheriting from a common abstract base, Portal, which contains all the common features of their interface.
Portal
0

Window

Door

STEREOTYPES AND POLYMORPHISM. Notice how this allows Window and Door objects to be stereotyped. They can both be referred to as Portals. While this is an incomplete description of either a Door or a Window, it is nonetheless accurate and useful. This ability to stereotype an object is a powerful design tool. It allows us to bundle all the common aspects of a set of objects together into an abstract class. As Lippman says: [An abstract class] provides a common public interface for the entire class hierarchy. 3 This common interface allows us to treat all such objects according to the stereotype. While such treatment may not be socially acceptable when dealing with humans, it provides for great efficiencies when dealing with software objects. For example, consider the Door and Window classes when they do not inherit from a common base:
class Window { public: enum WindowState {open, closed, locked}; void Open(); void Close(); void Lock(); void GetState() const; int GetXPos() const; int GetYPos() const; int GetHeight() const;

Abstract Class
0

The icon above represents a class. Note the 0 in the lower left corner. This represents the cardinality of the class, or the number of instances that the class can support. Since this class

int GetWidth() const; private: WindowState itsState; int itsXPos, itsYPos, itsWidth, itsHeight; }; class Door { public: enum DoorState {open, closed, locked}; void Open(); void Close(); void Lock(); void GetState() const; int GetXPos() const; int GetYPos() const; int GetHeight() const; int GetWidth() const; private: DoorState itsState; int itsXPos, itsYPos, itsWidth, itsHeight; };

public: virtual void open(); virtual void close(); virtual void lock();

}; The efficiencies that we gained are obvious. Several of the


Portal member functions can be reused. The declarations for

Door and Window are terse and understandable in terms of the functionality of Portal . Moreover, both Window and Door can be stereotyped as a Portal when convenient. For example:
void TornadoWarning(List<Portal*> thePortals) { ListIterator<Portal*> i(thePortals); for (; !i.Done(); i++) { (*i)->Open(); } }

These two classes are horribly redundant. They cry for some form of unification. That unification is supplied by creating the abstract base class.
class Portal { public: enum PortalState {open, closed, locked}; virtual void Open() = 0; virtual void Close() = 0; virtual void Lock() = 0; void GetState() const; int GetXPos() const; int GetYPos() const; int GetHeight() const; int GetWidth() const; private: PortalState itsState; int itsXPos, itsYPos, itsWidth, itsHeight; }; class Window : public Portal { public: virtual void open(); virtual void close(); virtual void lock();

Forgive the obvious license with the semantics of the Doors and Windows is superior to handling each type separately. As the application matures, more types of Portals will probably be added. However, since the TornadoWarning function deals with all species of Portals, it will not have to change. Thus, the polymorphic behavior of the abstract class Portal provides a more maintainable and robust design. It should be stressed that forcing Door and Window to inherit from Portal is not necessary to the proper functioning of the application. The application could be designed without the abstract class. However, creating the abstract Portal class results in a superior design which promotes polymorphism, and code reuse.
ListIterator. Clearly this method for opening all

Designing Applications with Abstract Classes During the first stages of a design, we usually have a good idea of the concrete classes that we need. As the design is refined, we should begin to find common features amongst some of these classes. These common features are not always obvious. They may come from different parts of the design, and may have different names and configurations. It sometimes takes a careful eye to spot them. For example, Jim, Bill and Bob are working on the design of the software that will control a car crushing machine. Jim is responsible for the control panel, Bill for the hydraulics control and Bob for the servo motors.

};
class Door : public Portal {

Jim has designed an Indicator class for the control panel. It looks like this:
class Indicator { public: enum IndicatorState {on,off}; Indicator(const IndicatorAddress&) void TurnOn(); void TurnOff(); IndicatorState GetState() const; }; };

virtual void Activate() = 0; virtual void Deactivate() = 0; ActuatorState GetState() const;

class Indicator : public Actuator { Indicator(const IndicatorAddress&); virtual void Activate(); virtual void Deactivate(); }; class Valve : public Actuator { Valve(const ValveAddress&); virtual void Activate(); virtual void Deactivate(); double GetFlowRate() const; }; class Motor : public Actuator { public: Motor(const MotorAddress&); virtual void Activate(); virtual void Deactivate(); void SetSpeed(int); int GetSpeed() const; };

Bill has designed a Valve class for controlling the hydraulics. It looks like this:
class Valve { public: enum ValveState {open, closed}; Valve(const ValveAddress&) void Open(); void Close(); int IsValveOpen() const; double GetFlowRate() const; };

Bob has designed a Motor class for controlling the servos. It has the following interface:
class Motor { public: enum MotorState {running, stopped}; Motor(const MotorAddress&) void SetState(const MotorState); MotorState GetState() const; void SetSpeed(int); int GetSpeed() const; };

In a more complex application, commonality can be much harder to detect. The names and forms of the methods can disguise the intrinsic similarities. Thus, care should be exercised in the search for commonality. The effort spent in the search will be paid back with a more maintainable design which supports a higher degree of code reuse. FACTORING. In the Actuator example, we found the common features of the concrete classes, and promoted them to the abstract base. Rebecca Wirfs-Brock, et. al. call this factoring 4 They go on to state a profound principles of Object Oriented Design: Factor common responsibilities as high as possible. The higher, in the inheritance hierarchy, common features can be factored, the more chances for re-use and polymorphism are engendered. Have we factored the features in the example high enough? Our example didnt have very many features, but there is still room for some more factoring. It seems unlikely that the Motor class will be the only class requiring a speed setting. So we might want to create an abstract subclass of Actuator with methods for handling speed. However, speed is just a variable quantity. We can generalize it by creating the abstract class

In this simple example, the common features arent too hard to find. Each of these classes has a binary state, methods to alter that state, and methods to interrogate that state. The forms and names of these methods are dissimilar, but their functions are common. Thus we can create an abstract base class Actuator which defines the common points of each:
class Actuator { public: enum ActuatorState {on, off}; Actuator();

VariableActuator:

Actuator
0

Actuator
0

Indicator

Valve

Variable Actuator
0

Indicator

Valve
0

Variable Actuator
0

Motor
This provides us with the opportunity to derive other variable controlled actuators from the common base. For example, we could create a variable brightness lamp, or a variable speed fan. EVOLUTION OF CONCRETE CLASSES. As the iterative process of design continues to add more and more detail to the application model, classes which had been concrete will tend to become abstract. As we incorporate more of the details of the application into the design, classes which had been specific become the generalities for the new details. For example, as the car crusher design proceeds, Bill discovers that he needs a safety valve which opens automatically when a pressure limit is exceeded. Bill could simply derive SafetyValve from the current Valve class. However, this assumes that the the SafetyValve is going to share all the features of the simple valve. This may not be the case. Bill might be better off creating an abstract class to represent all valves, and then deriving SimpleValve and SafetyValve from that common base.

Simple Valve

Safety Valve

Motor

By taking this tack, Bill has created a generalization which fits not only his existing concrete valve classes, but any new valve class that may be needed in the future. This process of factoring common elements, higher and higher into the inheritance hierarchy as the design progresses, is typical. The more details that are added to the design, the more abstract classes are created to deal with the common generalities. PURE INTERFACES. Note that abstract classes define features which are common to the interfaces of their derived classes. But what implementation should be provided by the abstract class? For example, what is the implementation for Actuator::Activate(). There is no sensible implementation! It is only the derived classes that know how to deal with the Activate method. Indicator::Activate() turns on a real indicator lamp. SimpleValve::Activate() causes a real life valve to open. Motor:Activate() turns on a real motor. But Actuator::Activate() cant do anything because it doesnt have anything real to interface to. Thus, Actuator:: Activate() is a pure interface devoid of any implementation. It is the pure interfaces within an abstract class which define the common features encapsulated within it. They are the basis for the polymorphic behavior of abstract classes. The impure interfaces, those interfaces which have implementations, contain the code which is re-used by all the derived classes. Pure Virtual Functions In C++, pure interfaces are created by declaring pure virtual functions. A pure virtual function is declared as follows:
virtual void Activate() = 0;

The = 0 on the end of the declaration is supposed to represent the fact that the function has no implementation.

However, as we will see, implementations are sometimes provided. In C++, a class with a pure virtual function is an abstract class. The language provides special semantics for abstract classes. It enforces the constraint that abstract classes cannot have any instances. Thus the compiler will not allow an instance of an abstract class to be created. If you attempt to declare or create one, the compiler will complain:
Actuator myActuator; // error Actuator* myActuator = new Actuator; // error;

virtual void Activate() = 0; virtual void Deactivate() = 0; };

This restriction was relaxed in the 2.1 version of the compiler, so pure virtual function can be inherited without specifically being declared. If they are not declared in the derived class, they are inherited in pure form, making the derived class abstract.
class Valve : public Actuator { // This class is abstract. // It inherits the pure virtual functions // Activate() and Deactivate() };

This constraint does not, however, prevent you from declaring pointers and references to abstract classes. The compiler allows such constructs, and will allow them to refer to instances of concrete classes which have the abstract class somewhere in their inheritance hierarchy.
Actuator* myActuator = new Indicator; Actuator& anActuator = *(new Indicator);

Such pointers and references are very useful for taking advantage of the polymorphic attributes of abstract classes. The pure virtual functions of the abstract class are bound to the implementations defined in the instances to which they refer.
myActuator->Activate(); // Turn on indicator. anActuator.Deactivate();// Turn off indicator.

Including a pure virtual function in a class is the only way to tell the compiler that the class is abstract. This is sometimes considered to be a limitation. It has been suggested that an abstract keyword be added to the language so that classes could explicitly be declared abstract.
abstract class Actuator; // suggested syntax.

INSTANCES OF ABSTRACT CLASSES. Although it is impossible to explicitly create an instance of an abstract class, such instances can temporarily exist during construction or destruction. Instances under construction are only as complete as the currently executing constructor has made them. Instances under destruction are only as complete as the executing destructor has left them. Because these instances are incomplete, the language prevents the virtual mechanism from calling any virtual functions in classes which either have not been constructed yet, or have been destructed already. What might happen if a pure virtual function of such an incomplete object were called? The language specification (Arm 10.3) says that calling a pure virtual function from a constructor of destructor is undefined. This means that if f() is defined as pure virtual in class C, then calling f() from a constructor or destructor of C is an error. For exmaple, let us assume that the designer of Actuator wanted to initialize the instance in the deactivated state. To do this he called Deactivate() in the constructor:
Actuator::Actuator() { Deactivate(); }

However, in my opinion, this would create a redundancy in the language. A truly abstract class must contain a pure interface; otherwise, it would be instantiable. INHERITING PURE VIRTUAL FUNCTIONS. Pure virtual functions behave differently depending upon the version of the compiler you are using. Cfront 2.0 did not allow pure virtual functions to be inherited by derived classes. If the base class had a pure virtual function, then that function had to be declared in the derived classes, either in pure form or in impure form. Thus:
class Valve : public Actuator { public:

But Deactivate() is defined as pure virtual in Actuator. So this is an error. In fact, it is an error that compilers can easilty detect, so most will issue some kind of error message. However, it is unreasonable to expect a compiler to detect all the myriad ways in which pure virtual functions can be invoked from constructors or destructors. The error can be hidden by a slight, and very reasonable modification to the example above.
class Actuator

{ public: enum ActuatorState {on, off}; Actuator(); virtual void Activate() = 0; virtual void Deactivate() = 0; ActuatorState GetState() const; private: void Init(); }; Actuator::Actuator() { Init(); } void Actuator::Init() { Deactivate(); }

{ typedef MyBase superclass; public: int IsValid() const; /* ... */ }; int MyClass::IsValid() const { int retval = 0; if (superclass::IsValid()) { if (/* I am valid */) { retval = 1; } } return retval; }

Here, the designer has, quite reasonably, created a private Init() function which will be called by the constructor. Init calls Deactivate in order to initialize the device in the deactivated state. The same bug still exists. Deactivate() will still be called within the context of the Actuator constructor. But this time, the compiler will probably not complain. Thus, when an Indicator is created, the call to Deactivate will attempt to invoke a non-existent implementation, and will very likely crash.

Note the call to the super class. When IsValid is called for an instance, it checks to see if the portion of the instance which is described by its base class (superclass) is valid. If so, it checks its own parts and returns 1 if they are ok, and 0 if they are not. Each IsValid function in each of the base classes repeats this procedure. Thus, when IsValid is called for an instance, the call is passed all the way up the inheritance hierarchy. Now, lets presume that the base most class of the hierarchy is as follows:
class Validatable { protected: virtual int IsValid() = 0; };

PURE VIRTUAL IMPLEMENTATIONS Sometimes it is convenient for pure virtual functions to have implementations. C++ allows this. The implementation is coded in exactly the same way as the implementation of any other member function:
void Actuator::Activate() { /* Do something clever here */ }

There are not very many good reasons to supply implementations for pure virtual function. After all, a pure virtual function represents a pure interface whose implementation would make no sense in the context of the abstract class. But sometimes there are exceptions. For example, assume we have a virtual function IsValid which checks the instance to see if it is in a valid state. The general form of such a function will be:
class MyClass : public MyBase

In other words, the abstract base class describes the set of all classes which have IsValid functions. Can the immediate derivatives of Validatable obey the IsValid protocol by passing the call up to their superclass (i.e. Validatable)? This would be very desirable since we dont want to have one convention for immediate derivatives of Validatable, and another for its indirect derivatives. If at all possible, we want all the derivatives of Validatable to use the same form for IsValid(). Fotunately, the ARM(10.3) allows implementations of pure virtual function to be called explicitly by using their qualified name.
if (Validatable::IsValid()) ...

Thus, if we supply the following implementation for the

pure virtual function Validatable::IsValid(), then the immediate derivatives of Validatable can obey the superclass protocol.
int Validatable::IsValid() { return 1; }

};

This declaration shows the interface of a class which will activate an actuator for a specified period of time, and then turn it off again. Any derivative of Actuator can be used as an argument to the ActuatorTimer constructor. The specified Actuator will be polymorphically activated and deactivated by the ActuatorTimer class. Summary Abstract classes provide a powerful design technique which promotes code re-use and polymorphism. According to Coplien: The power of the object paradigm in supporting reuse lies in abstract base classes. 5 Abstract classes are produced by factoring out the common features of the concrete classes of the application. Although such factorings are sometimes hard to find, the effort put into finding them is usually well worth the benefits of the extra maintainability and reusability. In general, common features should be factored out and moved as high as possible in the inheritance structure. In C++, pure virtual function are used to specify the pure interfaces of abstract classes. An abstract class in C++ must have at least one pure virtual function. Although pure virtual functions typically have no implementation, C++ allows implementations to be given to them. The user must take care not to invoke pure virtual functions in the constructors or destructors of an abstract class. Although abstract classes cannot be instantiated, they can be used in every other way to represent normal objects. They can be contained or passed by reference, and their interface can be used to invoke their intrinsically polymorphic behavior. References

PURE VIRTUAL DESTRUCTORS. There is one aspect of pure virtual functions which the language specification does not define very well. This is the behavior of pure virtual destructors. Pure virtual destructors are an oddity. They are, I suspect, an accident of syntax rather than a designed feature. They look like this:
class OddClass { public: virtual ~OddClass()=0; // valid but strange.

}; A destructor is not a normal function, it cannot be inherited (ARM 12.4), it cannot be overridden and it cannot be hidden. A virtual destructor is not a normal virtual function. It does not share the same name as the base class destructor, and it is not inherited. A pure virtual function is meant to be inherited, its is the interface for a feature which is to be defined in a derived class. So what is a pure virtual destructor? It is not an interface for a destructor which is to be defined by a derived class, because destructors cannot be inherited. It is not a function without an implementation, becauseit will be called as the destructor for the class, and so it must be implemented. The only guaranteed feature of a pure virtual destructor is that it makes the class that contains it abstract. Using Abstract Classes Although programs are not allowed to instantiate abstract classes, in every other way they can be used to manipulate normal objects. Classes may contain pointers or references to abstract classes.
class ActuatorTimer { public: ActuatorTimer(Actuator&); void SetTimer(int); int GetTimer() const; void Start(); private: Actuator& itsActuator;

Booch, Object Oriented Design with Applications , Benjamin/Cummings, 1991 2 Rumbaugh, et. al. Object-Oriented Modeling and Design, Prentice-Hall,1991 3 Lippman, C++ Primer , second edition, AddisonWesley, 1991 4 Wirfs-Brock, et. al. Designing Object-Oriented Software, Prentice-Hall, 1990 5 Coplien, Advanced C++ Programming Styles and Idioms, Addison-Wesley, 1992

Collaboration
"The objects within a program must collaborate; otherwise, the program would consist of only one big object that does everything."

-- Rebecca Wirfs-Brock, et. al., Designing Object-Oriented Software, Prentice Hall, 1990
INTRODUCTION Collaboration, to my mind, is not discussed enough. It is one of the essential elements of object-oriented analysis and design. As Booch says: "Equally important [as inheritance] is the invention of societies of objects that responsibly collaborate with one another. ... These societies form what I call the mechanisms of a system, and thus represent strategic architectural decisions because they transcend individual classes." [The C++ Journal, Vol. 2, NO. 1 1992, "Interview with Grady Booch"] In this article we will talk about what collaboarations are and why they are so important. We will discuss how collaborations are unearthed through analysis of the problem domain, and how they are designed into the application. We will also discuss the C++ "friend" mechanism, and how it aids the design of collaborations. Some of the examples in this article use a variation of the Booch Notation for describing analysis and design decisions. Where necessary I will digress to explain the notation. WHAT IS COLLABORATION? A collaboration occurs every time two or more objects interact. A collaboration can be as simple as one object sending one message to another object. Or it can be a as complex as dozens of objects exchanging messages. In fact, an entire application is really a single gigantic collaboration involving all of the objects within it. An object-oriented application can be broken down into a set of many different behaviors. Each such behavior is implemented by a distinct collaboration between the objects of the appliation. Every collaboration, no matter how small or large, always implements a behavior of the application that contains it. Imagine an object-oriented application as a network of objects connected by relationships. Collaborations are the patterns of messages that play through that network in pursuit of a particular behavior. A collaboration can be viewed as an algorithm which spans this network, using many different objects and methods. The algorithm is distributed across the network of objects, and so does not exist in any one place. This is in distinct contrast to the behaviors of a class. All behaviors pertinent to a class are methods of that class. They exist in one place. But an object-oriented application is made up of many such classes. Its behaviors are a synthesis of the individual class behaviors. So the application's behaviors are distributed through the classes as collaborations. This identification with the behaviors of the application gives collaborations a very central role in the analysis and design of object-oriented programs. It is these behaviors, after all, that we are trying to achieve. If the collaborations which implement them are not properly designed, then the application will be inaccurate or brittle. IDENTIFYING COLLABORATIONS Collaborations are typically unearthed during the analysis of the problem domain. The first step in this process is to discover the primary classes and their relationships. These are arranged into a model of the static structure of the application. To test this structure, behavioral scenarios are examined. In each scenario we ask which objects will be present, and how they will respond

to one particular event. We then attempt to figure out which messages are sent between the objects in order to handle the event. It is within these scenarios that the first hints of collaboration are to be found. For example, consider an application to automate a public library. The analysis of such an application might yeild the following static model. This model is by no means complete, it simply shows a few of the classes in the problem domain.

Library

Employs

maintains

1..n

Librarian

LibraryCard List

0..n Identifies

Borrower

This diagram is called a class diagram. It is typical of those produced during object-oriented analysis. It is similar to an entity relationship diagram (ERD), except that it uses Booch symbols. It shows the classes in the model, and the static relationships between those classes. In this case we see that the Library employs some number of Librarians. It also maintains a list of all the library cards which identify the Borrowers that the Library is willing to loan books to. Lets examine the behavioral scenario related to borrowing a book from the library. A Borrower takes a book up to a Librarian and presents his or her library card with a request to check the book out. The librarian enters the book id and library card number into a terminal. This creates an event from which we can trace out the flow of messages through the system.
Borrower LibraryCard 3-GetBorrower :Library 2-GetBorrower LibraryCard Borrower :Librarian 1-CheckOut LibraryCard BookCopy 4-IsAllowedToCheckOut allowed :Borrower :BookCopy 5-SetLocation Borrower 6-SetReturnDate

:LibraryCard List

This diagram is called an object diagram. It shows the objects that we expect to participate in the behavior, and shows the messages and data that flow between those objects. Note that each message is numbered in the sequence that it occurs. We have shown the initial event as the CheckOut message which is sent to the Librarian object (message #1). The message includes the BookCopy, which is an object

which represents a particular copy of a book. The message also contains the LibraryCard of the Borrower. The Librarian asks the Library to look up the Borrower from the LibraryCard (#2), The Library in turn asks the LibraryCardList for the same information (#3). Once in possession of the Borrower, the Librarian checks its status (#4), to see if it is allowed to check out any books. In this example, the Borrower is allowed to check out books, so the Location of the book is set to the Borrower (#5), and the appropriate return date is set (#6). This behavioral scenario is a first step towards identifying the collaboration for checking a book out of the library. Its purpose, at this stage, is to prove that the static model is capable of supporting the behavior. But is also gives us a very good idea of the methods that the classes will need in order to properly collaborate. Every behavior of the application should be modeled in this way. From this work a set of behavioral scenarios is generated. Each of these is an early representation of the collaborations within the application. DESIGNING COLLABORATIONS Identification is not enough. By analyzing the problem domain we have compiled a list of proto-collaborations. Now we need to design the detailed structure of the application so that the collaboration can be supported. This involves replacing the weak relationships in the analysis model, with strong OOD relationships such as inheritance (IsA), containment (HasA) and usage relationships. This is done by inspecting the behavioral scenario to see how the messages flow. For example, the first message in the library collaboration comes to the Librarian from the outside. This implies some kind of LibrarianTerminal object which knows about the Librarian.
Librarian Terminal Librarian

The black ball and double line represents a containment (HasA) relationship. The class LibrarianTerminal contains a Librarian. This relationship means that the LibrarianTerminal has intrinsic knowledge of the Librarian. This is important if the LibrarianTerminal is to send a message to the Librarian. The second message in the collaboration is between the Librarian and the Library. Since none of the data currently flowing in the collaboration has identified a particular Library object, the Librarian must has intrinsic knowledge of the Library. Once again, this implies containment.
Librarian Terminal Librarian

Library

Note that this relationship seems to go the "wrong" direction when compared to the analysis model. In the analysis model the Library employed the Librarian. However, in this design, the Librarian contains the Library. Although the analysis model makes perfect sense by itself, it does not support the needed collaboration at the detailed level. Thus, the direction of the relationship must changed to support the collaboration. Message number 3 is sent from the Library to the LibraryCardList. Again, intrinsic knowledge is needed, again implying containment. Moreover, we know from the analysis model that the LibraryCardList identifies all the Borrowers. This too implies containment.

Librarian Terminal

Librarian

Borrower

Library

Library CardList

Message number 4 represents the Librarian interrogating the Borrower about its ability to borrow books. Intrinic knowledge is not implied since the Borrower was returned to the Librarian through message number 2 and 3. Thus we say that the Librarian uses the Borrower, but does not contain it. The using relationship, represented by the double line and white ball, implies that the used object is somehow made available to the user via the user's interface. By the same reasoning, messages 5 and 6 imply that the Librarian uses the class BookCopy, since it finds out about the BookCopy from the LibrarianTerminal in message #1.

BookCopy

Librarian Terminal

Librarian

Borrower

Library

Library CardList

This design of the classes within the library model now fully supports the check-out collaboration. Similar exercises need to occur for each of the collaborations unearthed through the analysis. Notice that the static model of the analysis was used in the creation of our collaboration, and that the collaboration was then used to refine the static model. This oscillation between the static and dynamic models is typical and essential. We only showed one small oscillation, but in a real analysis and design, the oscillations would continue many more times before the design was considered sufficiently refined. Each change to the static model sheds new light on the dynamics of the collaborations. Each refinement made to the collaborations may expose deficiencies in the static model. TYPES OF COLLABORATION We can classify the ways in which classes collaborate into 4 broad categories. Each of these categories has to do with the relationships between the collaborating classes. The differences between these 4 classifications has to do with the intimacy of the collaboration. Some collaborations take place strictly through their public interfaces, and are therefore not very intimate. Other collaborations require closer coupling between the participants. Peer- to-Peer collaborations All the collaborations that we have studied so far have been of the Peer-to-Peer variety. Peer-to-Peer collaborations occur when two unrelated classes exchange messages. This is the most common form of collaboration. Typically, peer-to-peer collaborations are not intimate; i.e. the collaborators do not depend upon special knowledge of each other. In C++, they are seldom declared as friends. This is not a hard and fast rule however. Sometimes intimacy is indicated. Containers and iterators are an example of peer-to-peer collaborators which are generally intimate and require friendship.

Sibling Collaborations A Sibling collaboration occurs when two or more classes, derived from a common base, exchange messages. Often such collaborations are more intimate than the Peer-to-Peer variety, since the objects know more about each other. For example:
A

BookCursor

LibraryCursor

AisleCursor

ShelfCursor

Here we see three classes derived from the same base class; they are siblings. The BookCursor base class is abstract, which is signified by the triangular icon. BookCursor represents the set of classes which search the library for books. The three siblings represent different scopes in which such searches can occur. You can search an entire shelf with ShelfCursor, an entire aisle with AisleCursor and the whole library with LibraryCursor. Notice that the siblings make use of each other in a directional manner. The LibraryCursor uses the AisleCursor which in-turn uses the ShelfCursor. This makes perfect sense, since searching the library is a matter of searching all the aisles, and searching an aisle is a matter of searching all the shelves within the aisle. This kind of hierarchical relationship is typical of sibiling collaborations. Each sibling builds on the facilities of the other. However, siblings are often able to deal with peer clients as well. When dealing with peers, the relationship is usually not as intimate as when dealing with a sibling, so the two may use different interfaces, one more intimate than the other. For example:
1-Search BookCursor 3-PrivateSearch BookCursor 4-PrivateSearch :ShelfCursor (x) :LibraryCursor

:AisleCursor

2-Initialize (x) :BookCursor

Here we see a client sending the Search message to object (x):LibraryCursor. The name of the object is 'x', but the parenthesis indicate that the name is local to this diagram, and not known to the rest of the design. It's kind of like a local variable. Object 'x' responds by sending itself the Initialize method, which is handled by the BookCursor base class. This method clears a set of counters in the BookCursor which keep track of statistics concerning the search. Since each of the siblings must be able to deal directly with clients, they must each respond to the Search method by initializing the base class with the Initialize method. However, when we are searching the entire library, we want all the statistics gathered in the base class of the LibraryCursor object, rather than spread out through a bunch of AisleCursor and ShelfCursor objects. So the LibraryCursor object 'x' tells the AisleCursor to use the statistics counters in the base class of 'x'. Moreover, the AisleCursor passes this information along to the ShelfCursor as well. This information is passed using the PrivateSearch method, which is designed for intimate use between siblings, rather than general purpose client access. Since the classes have a method that they wish to keep private amongst themselves, they should declare the method to be restricted to private access. In order for the siblings to access the methods, they must be friends of each other. Thus we modify the class diagram to show the

friendship.
A

BookCursor

LibraryCursor

AisleCursor

ShelfCursor

Base-Derived collaborations We saw a small example of a Base-Derived collaboration in the previous example. Such collaborations occur when a derived class exchanges messages with its base. Such collaborations are often very intimate; base and derived classes know a lot about each other and can take advantage of that knowledge. Such collaborations typically involve short term violations of class invariants, i.e. they temporarily leave the class in an illegal state between messages. But these invariants are always restored prior to the end of the collaboration.
(x) :BookCursor 2-InitializeDerived 1-Initialize (x) :LibraryCursor

Here we see an elaboration of part of the previous example. The LibraryCursor object initializes itself by sending itself the Initialize message. The BookCursor base class handles this message and sends the InitializeDerived message back to the derived class (probably via virtual deployment). Thus, the base portion of the class is initialized first, and then the base class initializes the derived class. In between these two messages, the object is in an invalid state, being only partially initialized. Certainly the InitializeDerived method should be private and virtual. Auto-Collaboration Auto-collaboration occurs when an object sends a message to itself. This is the most intimate of all collaborations, since the object is generally talking to itself. Such collaboration is typically used to encapsulate portions of the implementation. For example, task x may be a component of many of the methods of class Y. Rather than coding task x in each of these methods, it makes better sense to create a new method which performs task x. Certainly such a method should be kept private, since its function is never meant to appear in isolation from the other methods of which task x is a component.
1-Search

(x) :LibraryCursor

(x) :LibraryCursor 2-PrivateSearch BookCursor

Here we see a typical case of auto-collaboration. When a LibraryCursor object is sent the Search method, it invokes the PrivateSearch method. The data item sent along is presumably its own base class. Notice how this encapsulates the task of searching within the PrivateSearch method. No other method of this class knows the details of a search.

USING FRIENDSHIP IN COLLABORATION In one of the examples above, we used friendship to aid the collaboration of siblings. Friendship is also sometimes used in peer-to-peer collaborations. In early versions of C++, before the protected keyword was added, friendship was also used to support base-derived collaborations. In fact, the proliferation of base classes declaring their derivatives as friends was a principle factor in the decision to add protected access to the language. Friendship allows unrelated classes to participate in intimate collaborations. This is important when several classes are working together to present a single abstraction. As a case in point, take the example of the LibraryCursor. This class collaborated with its sibling AisleCursor to present a single abstraction: that of searching the entire library for books. This collaboration required that the two classes be friends. Such multi-class abstractions are an important design technique. There are situations where it is not practical or possible to represent an abstraction as a single class. A good example of this is iterators. Container classes and their iterators represent a single abstraction. But there is simply no good way to represent this abstraction as a single class. Another role of friendship is to prevent private portions of a collaboration from leaking out into the public arena. Again, the LibraryCursor class provides us with an example. The PrivateSearch method is a dangerous method to make public. It badly violates the invariants of the BookCursor abstraction. Friendship allows these dangerous functions to remain private to the abstraction, and to be used by the friends participating in that abstraction. When many classes collaborate, the use of friendship to solve the problems of access and efficiency will result in classes that are bound tightly to each other. Sometimes they can be so tightly bound that they cannot be separated from each other. Certainly we want to avoid, at all costs, huge networks of classes which are all friends and which all take great liberties with each others internal parts. Such a perversion could not be called object-oriented. Also, we want to avoid the temptation to use friendship to join two very separate abstractions. If such abstractions need to be joined in some way, the joining should generally be accomplished through their interfaces, or through an intermediary class. However, when two ore more classes are truly part of the same abstraction, then tight binding and friendship should not be discouraged. As Rumbaugh says: "Some object-oriented authors feel that every piece of information should be attached to a single class, and they argue that associations violate encapsulation of information into classes. We do not agree with this viewpoint. Some information inherently transcends a single class, and the failure to treat associations on an equal footing with classes can lead to programs containing hidden assumptions and dependencies." [Object Oriented Modeling and Design, Rumbaugh et. al., Prentice Hall, 1991] Since friendship can only be given, and cannot be taken, the choice of who to give friendship to becomes a design decision. This means that the class is designed to collaborate with certain special friends. The collaborators become members of a team which work more closely together than normal in order to achieve a single end. Thus, encapsulation is not lost, nor even compromised. The "capsule" simply widens to enclose all the friends. SUMMARY In this article we have examined collaboration. We have shown that all the behaviors of an application are implemented through collaborations. We have shown how collaborations are first detected in the analysis phase of a project, and how their static and dynamic elements can be expressed using the Booch notation. We have shown how the static and dynamic views can be iterated to provide successive refinement of the application's design. We have discussed the various types of collaborations, and typical situations when they may be used. Finally we have discussed the role of friendship in collaborations. Collaboration is at the heart of OOA/OOD. The proper design of an object-oriented application depends upon a thorough and detailed understanding of the collaborations which implement its behaviors.

OO(A,D,P(c++))
In my last two articles, I talked about abstract classes, pure virtual functions and collaboration. In this article I will use both these concepts in a case study which explores the topic of ObjectOriented Analysis and Design. Although the subjects of OOA and OOD are steeped in theory and current controversy, this article will bypass all that; and present them from a pragmatic, how-to point of view. Make no mistake, OOA and OOD are big topics. It is not feasible to cover them in any depth within the context of a single article. Therefore this article will present the basics of OOAD, and will not attempt a detailed account. However, the information in this article should be sufficient to give you an inkling or two about what OOA and OOD are all about. There seems to be a great deal of discussion about the merits of various object oriented languages. Some of the participants in these discussions favor C++ as an OOPL based on its nearness to C and its pragmatic outlook. Others berate C++ as an ugly and incomplete language because of its nearness to C and its pragmatic outlook. I would like to make a different point altogether: The quality of an object oriented application is determined far more by the quality of its analysis and design, than by the features of the implementation language. Certainly I believe that language features are important. Indeed, certain features such as inheritance and dynamic binding are essential to the implementation of an object-oriented design. But given that base set of features, and a reasonable amount of expressive ability, the success of an application depends on a correct analysis and design, not on the nuances of the language. In my opinion, C++ offers features in sufficient quantity to implement any object-oriented design. This, and its nearness to C and its pragmatic outlook, make is a natural choice for me. If the truly important part of creating an object-oriented application is its analysis and design, it might be a good idea if we spent some time learning what object-oriented analysis and design really are. There seems to be a great deal of confusion on this subject. For example, I hear the following questions being asked quite often: Are there any differences between OOA and OOD? If so, what are they? Should we do OOA first, followed by OOD? What is a reasonable output from OOA, OOD? Are there different notations for OOA and OOD?

Before we can understand Object-Oriented Analysis well enough to answer these questions, we should first try to understand what the concept behind Analysis is. Webster supplies us with several interesting definitions: separation or breaking up of a whole into its fundamental elements or component parts. or A detailed examination of anything complex. These definitions come close to the traditional meaning of Systems Analysis, but another of the definitions rounds the concept out: The practice of proving a mathematical proposition by assuming the result and reasoning back to the data or to already established principles. Analysis, is the decomposition of an application into its constituent parts. It is the exposure of the organization of the innards of a problem. This is accomplished by beginning with a set of stated requirements, and then reasoning backward from those requirements to a set of already established software components and structures. It is a way of re-expressing a human problem in a language more suited for describing a computer application. It is important to understand, in general terms, just what we must analyze. We analyze requirements. What are requirements? Regardless of their form, requirements boil down to a set of desired behaviors. Requirements are the behaviors that the completed application should express.

This is a central concept. Analysis is the restatement of a set of required behaviors in terms suitable for describing a computer application. This restatement is arrived at by considering the required behaviors and reasoning backwards to the components of those behaviors which can be expressed in a computer program. In the case of Object-Oriented Analysis the components of the required behaviors are objects and their collaborations. In Object-Oriented Analysis, we start from the required behaviors and we reason backwards to find the abstractions that underlie those behaviors. Then we attempt to define the objects that represent those abstractions, and the messages which those objects pass between each other in order to implement the required behaviors. We do this by studying use cases and their scenarios. We examine each requirement, one by one, and develop dynamic scenarios composed of objects and messages which address those requirements. Boochs object diagrams are ideal for this purpose. For example, consider a Payroll application with the following requirements: Some employees are paid once every two weeks, while others are paid once per month. Salesmen are paid a base rate plus a commission on receipts. Other exempt employees are paid a fixed salary per pay period. Hourly employees are paid an hourly rate plus any authorized overtime; based on the data in their timecards. Employees may choose whether they will be issued a paycheck, or have their pay directly deposited into their accounts.

Lets take the first point. It appears that employees are not all paid at the same time. Some get their pay every month while others get their pay every two weeks. What, as analysts, can we say about this situation? As Object-Oriented analysts we hunt for abstractions, for objects which express the underlying concept. It would be incredibly nive of us to presume that this requirement simply implied the following: AreYouPaidEveryTwoWeeks

Payroll System

Employee
YES | NO

Rather, we want to look for the motivation which underlies the requirement. What is it about this requirement that could change, and what will always be invariant. Clearly the invariant portion of the requirement is that all employees will be paid according to some schedule. But what could easily change is the nature of that schedule. It is not hard for us to imagine that new policies might allow for employees who are paid weekly, or even quarterly. Thus we can identify that the payment schedule for an employee is an abstraction. Although each employee has one, there may be many different kinds. Having created the abstraction of a payment schedule, we can create our first analysis object. We will name it: PaymentSchedule. This object has the following interface: IsThisYourPayday date YES | NO

???

Payment Schedule

This is certainly not the only adequate interface for the PaymentSchedule object; nor perhaps,

is it the best. But it states the abstraction succinctly enough for my tastes. How is an Employee object associated with the appropriate PaymentSchedule object? The simplest answer is that it contains it. We can express this in a class diagram as a class relationship; as follows.

Employee

Payment Schedule

How does PaymentSchedule work? There are any number of ways.; lets look at one of the worst ways first. It could be a simple object with an enum variable inside it: class PaymentSchedule { public: enum PaymentScheduleType {biweekly, monthly}; PaymentSchedule(const PaymentScheduleType& x) : itsScheduleType(x) {} int IsThisPayday(const Date&) const; private: PaymentScheduleType itsScheduleType; }; It is easy to see how this class works. It is constructed with a schedule type as in: PaymentSchedule schedule(PaymentSchedule::monthly); Then the object simply consults its private variable whenever it is asked whether any particular date is a payday. The method must determine if the date matches one of the enumerated schedule types. This works, but it is a horribly icky design. Ill point out why this design is so bad in just a minute, but did you notice that I said design? This entire discussion of how the PaymentSchedule object works is a design issue. The really rotten design that I just cooked up above is completely consistent with the analysis model. Moreover it meets the requirements flawlessly. Yet it is just one of many ways that the analysis model can be satisfied, and some of those other ways are much better. This demonstrates the point that a good analysis can be coupled with a bad design. It is not sufficient simply to recognize the abstractions in application. Effort must be applied to finding elegant designs for those abstractions. Notice how easy and natural it was to slide from analysis into design? This is typical, and should not be avoided. We constantly shuttle back and forth across the analysis-design border while we develop object-oriented software. This shuttling is a good thing and is to be encouraged. It solidifies our faith in the analysis when we can design a proper solution. Also, if it is difficult to complete a design from the analysis model, it indicates that something may be wrong with that model, and reanalysis is indicated.

So, what was wrong with the design of PaymentSchedule. It is too limited and inflexible. If a new payment schedule is required, I must modify the old PaymentSchedule class. This risks that, during my modification, I will break the code that already exists in that class. It also puts me in the unhappy situation of having some horrible switch or if/else monstrosity in the IsThisPayday method. A better way to design this class is as follows:
A

Employee

Payment Schedule

Monthly Payment Schedule

Biweekly Payment Schedule

Now we see that PaymentSchedule is an abstract class with at least two derivatives, one which implements the monthly payment schedule, and the other which pays employees every two weeks. Moreover, we have denoted that Employee contains its instance of PaymentSchedule by reference, in order that the containment can be polymorphic. The class declarations look like this: class Employee { public: private: PaymentSchedule* itsPaymentSchedule; }; class PaymentSchedule { public: virtual int IsThisPayday(const Date&) const = 0; }; class MonthlyPaymentSchedule : public PaymentSchedule { public: virtual int IsThisPayday(const Date&) const; }; class BiweeklyPaymentSchedule : public PaymentSchedule { public: virtual int IsThisPayday(const Date&) const; }; This design allows us to add new payment schedules as needed. These payment schedules can

be as complex or as simple as desired. No funny switch or if/else segments are implied, and the code in previously working derivatives of PaymentSchedule will not be affected. Also, this design hides the complexity of the concept of payment schedules. The differences between the payment schedules are ignored by all but the classes derived from PaymentSchedule. This is an important concept. Remember that we said that one of the goals of analysis is to expose the components of the required behaviors? In contrast, it is one of the goals of the design to hide the complexity of those behaviors from other parts of the design. So, analysis exposes while design hides. Switching back to analysis now, lets look at the next requirement: Salesmen are paid a base rate plus a commission on receipts.

This would seem to imply that there should be a Salesman object which contains a set of Receipt objects; as follows: 0..n

Salesman

Receipt

This certainly is in keeping with the requirements, but does this express the underlying motivation of the requiment? Once again, we can test this by asking what can change about the requirement, and what is invariant. This requirement is stating a method for calculating pay. In fact there are two other requirements which do exactly the same thing for different types of employees: Other exempt employees are paid a fixed salary per pay period. Hourly employees are paid an hourly rate plus any authorized overtime; based on the data in their timecards.

It seems clear that the underlying abstraction for these three requirements is: There are many different types of employee, and each has its own distinct way of calculating its pay. This leads us immediately to an interface for Employee objects which looks like this: GetPayment Amount

???

Employee

Now we are presented with several design possibilities. First, we could create many subclasses of Employee as follows:

Employee

Commissioned Employee

Salaried Employee

Hourly Employee

Alternatively, we could have the Employee object contain an object called: PaymentCalculator. like this:
A

Employee

Payment Calculator

Commission Calculator

Salary Calculator

HourlyPay Calculator

I prefer the first option becuase the calculator objects are going to need a lot of information from the Employee objects, and so the interface between them is going to be wide and complex. Moreover, the three different payment calculators are going to need three different types of documents to calculate pay from. The CommissionCalculator is going to need to see receipts. The HourlyPayCalculator is going to need to see time cards. The calculators can only get these documents from the Employee object itself. Thus there must be three different flavors of Employee just to hold the three different kinds of documents. So I prefer the first model, fleshed out as follows:

Employee

Commissioned Employee
0..n

Salaried Employee

Hourly Employee
0..n

Receipts

Timecards

Finally, we turn back to the analysis of the final requirement. Employees may choose whether they will be issued a paycheck, or have their pay directly deposited into their accounts.

Once again we want to find the central abstraction which motivates this requirement. And it seems clear that this abstraction is: Every employee will be able to choose some mechanisms for being paid. The requirement names two such mechanisms; specifically: paycheck or directdeposit. However it seems likely that other payment methods will evolve over time. Thus, the following interface suggests itself: paymentMethod 2-GetPaymentMethod

Payroll System
3-Pay Amount

Employee
1-GetPayment Amount

Payment Method
Here the PayrollSystem gets the payment amount and the payment method from the Employee. We presume that it doctors the payment amount by making income tax and FICA deductions, insurance payments, etc. And then it sends the final payment amount to the PaymentMethod object that it got from the Employee.

Given this interface, the design is as straightforward as the others have been.
A

Employee

Payment Method

Paycheck Payment Method

DirectDeposit Payment Method

Note again that Employee contains PaymentMethod by reference. This is to insure that the actual object contained can be a derivative of PaymentMethod and so act polymorphically. On several of the diagrams we have seen an object called: PayrollSystem. What is this object, and what are its class relationships? I think that this class provides the central control for the Payroll application and contains the set of all employees; as follows: <Employee>
A

Payroll System

itsEmployees

Set
0..n

Employee

Note the Instantiated Class icon for Set<Employee>. This is a standard Booch icon, and it corresponds to the use of templates in C++. Thus the corresponding code is: class PayrollSystem { private: Set<Employee> itsEmployees; }; Now we can create an object diagram which shows the gross flow of control within the entire application.

Set<Employee> 1-Create

SetIterator <Employee> 2-1-GetNext


Employee 2-2-PayToday YES

Payroll System
2-6-Pay 2-4-GetPay amount 2-5-GetPayment Method payment Method amount
P

Employee
2-3-IsThisPayday today YES

Payment Method

Payment Schedule
This shows just one of the many scenarios that apply to this simple application. This scenario shows the creation of the SetIterator, the test to see if the Employee should be paid today, with an affirmative result. It also shows the acquisition of the payment method, and the act of payment. Other scenarios that should be drawn would show the end of the iteration, negative responses to the IsThisPayday message and the flow of messages in the derivatives of PaymentSchedule, PaymentMethod and Employee. Note the simplicity of this algorithm. It knows nothing of the different kinds of schedules, employees or payment methods. It simply invokes the abstractions without knowing or caring what they actually do; the hallmark of OO. The algorithm can be expressed in C++ as follows: class PayrollSystem { public: void Run(); private: Set<Employee> itsEmployees; }; void PayrollSystem::Run() { SetIterator<Employee> i(itsEmployees); Employee* e = 0; while ((e = i.GetNext()) != 0) { if (e->PayToday()) {

double amount = e->GetPay(); // Calculate deductions et. al. e->GetPaymentMethod().Pay(amount); } } } This completes our brief case study of Object-Oriented Analysis and Design. We are now in a position to re-visit the questions that appeared at the beginning of this article. Are there any differences between OOA and OOD? If so, what are they? Yes, there are differences. OOA is the act of determining the abstractions which unerly the requirements. It is the backwards reasoning that starts with the requirements and ends up with a set of objects that express the abstractions; and messages that support the required behaviors. OOD embodies the set of decisions that determines how those objects and methods will be implemented. Typically class inheritance and composition hierarchies are among those design decisions. OOA exposes the components of the required behaviors, in order that they may be implemented. On the other hand, OOD hides the details of the implementation so that those details do not pollute the rest of the design. Despite the differences, however, OOA and OOD are simply at opposite ends of the same activity. Moreover, there is no good dividing line which separates them, either in time or in function. It is better to view OOA and OOD like the different colors in a spectrum. They are certainly different, but it is difficult to tell when one starts and the other leaves off. OOAD is a continuum of activity which incorporates both analysis and design. Should we do OOA first, followed by OOD? No, they are best done concurrently. Analysis and Design should not be separated into broad phases. Nor should the analysis model be frozen before the design modeling begins. It should be evident from our case study that OOA and OOD are very closely related, and that they cooperate synergistically. The analysis model cannot be completed in the absence of the design model. And the design model cannot be completed in the absence of the analysis model. One of the most important aspects of OOAD is the synergy between the two concepts. What is a reasonable output from OOA, OOD? At very least a set of class and object diagrams. The class diagrams show the static structure of the application. It is from these diagrams that class headers can be written. Object diagrams show the dynamics of the collaborations between objects. It is from these diagrams that the class implementations will be written. However, in large projects these meager beginnings are hardly adequate. There is much that we have not covered in this simple case study. We have not discussed how to organize the analysis and design of a large program. How to control large scale visibility.

How to control the physical model so that a large project can be developed and released in an organized and well managed way. These topics are important parts of the OOAD universe, and should not be neglected. Are there different notations for OOA and OOD? Yes and No. OOA, since it is based on the analysis of required behaviors, tends to focus on the behavioral model. Thus tit places more emphasis on the generation of object diagrams. We saw this in the case study. In every case, the central abstraction for each requirement was documented, first, by an Object Diagram. Conversely, since OOD focuses on finding static structures which support the required dynamics, it tends to focus on class diagrams. However, there are certainly plenty of cases where class diagrams are produced by OOA activities and object diagrams are produced by OOD activities. One of the great advantages of OOAD over other analysis and design techniques is that the notation for analysis is compatible with the notation for design. In fact, only one set of documents is maintained. We do not separate them into analysis documents and design documents.

There is one more point that I would like to make. In this case study, we walked through the requirements point by point. We analyzed each point, and tried to find the underlying abstractions. We used object diagrams to document the way that those abstractions worked. Then we used class diagrams and more object diagrams to express a design which implemented the abstractions. Thus we created a trail of documents that lead from requirements to design. Each design decision can be traced backwards, through the documentation trail, to one or more requirements. This is a very powerful mechanism. It means that we know that our design will implement all the requirement. We can prove that each requirement is addressed by the design. This is in contrast to other analysis and design methodologies in which tracing from requirements to design is very difficult if not completely impossible. Let me say this a different way. In previous analysis and design techniques, analysis focused on the static nature of the application as opposed to its dynamics. Rather than beginning with a point by point analysis of the requirements, a high level picture was drawn which was supposed to represent the amalgamation of all the requirements. Then this picture was decomposed into finer and finer partitions, each of which were supposed to express one or more requirements. When you were done with this kind of analysis, you had a set of pretty pictures, but did you have any proof that your partitioning was correct and that your pictures really represented an analysis of the requirements? Typically not. However, in this case study, by employing the techniques of OOA and OOD, we have provided an analysis and design of each requirement. We analyzed each requirement by considering the scenarios and use cases which contained the required behavior. Then we designed the software structures which would best implement those scenarios. We know that our design supports our analysis, and we know that our analysis is a set of abstractions which are based on the requirements. Therefore we can prove that each requirement is satisfied by the design; and that is an enormously powerful ability. In this article we have used a case study to learn about how Object-Oriented Analysis and Design are employed in the creation of a simple application. We saw how to analyze a requirement by searching for the underlying abstraction, and then expressing that abstraction as a collaboration

between objects. We also saw how to design the class structures which most effectively support those collaborations. Although this brief article does not do justice to the complex topic of OOAD, it is my hope that it has served as an appropriate introduction. If so, you should now be aware of the power and expressiveness of OOAD. It is my opinion that proper use of the object paradigm is embodied in the practice of OOAD, and that the object model cannot be properly exploited in any other manner. The most important decision a software engineer will make with regard to developing an application is whether he will spend the time needed in analyzing and designing it.

S-ar putea să vă placă și