heading 1 - department of computer science | san jose ... · web viewalthough word processors,...

95
3. Frameworks, Toolkits, and Polymorphsim Overview Polymorphism allows programmers to declare partially complete classes and functions. The idea being to complete the declaration with a subclass or a template instantiation when more application-specific information is available. Of course this is also the idea behind frameworks, toolkits, and many design patterns. In each case application-independent logic is captured by one part of the program, while application- dependent logic is concentrated in another. After a brief survey of frameworks and a more formal introduction to polymorphism, fragments of a fictional music framework called MFW are presented as a pretext to introducing virtual functions, abstract classes, templates, and several important design patterns. These patterns and mechanisms are subsequently assembled to create a working computer game framework called FUN (application frameworks

Upload: nguyendang

Post on 14-May-2018

216 views

Category:

Documents


3 download

TRANSCRIPT

3. Frameworks, Toolkits, and Polymorphsim

Overview

Polymorphism allows programmers to declare partially complete classes and functions. The

idea being to complete the declaration with a subclass or a template instantiation when

more application-specific information is available. Of course this is also the idea behind

frameworks, toolkits, and many design patterns. In each case application-independent logic

is captured by one part of the program, while application-dependent logic is concentrated in

another.

After a brief survey of frameworks and a more formal introduction to polymorphism,

fragments of a fictional music framework called MFW are presented as a pretext to

introducing virtual functions, abstract classes, templates, and several important design

patterns. These patterns and mechanisms are subsequently assembled to create a working

computer game framework called FUN (application frameworks will be developed in

Chapter 6) and a working toolkit called PIPES for assembling applications that instantiate

the Pipe and Filter architectural pattern.

Frameworks

A framework is a library of collaborating base classes that capture the logic, architecture,

and interfaces common to a family of similar applications. Frameworks are customized into

specific applications by deriving classes from framework classes.

Pattern Oriented Programming with C++/Pearce

A horizontal framework provides general services, and so can be customized into a wide

assortment of diverse applications. A vertical framework fixes a narrow application

domain and therefore requires less customization:

The idea of a partially completed application sounds strange at first. Why would anyone

want such a thing? But the idea makes sense for organizations that can't afford custom

software, and that require something beyond the general features of off-the-shelf software.

Typically, framework developers are not the application developers who customize the

framework. But if a framework for a particular application doesn't exist, it might make

sense for the application developers to create their own framework as a first step. In this

way a library of frameworks evolves that enables developers to quickly produce new

applications based on combinations of frameworks that have already been tested by

3-2

3. Frameworks, Kits, and Polymorphism

previous applications. This is called framework-based software development [ROG].

Framework-based development also makes sense for students, who can gain experience

writing challenging code without devoting too much time to application domain details.

Example: Application Frameworks

Although word processors, spread sheets, and database browsers don't have much in

common, they all have elaborate, platform-dependent graphical user interfaces, and they

can all save the user's work into a file or database. Application frameworks such as

Microsoft Foundation Classes (MFC), Object Windows Library (OWL), ET++, and JFC

provide generic graphical user interfaces with windows, menus, toolbars, dialog boxes, and

other common user interface components. (Application frameworks are covered in detail in

Chapter 7.)

Example: Expert System Shells

Expert systems are interactive programs that automate the collection, representation, and

generation of knowledge in a specific application domain. Expert systems are used by

doctors to diagnose patients (MYCIN), by geologists to locate good places to drill for oil

3-3

Pattern Oriented Programming with C++/Pearce

(PROSPECTOR), and by engineers to configure computers (XCON). These applications

are different, but they have common features such as user interfaces, inference engines, and

schemes for representing and organizing knowledge (semantic networks, frames, scripts,

rules, etc.). These common features can be captured in an expert system framework

(more commonly called an expert system shell). Examples include ART (Automated

Reasoning Tool by Inference Corporation), KEE (Knowledge Engineering Environment by

IntelliCorp), and GURU (by Micro Data Base Systems). See [FIRE] for a survey of expert

systems.

Example: Client-Server Frameworks

In a client-server application shared data is maintained by a program called the server, and

presented and manipulated by programs called clients. Often machine or network

boundaries separate clients and servers. The World Wide Web (WWW) is a typical client

server application. Web servers maintain collections of web pages that are delivered upon

request to web clients (i.e., web browsers).

In fact, WWW is such a generic client-server application, that Java has created a client-

server framework that piggy backs on top of WWW. An applet is a customizable client

3-4

3. Frameworks, Kits, and Polymorphism

that is executed by a web client. The applet communicates with a customizable server

called a servlet that is executed by a web server. Applets and Servlets are documented on

the web at [WWW 7].

Example: Workflow Frameworks

A workflow is a routine business or engineering process. For example, the workflow of a

grocery store check-out clerk might be described by the following state diagram:

3-5

Pattern Oriented Programming with C++/Pearce

A workflow application is a program that guides a worker through a workflow. For each

state, the workflow application prompts the worker for the information required to

complete the state. This may involve fetching and updating records in various remote

databases. When the information is collected, the application determines which state to

move to next.

For example, a help desk application guides a customer service agent through a sequence of

diagnostic questions and tests; a warehouse inventory management system guides

warehouse workers through an order processing workflow; and a point-of-sale application

guides check-out clerks through the states described above. When a sale is completed, the

inventory database is updated, any accounting software is notified, and the total amount in

the cash register is recomputed.

Although these applications are different, they have a much in common that can be

captured in a workflow framework. Each consists of a number of well defined

transactions. A graphical user interface displays each transaction and provides controls for

completing, canceling, undoing, redoing, saving, and restoring the transaction. Information

gathered during a transaction determines the next transaction.

3-6

3. Frameworks, Kits, and Polymorphism

IBM San Francisco is a family of workflow frameworks written in Java that can be

customized into a variety of business management systems. Currently, IBM San Francisco

offers three frameworks: a general ledger framework that provides the core functionality

used by most accounting applications, including budgeting, accounts receivable, and

accounts payable; a warehouse management framework which provides warehouse

control, picking stock, reception, and shipment processes needed by most warehouse

management systems; and an order management framework which provides the business

logic required in many manufacturing applications, including sales orders, purchase orders,

and pricing. (Documentation about IBM San Francisco can be found on the web at [WWW

10].)

Polymorphism

A type system for a language L is a set of primitive types together with a set of rules for

constructing, comparing, and naming types, as well as rules for binding types to the

expressions and values of L. For example, the primitive types of C++ include int, float,

char, and bool. Arrays, structs, unions, and pointers are examples of constructed types.

3-7

Pattern Oriented Programming with C++/Pearce

Instances of a monomorphic or uniform type all have the same representation, while

instances of a polymorphic or multi-form type can have a variety of representations. For

example, a monomorphic Real type might represent all real numbers using the IEEE

floating point standard representation, but a polymorphic Complex type might represent

some complex numbers using polar coordinate representation (reit) and others using

rectangular coordinate representation (a+bi). A polymorphic type often can be viewed as a

family of logically related subtypes.

In C++ an abstract class is a class with one or more pure virtual functions. In the following

example, Shape is an abstract class with a pure virtual draw() function:

class Shape{public:

virtual void draw() = 0;// etc.

};

Abstract classes can be regarded as polymorphic types1 in the sense that instances of

concrete shape-derived classes can be treated as generic shapes. For example, although we

can't declare instances of the Shape class, we can declare Shape pointers that can point at

any instance of a concrete Shape subclasses:

Shape* s[3];s[0] = new Triangle();s[1] = new Circle();s[2] = new Rectangle();for(int i = 0; i < 3; i++) s[i]->draw();

A C++ class template defines a paremeterized family of types, and therefore can also be

regarded as a polymorphic type (although this is somewhat at odds with C++ terminology).

For example, the list<T> template in the standard template library defines a family of

types, including:

1 We should be more careful with our terminology. Technically, a type is a data representation scheme. A class is a type together with a collection of member functions, and an abstract data type (ADT) is a specification of a type in terms of member function prototypes and axioms.

3-8

3. Frameworks, Kits, and Polymorphism

list<string>list<Shape*>list<list<int> >etc.

The term "polymorphic" is also applied to functions, although a polymorphic function is

simply an instance of a polymorphic function type. More concretely, a polymorphic

function is a family of closely related functions. For example, the virtual Shape::draw()

function is a polymorphic function that represents the family of draw() functions defined in

the Shape-derived classes.

C++ allows programmers to define function templates:

typedef <typename T>void swap(T& x, T& y){

T temp = x;x = y;y = temp;

}

We can think of the swap() template as the polymorphic representative of the family of its

instances.

Even a family of functions that simply share a name:

double area(Triangle x) { return x.height * x.base / 2; }double area(Rectangle x) { return x.height * x.width; }double area(Circle x) { return pi * x.radius * x.radius; }

can be thought of as variants of a single polymorphic function.

Working with Unknown Classes

In a sense, a framework is a polymorphic application. A framework is an application that

can take on many forms, depending on how it is customized. As such, framework

programmers often need to refer to classes that won't be defined until the framework is

customized, and of course different customizations may define these classes in different

ways. Polymorphism allows framework programmers to work around these critical pieces

of missing information.

3-9

Pattern Oriented Programming with C++/Pearce

MFW: A Framework for Music Applications

Assume we are developing a music framework called MFW. MFW is a horizontal

framework that can be customized into various musical applications such as score editors,

virtual recording studios, virtual instruments, expert systems for composers, and interfaces

to computer controlled instruments such as synthesizers.

One part of MFW defines various ensembles of musical instruments: bands, orchestras,

trios, quartets, etc. Unfortunately, the types of musical instruments will be defined much

later in the various customizations of the framework. How can MFW form ensembles

without knowing the identity of their constituent instruments? There are two solutions:

make Instrument an abstract class in MFW, or make Instrument an MFW template

parameter.

MFW with Abstract Classes

Although the specific types of instruments may be unknown, MFW can define an abstract

Instrument base class:

class Instrument{public:

virtual void play() = 0;};

MFW is unable to implement the play() member function, so it is declared as a pure virtual

function. Derived class programmers will be required to provide implementations. Even

though MFW couldn't implement play(), it can still call play(). For example, here is the

MFW definition of Trio. Notice that Trio::play() calls the play() function of each

instrument:

3-10

3. Frameworks, Kits, and Polymorphism

class Trio{public:

Trio(Instrument *a = 0, Instrument *b = 0, Instrument *c = 0){

first = a;second = b;third = c;

}void play(){

if (first) first->play();if (second) second->play();if (third) third->play();

}private:

Instrument *first, *second, *third;};

Suppose a programmer customizing MFW wishes to create and play trios consisting of

different combinations of horns, harps, and drums. The first step is to create Instrument

derived classes. For example:

class Harp: public Instrument{public:

void play(){

cout << "plink, plink, plink\n";}

};

Here is how trios are created and played in the customization:

Trio t1(new Harp(), new Horn(), new Drum());Trio t2(new Drum(), new Drum(), new Harp());t1.play();t2.play();

Heterogeneous Containers

A container or collection is an object that stores other objects. Stacks and queues are

familiar examples of containers. A heterogeneous container stores objects of different

types. For example, an orchestra can be regarded as a container of different types of

instruments:

3-11

Pattern Oriented Programming with C++/Pearce

class Orchestra{public:

void add(Instrument* p) { instruments.push_back(p); }void play();

private:vector<Instrument*> instruments;

};

An orchestra plays by invoking the play() method of each of its instruments:

void Orchestra::play(){

for(unsigned i = 0; i < instruments.size(); i++)instruments[i]->play();

}

Clients of the orchestra class can add a variety of instruments to orchestra instances:

Orchestra orch;orch.add(new Horn());orch.add(new Harp());orch.add(new Horn());orch.add(new Harp());orch.add(new Drum());orch.play();

Unfortunately, orchestras can only hold Instrument pointers, not the instruments

themselves. Why?

Virtual Factory Methods

Let's enhance MFW by adding an abstract Note class. Each note encapsulates a frequency

and a duration:

class Note{public:

Note(double f = 100, double d = 300) { freq = f; duration = d; }virtual void play() = 0; // quality? timbre?

protected:double freq; // in Hzdouble duration; // in mSec

};

Of course any musician will tell you that a note played on a guitar sounds quite different

from the same note played on a horn. For this reason we have left the implementation of

play() to MFW customizations; for example:

3-12

3. Frameworks, Kits, and Polymorphism

class HornNote: public Note{public:

HornNote(double f = 100, double d = 300): Note(f, d) {}void play(){

cout << "honk\n";}

};

Now that we can talk about notes in MFW we can provide an implementation of the play()

function in the Instrument base class:

void Instrument::play() {

for(int i = 0; i < 3; i++){

Note* n = makeNote();n->play(); delete n;

}}

But how can the Instrument class know what type of Notes to create? It would seem that

creating an unknown type of object is more difficult than simply using an unknown type of

object. How much memory should be allocated? How should the memory be initialized?

Creating unknown objects is a common theme in programming. The various approaches to

this problem are summarized by the Factory Method design pattern:

Factory Method [Go4]

Other Names

Virtual constructor.

Problem

A "factory" class can't anticipate the type of "product" objects it must create.

Solution

Provide the factory class with an ordinary member function that creates product objects. This is called a factory method. The factory method can be a virtual function implemented in a derived class, or a template function parameterized by a product constructor.

There are three variations of the pattern: virtual, smart, and template factory methods.

Template factory methods will be discussed later in the chapter, while smart factory

methods will be discussed in Chapter 5.

3-13

Pattern Oriented Programming with C++/Pearce

An ordinary member function that creates and returns new objects is called a factory

method. Although C++ doesn't allow virtual constructors, factory methods can be virtual:

class Instrument{public:

void play();// virtual factory method:virtual Note* makeNote(double f = 100, double d = 100) = 0;

};

The obligation to implement the virtual factory method falls on the shoulders of derived

classes:

class Horn: public Instrument{public:

// factory method:Note* makeNote(double f = 100, double d = 100) {

return new HornNote(f, d); }

};

One problem associated with virtual factory methods is that programmers must maintain

two parallel inheritance hierarchies, one for factories, and one for products:

If a programmer creates a new type of note, he must remember to create the corresponding

factory instrument that creates the note.

3-14

3. Frameworks, Kits, and Polymorphism

MFW with Templates

Instead of introducing an abstract Instrument class, MFW could have used templates

parameterized by instruments. For example MFW could have defined a trio to be a

template parameterized by the types of instruments in the trio:

template <typename A, typename B, typename C>class Trio{public:

Trio(){

first = new A();second = new B();third = new C();

}void play(){

if (first) first->play();if (second) second->play();if (third) third->play();

}private:

A* first;B* second;C* third;

};

Notice that the template parameters not only parameterize the types of instruments in the

trio, they also parameterize the instrument constructors called by the Trio constructor.

Because there is no abstract Instrument base class, MFW customizers don't need to derive

their instrument classes:

class Drum{public:

void play() { cout << "thump, thump, thump\n"; }};

The customization specifies the types of instruments a trio contains as template arguments:

Trio<Horn, Harp, Drum> trio1;Trio<Drum, Drum, Harp> trio2;trio1.play();trio2.play();

3-15

Pattern Oriented Programming with C++/Pearce

Of course the compiler will complain if one of the template arguments doesn't have a

member function called play():

Trio<Horn, Harp, Airplane> trio3; // error, no Airplane::play()!

Homogeneous Containers

The type of objects stored in a template container is given by the template argument. For

example, an orchestra might consist of several sections: horns, drums, strings, etc. each

with varying numbers of a particular type of instrument:

template <typename Instrument>class Section{public:

void add(Instrument* p) { instruments.push_back(p); }void play();

private:vector<Instrument*> instruments;

};

Notice that sections are not heterogeneous collections. One section contains one type of

instrument. We could define a section of mixed instruments if we reintroduce the abstract

instrument base class for Horn, Drum, and Harp, then proceed as follows:

Section<Instrument*> band;band.addInstrument(new Horn());band.addInstrument(new Drum());// etc.

Template containers are the approach taken in the C++ standard template library, while

heterogeneous containers are the approach taken in Java, where all classes extend a

common abstract base class called Object.

Template Factory Methods

Returning to the factory method design pattern, we could have defined Factory as a

template and treated the instrument class as a template parameter. Actually, it's not just the

instrument class that is represented by the template parameter, it's also the instrument

constructor:

3-16

3. Frameworks, Kits, and Polymorphism

template <typename Note>class Instrument{public:

void play(){

for (int i = 0; i < 3; i++){

Note* n = makeNote();n->play(); delete n;

}}Note* makeNote() { return new Note(); }

};

MFW customizations no longer need to derive their note classes from an abstract Note base

class:

class HarpNote{public:

void play(){

cout << "plink\n";}

};

Also, MFW customizations no longer need to create Instrument-derived classes. A horn is

simply an instance of the Instrument<HornNote> class, while a harp is an instance of the

Instrument<HarpNote> class:

Instrument<HornNote> *a = new Instrument<HornNote>();Instrument<DrumNote> *b = new Instrument<DrumNote>();Instrument<HarpNote> *c = new Instrument<HarpNote>();a->play();b->play();c->play();

Toolkits

A toolkit (also called an abstract factory or simply a kit) is a class or object that provides

factory methods that produce the components needed to construct all or part of an

application. In a sense, toolkits are precursors to frameworks. While a framework is already

assembled and only requires customization, a toolkit only provides the components

programmers will need to assemble an application. The advantage of a toolkit is that the

entire assembly process can be described relative to the toolkit. Thus, the toolkit decouples

3-17

Pattern Oriented Programming with C++/Pearce

the assembly process from the details of how the components are created. Toolkits are

formalized by the abstract factory design pattern:

Abstract Factory [Go4]

Other Names

Kit, toolkit

Problem

A system should be independent of how its components are implemented.

Solution

Provide the system with an abstract factory parameter. An abstract factory is a class consisting of virtual or template factory methods for making the system's components. Various derived classes or template instances provide implementations of the factory methods.

As an example, let's consider building graphical user interfaces (GUIs). Unfortunately, the

standard C++ library doesn't provide GUI components such as windows, menus, and

buttons. Part of the reason for this is that GUI components are highly platform-dependent.

So instead, these components are supplied by platform-specific libraries such as the

X-Windows library. This creates a huge problem when its time to port an application to a

new platform. Studies show that a GUI might comprise 30% to 60% of a commercial

application's code, all of which must be rewritten for the port!

We can partially relieve the pain of a port by using a toolkit to decouple the construction of

a GUI from the details of how the GUI components are constructed. We begin by defining

an abstract user interface toolkit:

class UIToolkit{public:

virtual Window* makeWindow() = 0;virtual Button* makeButton() = 0;virtual MenuItem* makeMenuItem() = 0;virtual EditBox* makeEditBox() = 0;virtual ListBox* makeListBox() = 0;// etc.

};

In addition, the toolkit includes a hierarchy of abstract component classes or interfaces:

3-18

3. Frameworks, Kits, and Polymorphism

Here is a sketch of the component hierarchy's base class:2

class UIComponent {public:

// draw this component in parent window:virtual void draw() = 0;// handle keyboard & mouse messages:virtual void handle(Message msg) {}// etc.

protected:Point corner; // upper left cornerint height, width; // sizeWindow* parent; // = 0 for desktop window

};

The GUI for a particular application is constructed by a function parameterized by

UIToolkit:

Window* makeMyGUI(UIToolkit* tk){

Window* w = tk->makeWindow();Button* b = tk->makeButton();w->adopt(b);EditBox* e = tk->makeEditBox();w->adopt(e);// etc.return w;

}

We only need to describe the construction of our GUI once, in makeMyGUI(). If we want

to construct a GUI for a particular platform, we simply call makeMyGUI() with a platform-

specific implementation of UIToolkit.

2 UI components will be discussed in greater detail in Chapter 6.

3-19

Pattern Oriented Programming with C++/Pearce

For example, assume Z Windows is a library of GUI components for a particular platform

(e.g., Z = X, MFC, MacApp2, etc.):

Someone knowledgeable in Z Windows programming could provide implementations of

each of the UIToolkit interfaces. This can be done using adapters (which will be discussed

in Chapter 4). For example:

class ZButtonAdapter: public Button{public:

ZButtonAdapter() { peer = new ZButton(); }~ZButtonAdapter() { delete peer; }void draw() { peer->paint(); }void handle(Message msg) { peer->onMsg(msg); }

private:ZButton* peer; // the corresponding Z component

};

We must also provide a Z Windows implement the abstract toolkit class:

class ZUIToolkit: public UIToolkit{public:

virtual Window* makeWindow() { return new ZWindowAdapter(); }virtual Button* makeButton() { return new ZButtonAdapter(); }virtual MenuItem* makeMenuItem() { return new ZMenuItemAdapter(); }virtual EditBox* makeEditBox() { return new ZEditBoxAdapter(); }virtual ListBox* makeListBox() { return new ZListBoxAdapter(); }// etc.

};

Here is how the programmer might start an application using the ZUIToolkit:

3-20

3. Frameworks, Kits, and Polymorphism

int main(){

Window* appWindow = makeMyGUI(new ZUIToolkit());appWindow->draw(); // draw app window & start msg loopreturn 0;

};

Generic Methods

Sometimes the general sequence of tasks a function must perform is the same across a

variety of applications, suggesting that the function belongs in a common framework.

However, the tasks performed by the function are application-specific, suggesting the

function belongs in the various framework customizations.

The generic algorithm design pattern solves this problem. Move the function into the

framework and declare the application-specific tasks to be pure virtual functions in the

framework. A function like this is called a generic algorithm or a template method.3

Generic Algorithm [Go4], [ROG]

Other Names

Template method

Problem

Parts of an algorithm are invariant across a family of framework customizations, while other parts are not.

Solution

Move the algorithm into the framework. Replace the non-invariant parts by calls to virtual functions that can be implemented differently in different customizations.

Generic algorithms follow the inverted control structure sometimes called the Hollywood

principle: "Don't call us, we'll call you."

Let's start with a trivial example to clarify the idea; more serious examples will come later.

Suppose we want to add a symphony class to our music framework, MFW. Symphonies are

usually divided into four movements, so playing a symphony is easy: play the first

movement, then the second, then the third, finally the fourth. Of course the similarity

between symphonies ends here. The particulars of each movement vary from one

symphony to the next, and will have to be specified in various customizations of the

3 Warning: template methods are not the same as C++ template functions.

3-21

Pattern Oriented Programming with C++/Pearce

framework. We can still add an abstract symphony base class to our framework, but play()

will have to be a generic algorithm:

class Symphony{public:

void play() // a generic algorithm{

doFirstMovement();doSecondMovement();doThirdMovement();doFourthMovement();

}protected:

virtual void doFirstMovement() = 0;virtual void doSecondMovement() = 0;virtual void doThirdMovement() = 0;virtual void doFourthMovement() = 0;

};

Framework customizers will have to subclass Symphony and implement the do-functions.

For example:

class TheFifth: public Symphony{

void doFirstMovement(){

cout << "dah, dah, dah, duh ...\n";}void doSecondMovement(){

cout << "duh, duh, duh, dah ...\n";}void doThirdMovement(){

cout << "dah, duh, duh, dah ...\n";}void doFourthMovement(){

cout << "duh, dah, dah, duh ...\n";}

};

Singletons

Sometimes we want a class to have at most one instance. Why? Multiple instances might

lead to confusion. For example, it would be confusing if an application had multiple user

interfaces, simultaneously:

GUI gui1, gui2; // which one to use?

3-22

3. Frameworks, Kits, and Polymorphism

Multiple instances might lead to sharing problems. For example, changes to one instance of

a company's budget must be reflected in other instances:

Budget budget1, budget2; // must remember to keep synchronized

In some situations, multiple instances might simply be illogical:

TheFifth s1, s2, s3; // how many Fifth Symphonies did he write?

The Singleton pattern solves this problem by making all constructors private. A public

factory method is provided that allows users to create instances, but the factory method

always returns pointers to the same hidden instance:

Singleton [Go4]

Problem

There must be at most one instance of a class.

Solution

Make all constructors private, including the default and copy constructors. Provide a static function that initializes and returns a private, static pointer to the sole instance of the class.

But doesn't this create a chicken and egg problem? How do we get the first instance of the

class from which we can invoke the factory method:

y = x->makeSingleton(); // where does x come from?

This problem is solved by making the factory method static:

y = Singleton::makeSingleton(); // no implicit parameter needed

For example, let's apply the singleton pattern to the TheFifth class from the previous

example:

3-23

Pattern Oriented Programming with C++/Pearce

class TheFifth: public Symphony{public:

static TheFifth* makeTheFifth() // factory method{

if (!theFifth)theFifth = new TheFifth(); // constructor access ok here

return theFifth;}// etc.

private:static TheFifth* theFifth; // the one and onlyTheFifth() {} // hide default constructorTheFifth(const TheFifth& tf) {} // hide copy constructor~TheFifth() {} // hide destructor

};

Notice that the default and copy constructors are made private, as is the destructor. The

pointer to the one and only instances is declared to be a private static variable. It must be

defined and initialized separately:

TheFifth* TheFifth::theFifth = 0;

Unsuspecting clients can call the factory method as many times as they want, but only one

instances is ever created:

int main(){

TheFifth* s1 = TheFifth::makeTheFifth();TheFifth* s2 = TheFifth::makeTheFifth();TheFifth* s3 = TheFifth::makeTheFifth();s1->play();s2->play();s3->play();return 0;

}

Applying the delete operator to one pointer, which would turn the other pointers into

dangling references, is prevented by the compiler because the destructor is private:

delete s3; // error, cannot access private member

Any attempt to create instances by other means meets with a similar fate:

TheFifth s4(*s3); // error, cannot access private memberTheFifth s5; // error, cannot access private member

3-24

3. Frameworks, Kits, and Polymorphism

The compiler even forbids Fifth Symphony value parameters, because the mechanism for

passing value parameters—the copy constructor—has been declared private:

void play(TheFifth s) { s.play(); } // error

Although Fifth Symphony reference parameters are allowed:

void play(TheFifth& s) { s.play(); } // okay

FUN: A Framework for Adventure Games

Let's put the patterns we have learned together by building a framework for text-based

adventure games. Our framework will be called FUN. The restriction to text-based games

(yes, such things really did exist) is only because of the unavailability of graphics utilities

in the standard library, but the architecture of the framework could easily be used for

graphics-based games.

The framework is possible because most adventure games follow the same basic plot:

A hero wanders from room to room in a spooky maze. He/she searches for enough keys to unlock a door and escape from the maze. Unfortunately, monsters hiding in each room hamper the search by attacking the hero upon entry. Naturally, the hero fights back. All of this fighting depletes the energy of the hero and the energy of the monsters. Some rooms contain "energy bowls". Drinking from an energy bowl partially restores the hero's energy. The game ends when the hero escapes or when the hero dies (i.e., the hero's energy level reaches zero).

This plot can be captured in the FUN framework. The nature of the rooms, monsters, and

heroes is specified in various customizations of the framework. For example, here are some

excerpts from a Medieval customization of the FUN framework called Dungeons and

Dragons. The user controls the hero using commands for moving, fighting, drinking, and

other of life's basic necessities:

3-25

Pattern Oriented Programming with C++/Pearce

-> helpObjective: Find enough keys to unlock a room and escape.help (displays this message)quit (terminates session)check (describe yourself)drink (drink any energy bowls in room)fight (fight all monsters in room)grab (grab any keys in room)map (describe the maze)move DIR (move hero DIR = N, S, E, or W)scan (describe the room)super (super charge hero)->

The maze is an N by M grid of rooms. Thus, each room has two, three, or four neighboring

rooms. The game begins with the hero standing in the North-West corner room of the

maze:

When the hero moves into a new room, the room is described. In a graphics-based game a

picture or animation of the room might be displayed in a window. If there are monsters in

the room, they attack and the hero defends:

-> move NThat door is locked-> move SThis is a dark, damp dungeon where prisoners rot (# of monsters = 1) (# of energy bowls = 5) (# of keys = 1) (# of locks = 26)A dragon flames Lancelot (damage = 3)Lancelot slashes at a dragon (damage = 8)done-> checkLancelot is a brave knight with a sharp sword (Lancelot.energy = 96) (Lancelot.keys = 0)done->

3-26

3. Frameworks, Kits, and Polymorphism

Notice that the hero's energy level has dropped from 100%, the maximum, to 96% as a

result of the fighting.

In addition to monsters, some rooms contain keys and energy bowls. The hero drinks from

the bowls and collects the keys:

-> drinkLancelot drinks 5 energy bowlsdone-> grabLancelot collects 1 keys.done->

When the hero moves into a room in which the number of locks is less than or equal to the

number of keys the hero has collected, the hero escapes and the game ends in a victory for

the user:

-> move nLancelot has escaped!

Of course in a space-age customization of FUN heroes are spacemen, rooms are labs, and

monsters are killer robots:

-> move wThis is a creepy lab where horrible experiments occur (# of monsters = 1) (# of energy bowls = 5) (# of keys = 1) (# of locks = 26)A robot crushes Buzz (damage = 8)Buzz zaps a robot (damage = 1)done-> checkBuzz is a futuristic spaceman with a lazer gun (Buzz.energy = 91) (Buzz.keys = 0)done

The Design

Players will use a game console to navigate their heroes through a maze of monster-

inhabited rooms. A game factory decouples the construction of the maze from the

construction of monsters and rooms. Here's our design:

3-27

Pattern Oriented Programming with C++/Pearce

Customizations of FUN will be expected to complete implementations of critical functions

in classes derived from the abstract Hero, Room, and Monster classes.

The Cast of Characters

Although we don't know the exact nature of heroes and monsters, we can introduce abstract

Hero and Monster base classes in FUN. These classes derive from FUN's Character base

class:

3-28

3. Frameworks, Kits, and Polymorphism

class Character{public:

Character(string nm = "unknown"){

name = nm;energy = 100;

}virtual ~Character() {}int getEnergy() { return energy; }void setEnergy(int e) { energy = e; }string getName() { return name; }int injure(Character* c);virtual void describe() = 0;

protected:int energy; // dead = 0% <= energy <= 100%string name;

};

Characters can cause injuries to other characters using the injure() member function.

Injuring a character randomly lowers that character's energy by an amount between 0 and

MAX, where:

MAX = DAMAGE_CONTROL + energy/c->energy

DAMAGE_CONTROL is a constant that can be redefined by FUN customizations. Notice

that the maximum amount of damage also depends on the comparative strengths of the

attacker and the victim. Injuring a character also causes a small depletion in the attacker's

energy level. Of course exceptions are thrown if either attacker or victim is initially dead:

int Character::injure(Character* c){

if (!energy) throw AppError(name + DEAD);if (!c->energy) throw AppError(c->name + DEAD);int damageCaused = rand() % (DAMAGE_CONTROL + energy/c->energy);int damageSustained = 1; // cuz fight'n is tough workc->energy = max(c->energy - damageCaused, 0);energy = max(energy - damageSustained, 0);return damageCaused;

}

The only thing we can say in FUN about monsters is that they are characters who attack

heroes:

3-29

Pattern Oriented Programming with C++/Pearce

class Monster: public Character{public:

Monster(string nm = "unknown"): Character(nm) {}virtual ~Monster() {}virtual void attack(Hero* h) = 0;virtual void describe() = 0;

};

We can say more about heroes in FUN. A hero is a character who defends himself against

monsters. In addition, a hero can move from room to room, collect keys, pick fights with

monsters, and invigorate himself by drinking from energy bowls:

class Hero: public Character{public:

Hero(string nm = "unknown"): Character(nm) { keys = 0; room = 0; }virtual ~Hero() {}virtual void defend(Monster* m) = 0;void move(Direction d); // move to a new roomvoid invigorate(); // drink room's energy bowlsint getKeys() { return keys; }void takeKeys(); // collect room's keysRoom* getRoom() { return room; }void setRoom(Room* r) { room = r; }void fight(); // fight room's monstersvirtual void describe() = 0;

private:Room* room; // = current locationint keys; // = # of keys collected

};

Even though describe() is a pure virtual function in both the Character and Hero classes, we

can still provide them with implementations:

void Character::describe(){

cout << " (" << name << ".energy = " << energy << ")\n";}

void Hero::describe(){

Character::describe();cout << " (" << name << ".keys = " << keys << ')' << endl;

}

Declaring a member function to be pure virtual requires concrete derived classes to

overwrite it. However, the overwrite can call the partially implemented function inherited

from the abstract base class.

3-30

3. Frameworks, Kits, and Polymorphism

The Field of Battle

What can we say in FUN about rooms? We can't say what rooms look like, but we can say

that each room has keys, locks, energy bowls, monsters, and up to four neighboring rooms.

class Room{public:

Room(int e = 0, int k = 0, int l = 500);virtual ~Room() {}virtual void enter(Hero* h);int takeEnergy(Hero* h); // give energy bowls to hint takeKey(Hero* h); // give keys to hvirtual void describe() = 0; // display or printvirtual void attack(Hero* h); // monsters attack hint getLocks() { return locks; }void setEnergy(int bowls) { energy = bowls; }void setKeys(int k) { keys = k; }void setLocks(int l) { locks = l; }void add(Monster* m) { monsters.push_back(m); }Room* getNeighbor(Direction d) { return neighbors[int(d)]; }void setNeighbor(Direction d, Room* r) {

neighbors[int(d)] = r;}

private:int keys; // = # of keys to be collected (< locks)int energy; // = # of energy bowls vector<Monster*> monsters;Room* neighbors[4];bool visited; // = true after 1st hero visit (not used)int locks; // = # of keys needed to escape this room

};

When a hero enters a room, he immediately escapes if the number of keys he has collected

is greater than or equal to the number of locks in the room. Otherwise, the room is

described by calling the virtual describe() function, which must be defined by FUN

customizations. Next, the monsters attack:

void Room::enter(Hero* h){

h->setRoom(this);if (h->getKeys() >= locks) // hero wins game!

throw AppError(h->getName() + ESCAPED); describe(); // describe the room to hattack(h); // get h!visited = true; // h waz here (may or may not be important)

}

3-31

Pattern Oriented Programming with C++/Pearce

The attack() function uses the Generic Algorithm pattern. Each live monster in the room

attacks the hero, and the hero defends himself against the monster. Of course the actual

meaning of attack and defend must be determined by customizations. If the hero is dead,

either before or after the attack, an exception is thrown that ends the game. After the

fighting, the dead monsters are removed from the room:

void Room::attack(Hero* h){

for(int i = 0; i < int(monsters.size()); i++)if (monsters[i]->getEnergy()){

if (!h->getEnergy()) throw AppError(h->getName() + DEAD);monsters[i]->attack(h);if (!h->getEnergy())

throw AppError(h->getName() + " has been killed!");h->defend(monsters[i]);if (!monsters[i]->getEnergy())

cout << "A monster has been killed!\n";}

// clean out the corpses:vector<Monster*>::iterator p = monsters.begin();for( ; p != monsters.end(); ){

if (!(*p)->getEnergy()) monsters.erase(p);// p++ if previous line didn't advance p to the end:if (p != monsters.end()) p++;

}}

The Maze uses the Singleton pattern to insure that there is at most one maze. The maze

contains a two dimensional array of Room pointers. The dimensions of the array are

constants that can be redefined in the customization:

3-32

3. Frameworks, Kits, and Polymorphism

class Maze{public:

static Maze* makeMaze(GameFactory* gf = 0){

if (!theMaze) theMaze = new Maze(gf);return theMaze;

}void add(Room* room, int i, int j);Room* getRoom(int i, int j){

if (i < 0 || ROWS <= i || j < 0 || COLS <= j)throw AppError("room index out of range");

return rooms[i][j];}void describe(); // describe every room in this maze

private:static Maze* theMaze;Maze(GameFactory* gf = 0);Maze(const Maze& m) {}~Maze() {}Room* rooms[ROWS][COLS];

};

The private Maze constructor is parameterized by a toolkit called a game factory, which is

used to create rooms and monsters. By default, each room in the maze contains one key,

five energy bowls, and a number of locks equal to the total number of keys plus one. Only

the room in the North-West corner has a number of locks equal to the total number of keys.

Thus, the hero will have to visit every room in the maze, collect all of the keys, then return

to the North-West corner room to escape. Also, the number of monsters per room increases

as the hero moves toward the South-East corner.

Maze::Maze(GameFactory* gf){

srand(time(0)); // seed rand()// pre-initialize room pointers:for(int i = 0; i < ROWS; i++)

for(int j = 0; j < COLS; j++)rooms[i][j] = 0;

// create rooms, keys, locks, bowls, & monsters:for(int i = 0; i < ROWS; i++)

for(int j = 0; j < COLS; j++){

add(gf->makeRoom(), i, j);// add i + j monsters to this room:for(int k = 0; k < numMonsters; k++)

rooms[i][j]->add(gf->makeMonster());}

rooms[0][0]->setLocks(KEYS); // the only way out}

3-33

Pattern Oriented Programming with C++/Pearce

Of course the number of keys, locks, monsters, energy bowls, and neighbors a room has

can be easily adjusted in framework customizations.

The FUN framework provides an abstract factory for making game components:

class GameFactory{public:

virtual ~GameFactory() {}virtual Monster* makeMonster() = 0;virtual Hero* makeHero() = 0;virtual Room* makeRoom(int e = 5, int k = 1, int l = KEYS + 1) = 0;virtual Maze* makeMaze() { return Maze::makeMaze(this); }

};

Dungeons and Dragons

Dungeons and Dragons is an adventure game set in Medieval times. We implement

Dungeons and Dragons as a customization of the FUN framework. Monsters are fire-

breathing dragons:

class Dragon: public Monster{public:

Dragon(string nm = "dragon"): Monster(nm) {}void attack(Hero* h){

cout << "A dragon flames " << h->getName();cout << " (damage = " << injure(h) << ')' << endl;

}void describe(){

cout << "Here is a fire-breathing dragon\n";Monster::describe();

}};

Heroes are brave knights who defend themselves from dragons by slashing at them with

sharp swords:

3-34

3. Frameworks, Kits, and Polymorphism

class Knight: public Hero{public:

Knight(string nm = "Lancelot"): Hero(nm) {}void defend(Monster* m){

cout << name << " slashes at a dragon";cout << " (damage = " << injure(m) << ")\n";

}void describe(){

cout << name << " is a brave knight with a sharp sword\n";Hero::describe();

}};

And rooms are dark, damp dungeons:

class Dungeon: public Room{public:

Dungeon(int e = 0, int k = 0, int l = 500): Room(e, k, l) {}void describe(){

cout << "This is a dark, damp dungeon where prisoners rot\n";Room::describe();

}};

Dungeons and Dragons implements the factory methods of FUN's GameFactory so that

they produce Dragons, Knights, and Dungeons:

class DNDFactory: public GameFactory{public:

Monster* makeMonster() { return new Dragon(); }Hero* makeHero() { return new Knight(); }Room* makeRoom(int e = 5, int k = 1, int l = KEYS + 1){

return new Dungeon(e, k, l);}

};

Starting a game is done by customizations. For example, here is how Dungeons and

Dragons gets started:

int main(){

GameConsole console(new DNDFactory());console.controlLoop();return 0;

}

3-35

Pattern Oriented Programming with C++/Pearce

Pipelines

A pipe is a message queue. A message can be anything. A filter is a process, thread, or

other component that perpetually reads messages from an input pipe, one at a time,

processes each message, then writes the result to an output pipe. Thus, it is possible to form

pipelines of filters connected by pipes:

The inspiration for pipeline architectures probably comes from signal processing. In this

context a pipe is a communication channel carrying a signal (message), and filters are

signal processing components such as amplifiers, noise filters, receivers, and transmitters.

Pipelines architectures appear in many software contexts. (They appear in hardware

contexts, too. For example, many processors use pipeline architectures.) UNIX and DOS

command shell users create pipelines by connecting the standard output of one program

(i.e., cout) to the standard input of another (i.e., cin):

% cat inFile | grep pattern | sort > outFile

In this case pipes (i.e., "|") are inter process communication channels provided by the

operating system, and filters are any programs that read messages from standard input, and

write their results to standard output.

LISP programmers can represent pipes by lists and filters by list processing procedures.

Pipelines are built using procedural composition. For example, assume the following LISP

procedures are defined4. In each case the nums parameter represents a list of integers:

// = list got by removing even numbers from nums(define (filterEvens nums) ... )

// = list got by squaring each n in nums(define (mapSquare nums) ... )

// = sum of all n in nums(define (sum nums) ... )

4 See Programming Note 8 in Appendix 1..

3-36

3. Frameworks, Kits, and Polymorphism

We can use these procedures to build a pipeline that sums the squares of odd integers:

Here's the corresponding LISP definition:

// = sum of squares of odd n in nums(define (sumOddSquares nums)

(sum (mapSquare (filterEvens nums))))

Pipelines have also been used to implement compilers. Each stage of compilation is a filter:

The scanner reads a stream of characters from a source code file and produces a stream of

tokens. A parser reads a stream of tokens and produces a stream of parse trees. A translator

reads a stream of parse trees and produces a stream of assembly language instructions. We

can insert new filters into the pipeline such as optimizers and type checkers, or we can

replace existing filters with improved versions.

There's even a pipeline design pattern:

Pipes and Filters [POSA]

Other Names

Pipelines

Problem

The steps of a system that processes streams of data must be reusable, re orderable, replaceable, and/or independently developed.

Solution

Implement the system as a pipeline. Steps are implemented as objects called filters. Filters receive inputs from, and write outputs to streams called pipes. A filter knows the identity of its input and output pipes, but not its neighboring filters.

Filter Classification

There are four types of filters: producers, consumers, transformers, and testers. A

producer is a producer of messages. It has no input pipe. It generates a message into its

3-37

Pattern Oriented Programming with C++/Pearce

output pipe. A consumer is a consumer of messages. It has no output pipe. It eats messages

taken from its input pipe. A transformer reads a message from its input pipe, modulates it,

then writes the result to its output pipe. (This is what DOS and UNIX programmers call

filters.) A tester reads a message from its input pipe, then tests it. If the message passes the

test, it is written, unaltered, to the output pipe; otherwise, it is discarded. (This is what

signal processing engineers call filters).

Filters can also be classified as active or passive. An active filter has a control loop that

runs in its own process or thread. It perpetually reads messages from its input pipe,

processes them, then writes the results to its output pipe. An active filter needs to be

derived from a thread class provided by the operating system:

class Filter: public Thread { ... };

An active filter has a control loop function. Here's a simplified version that assumes the

filter is a transformer:

void Filter::controlLoop(){

while(true){

Message val = inPipe->read();val = transform(val); // do something to valoutPipe->write(val);

}}

We will explore active filters in Chapter 7.

When activated, a passive filter reads a single message from its input pipe, processes it,

then writes the result to its output pipe:

void Filter::activate(){

Message val = inPipe->read();val = transform(val); // do something to valoutPipe->write(val);

}

There are two types of passive filters. A data-driven filter is activated when another filter

writes a message into its input pipe. A demand-driven filter is activated when another

filter attempts to read a message from its empty output pipe.

3-38

3. Frameworks, Kits, and Polymorphism

Dynamic Structure: Data-Driven

Assume a particular data-driven pipeline consists of a producer connected to a transformer,

connected to a consumer. The producer writes a message to pipe 1, the transformer reads

the message, transforms it, then writes it to pipe 2. The consumer reads the message, then

consumes it:

Dynamic Structure: Demand-Driven

A data-driven pipeline pushes messages through the pipeline. A demand-driven pipeline

pulls messages through the pipeline. Imagine the same set up using demand-driven passive

filters. This time read operations propagate from the consumer back to the producer. A

message is produced and written to pipe 1. The transformer reads the message, transforms

it, then writes it to pipe 2. This message is the value returned by the consumer's original

call to read():

3-39

Pattern Oriented Programming with C++/Pearce

A Problem

Both diagrams reveal a design problem. How does the transformer know when to call

pipe1.read()? How does the data-driven consumer know when to call pipe2.read()? How

does the demand-driven producer know when to produce a message? Active filters solve

this problem by polling their input pipes or blocking when they read from an empty input

pipe, but this is only feasible if each filter is running in its own thread or process.

We could have the producer in the data-driven model signal the transformer after it writes a

message into pipe 1. The transformer could then signal the consumer after it writes a

message into pipe 2. In the demand-driven model the consumer could signal the

transformer when it needs data, and the transformer could signal the producer when it

needs data. But this solution creates dependencies between neighboring filters. The same

transformer couldn't be used in a different pipeline with different neighbors.

Our design problem fits the same pattern as the problem of how the reactor in Chapter 2

communicates with its unknown monitors. We solved that problem by making the reactor a

publisher and the monitors subscribers. We can use the publisher-subscriber pattern here,

too. Pipes are publishers and filters are subscribers. In the data-driven model filters

subscribe to their input pipes. In the demand-driven model filters subscribe to their our

output pipes.

3-40

3. Frameworks, Kits, and Polymorphism

Pipes: A Pipeline Toolkit

How can we provide a toolkit for building data-driven pipelines? The toolkit should

provide factory methods for making different types of filters: testers, transformers,

producers, and consumers. Each factory method should be parameterized by the input and

output pipes, if needed, and a pointer to a function to be used for testing, transforming,

producing, or consuming a single message:

Filter* PipelineKit<Msg>::makeProducer(ProducerProc f, Pipe* op);Filter* PipelineKit<Msg>::makeTransformer(

Pipe* ip, TransformerProc f, Pipe* op);Filter* PipelineKit<Msg>::makeTester(

Pipe* ip, TesterProc f, Pipe* op);Filter* PipelineKit<Msg>::makeConsumer(Pipe* ip, ConsumerProc f);

Of course the kit should allow users to build pipelines that process any type of message. To

accomplish this, the kit can make no assumptions about message types. In C++ we can

make kit a template parameterized by message type:

template <class Msg> class PipelineKit { ... };

For example, our test driver creates a pipeline that computes the sum of even squares. We

begin by defining several simple global functions:

int square(int x) { return x * x; }

bool isEven(int x) { return x % 2 == 0; }

void accum(int x){

static int total = 0; // retains its value between callstotal += x;cout << total << endl;

}

bool getNumber(int& val){

cin >> val;return cin; // = false if failure

}

The main() function uses the pipeline kit to create pipeline:

3-41

Pattern Oriented Programming with C++/Pearce

int main(){

typedef PipelineKit<int> Kit; // for brevityKit::PipePtr p1, p2, p3;Kit::FilterPtr f1, f2, f3, f4;

p1 = Kit::makePipe();p2 = Kit::makePipe();p3 = Kit::makePipe();

// f1-p1->f2-p2->f3-p3->f4f1 = Kit::makeProducer(getNumber, p1);f2 = Kit::makeTester(p1, isEven, p2);f3 = Kit::makeTransformer(p2, square, p3);f4 = Kit::makeConsumer(p3, accum);

f1->start();return 0;

}

To run the program, we need to create several files of test numbers. For example, the file

nums.txt contains the following entries:

1234 5 6789101112

We can use file redirection to connect the standard input, cin in getNumber(), to nums.txt,

and the standard output, cout in accum(), to a new file called squares:

D:\> soes < nums.txt > squares

Here's the content of squares after the run:

42056120220364

We can also run our program interactively. The boldface text indicates program output:

3-42

3. Frameworks, Kits, and Polymorphism

C:/> soes1243420565678120910220quitC:/>

From the test driver we can infer that the kit's factory methods are class functions (i.e. static

functions) that return pointers to pipes and filters. We can also see that pipes and filters are

inner classes, more on this later.

Design

Users of a pipeline toolkit will want to add and remove filters from pipelines without too

much difficulty. Therefore, filters in a pipeline should be independent of each other. In

other words, a filter should only know the identity of the pipes that connect it to its

neighboring filters, not the neighbors themselves. A pipe should be able to connect any

filters. Therefore, although a filter may know the identity of its input and output pipes, a

pipe is loosely coupled to the filters it connects. As mentioned earlier, this creates a

notification problem. How will a pipe in a data-driven pipeline notify its downstream filter

that a message has arrived? We use the Publisher-Subscriber problem to solve this

problem. Pipes are publishers, and filters are subscribers that subscribe to their input pipes

in data-drive pipelines.

Using the Publisher-Subscriber pattern may be an example of over-design. After all, a pipe

only has one filter it must notify when a message arrives. Although we don't demonstrate

this feature, our pipes can also be used as tees. A tee can connect a filter to multiple

downstream filters.

3-43

Pattern Oriented Programming with C++/Pearce

In this case a pipe may need to notify unknown numbers and types of downstream filters

when a message arrives.

Although a particular pipeline processes a particular type of message, different pipelines

may process different types of messages. In any case, the toolkit can make no assumption

about the types of messages that will be processed. This information will be supplied by

users as they build pipelines. We could deal with this missing information the same way

the FUN framework dealt with monsters and heroes: by introducing Msg (Message) as an

abstract base class. For variety, PIPES takes a different approach: Msg will be a template

parameter.

The following class diagram shows the relationships between the principle classes of the

PIPES framework. The toolkit itself is not shown:

The Filter base class maintains one or two pointers to pipes. These are the input and output

pipes. It also provides a virtual start function that is redefined in the Producer class. This is

3-44

3. Frameworks, Kits, and Polymorphism

the engine that drives the pipeline. In a demand-driven pipeline the consumer implements

start(). Filter is shown as an abstract class because it doesn't provide an implementation of

the pure virtual update() function inherited from the Subscriber class. This job is left to the

Filter-derived classes.

Implementation

The entire toolkit is contained in a header file called pipes.h. This file contains the

declaration of a class of exceptions programmers must throw if minor problems arise:

class PipeLineError: public runtime_error{public:

PipeLineError(string gripe = "unknown"): runtime_error(gripe){}

};

In addition, the file contains the pipeline kit template:

template <class Msg>class PipelineKit{

class Pipe: public Publisher { ... };class Filter: public Subscriber { ... };class Transformer: public Filter { ... };class Producer: public Filter { ... };class Consumer: public Filter { ... };class Tester: public Filter { ... };// etc.

};

We have made Pipe, Filter, Producer, etc. inner classes of PipelineKit. This is common for

toolkits, as it offers the possibility of coexisting pipeline toolkits that create different types

of pipes and filters. Each toolkit hides its own pipe and filter implementations.

Using Function Pointers

Reexamine how the test driver created its four filters:

f1 = Kit::makeProducer(getNumber, p1);f2 = Kit::makeTester(p1, isEven, p2);f3 = Kit::makeTransformer(p2, square, p3);f4 = Kit::makeConsumer(p3, accum);

3-45

Pattern Oriented Programming with C++/Pearce

In each case the name of the message processing function was passed as an argument. This

seems a bit strange, at first. Normally, argument lists follow function names:

f(a, b, c);

A function name that isn't followed by an argument list is similar to an array name that isn't

followed by an index. For example, if nums is an array, then nums[0] is the same as *nums,

but by itself, nums is a pointer to the beginning of the array. Similarly, sin(0) calls the sin

function, but by itself, sin is just a pointer to the first binary instruction of the sin function.

Like any pointer, it can be passed as a parameter, assigned to a variable, or returned as a

value.

How would we declare a variable, f, that can hold a pointer to a function like sin? The

syntax is a little tricky:

double (*f)(double);

This declares f to be a pointer to a function that expects a double argument and returns a

double value. The assignment:

f = sin;

points f at the same place sin points to, namely the first instruction of the sin function. After

the assignment, we can call f the same way we call sin:

cout << f(0) << endl; // prints sin(0)

Later, we can reassign another function to f:

f = cos;

Now calling f is the same as calling cos:

cout << f(0) << endl; // prints cos(0)

In a sense, treating functions like data adds another degree of flexibility to our programs

beyond polymorphism. We can place a call to f in a program and each time this line is

executed, a different function might be called.

Writing typedef in front of a function pointer declaration:

3-46

3. Frameworks, Kits, and Polymorphism

typedef double (*f)(double);

declares f not to be the name of a pointer to a function, but rather a name for the type of all

pointers to functions. Let's change the name of the type to something a bit grander:

typedef double (*Fun)(double);

We can now declare and assign f using the syntax:

Fun f = sin;

If we reexamine the factory method parameter lists, we see that names have been

introduced for the various types of message processing functions:

makeProducer(ProducerProc f, Pipe* op);makeTransformer(Pipe* ip, TransformerProc f, Pipe* op);makeTester(Pipe* ip, TesterProc f, Pipe* op);makeConsumer(Pipe* ip, ConsumerProc f);

These names are introduced by typedefs that occur at the beginning of the PipelineKit

declaration:

template <class Msg>class PipelineKit{

// function pointer types:typedef Msg (*TransformerProc)(Msg);typedef bool (*TesterProc)(Msg);typedef void (*ConsumerProc)(Msg);typedef bool (*ProducerProc)(Msg&);// etc.

};

Take a moment to review these types. A TransformerProc expects a message as input and

returns a transformed message as output. A TesterProc expects a message as input and

returns true if the message passes the test, and false, otherwise. A ConsumerProc expects a

message as input, and returns nothing, because it has consumed the message. A

ProducerProc expects a reference to a message variable as input. This is a place where it

can store the message it produces. If successful, it returns true, otherwise it returns false.

3-47

Pattern Oriented Programming with C++/Pearce

Factory Methods

The PipelineKit template begins with private typedefs and inner class declarations. The

actual bodies of the inner classes appear separately to make things more readable. The

public part of the kit includes static factory methods for creating pipes and filters:

template <class Msg>class PipelineKit{

// inner typespublic:

typedef Pipe *PipePtr;typedef Filter *FilterPtr;// factory methods, ip = input pipe, op = output pipestatic FilterPtr makeProducer(ProducerProc f, Pipe* op){

return new Producer(f, op);}static FilterPtr makeTransformer(Pipe* ip,

TransformerProc f, Pipe* op){

return new Transformer(ip, f, op);}static FilterPtr makeTester(Pipe* ip, TesterProc f, Pipe* op){

return new Tester(ip, f, op);}static FilterPtr makeConsumer(Pipe* ip, ConsumerProc f){

return new Consumer(ip, f);}PipePtr makePipe() { return new Pipe(); }

};

Pipes

Initially, we defined a pipe as a message queue. This is only necessary for pipelines that

contain active filters, where one filter may write several messages to its out pipe before the

downstream filter has finished processing its current message. Because our filters are

passive, and because our pipes will notify their readers each time a message is written to

them, it won't be possible for several messages to be written before a single message is

read. Therefore, a pipe only needs to store a single message, which is replaced each time

write() is called:

3-48

3. Frameworks, Kits, and Polymorphism

class Pipe: public Publisher{public:

Msg read() { return message; }void write(Msg val){

message = val;notify(); // data driven

}private:

Msg message; // the stored message};

Filters

The filter base class maintains pointers to its input and output pipes. Declaring them to be

protected makes them visible to the Filter-derived classes. For producers, the input pipe

will be 0, for consumers, the output pipe will be 0. The start() function will be redefined in

the Producer class:

class Filter: public Subscriber{public:

Filter(Pipe *ip = 0, Pipe* op = 0){

inPipe = ip;outPipe = op;if (inPipe) inPipe->Subscribe(this);

}virtual ~Filter(){

if (inPipe) inPipe->unsubscribe(this);}virtual void start() // redefine in Producer{

throw PipeLineError("This is not a producer.\n");}

protected:Pipe *inPipe, *outPipe;

};

The destructor unsubscribes a data-driven filter from its input pipe. Subscribing to the input

pipe will be done in the derived classes that have input pipes.

Notice that Pipe does not implement the pure virtual update() function inherited from

Subscriber. This makes Filter an abstract class. We will not be able to create any generic

filters. Instead, update() must be implemented by classes derived from filter.

3-49

Pattern Oriented Programming with C++/Pearce

Transformers

A transformer implements update() by reading a message from its inherited input pipe,

transforming the message using the function pointer passed to its constructor, then writing

the transformed message to its inherited output pipe:

class Transformer: public Filter{public:

Transformer(Pipe* ip, TransformerProc f, Pipe* op): Filter(ip, op){

transform = f;}void update(Publisher* p, void* what){

Msg val = inPipe->read();val = transform(val);outPipe->write(val);

}private:

TransformerProc transform;}; // Transformer

Testers

A tester implements update() by reading a message from its inherited input pipe, then

writing the message to its inherited output pipe if it passes a test given by the function

pointer passed to the constructor:

class Tester: public Filter{public:

Tester(Pipe* ip, TesterProc f, Pipe* op): Filter(ip, op){

test = f;}void update(Publisher* p, void* what){

Msg val = inPipe->read();if (test(val)) outPipe->write(val);

}private:

TesterProc test;}; // Tester

3-50

3. Frameworks, Kits, and Polymorphism

Consumers

A consumer implements update() by reading a message from its inherited input pipe, then

passing the message to a consumer function pointer specified by the constructor:

class Consumer: public Filter{public:

Consumer(Pipe* ip, ConsumerProc f): Filter(ip, 0){

consume = f;}void update(Publisher* p, void* what){

Msg val = inPipe->read();consume(val);

}private:

ConsumerProc consume;}; // Consumer

Producers

A data-driven producer implements update() as a no-op. This is because the producer has

no input pipe that is subscribes to. A producer must redefine the inherited start() function.

The start() function is the engine that drives a data-driven pipeline. It repeatedly calls the

producer function initialized by the constructor. By assumption, this function either stores a

new message in the val variable and returns true (recall that the producer's parameter is a

reference), or it returns false. If it returns true, the message in val is written to the inherited

output pipe and the loop repeats. Otherwise the start() function terminates. This terminates

the flow of messages through the pipeline. If an exception is thrown anywhere in the

pipeline, it will be caught by the producer:

3-51

Pattern Oriented Programming with C++/Pearce

class Producer: public Filter{public:

Producer(ProducerProc f, Pipe* op): Filter(0, op){

produce = f;}void update(Publisher* p, void* what) {} // no opvoid start(){

bool more = true; Msg val;while(more)

try{

more = produce(val);if (more) outPipe->write(val);

}catch(PipeLineError e){

cerr << e.what() << endl;}catch(...){

cerr << "Unknown error! Shutting pipeline down\n";more = false;

}}

private:ProducerProc produce;

};

Programming Notes

Programming Note 3.1: Implementing FUN

The FUN adventure game framework consists of the following files and classes:

cast.h & cast.cpp (Character, Hero, Monster)place.h & place.cpp (Room, Maze)ui.h & ui.cpp (GameFactory, GameConsole)defs.h (Direction & constants)

defs.h

Constants that control the play of the game are stored in defs.h, which is included by most

of the other header files in FUN:

3-52

3. Frameworks, Kits, and Polymorphism

/* * File: pop\chapter3\defs.h * Programmer: Pearce * Copyright (c): 2000, all rights reserved. */ #ifndef DEFS_H#define DEFS_H

#include "..\util\console.h" // for ui#include <cmath> // for rand() & srand()#include <time.h> // for time()

// maze directions:enum Direction {N, E, S, W};// maze size:#define ROWS 5#define COLS 5#define KEYS ROWS * COLS // assumes 1 key/room// some standard error message suffixes:#define DEAD " is dead!"#define ESCAPED " has escaped!"#define LOST " is lost!"// minimum maximum damge inflicted:#define DAMAGE_CONTROL 10#define ENERGY 5 // energy bowls/room#define LOCKS 5 // minimum # of locks/room// collect keys only when # monsters <#define MIN_MONSTERS 2

#endif

ui.h

In addition to the abstract GameFactory class defined earlier, ui.h declares the game

console class, which is derived from the Console class. (The Console class was described

in Programming Note 2.2 and is one of our utility classes.) In this case, the game console's

context is the maze and a hero:

3-53

Pattern Oriented Programming with C++/Pearce

class GameConsole: public Console{public:

GameConsole(GameFactory* gf = 0) {

myHero = gf? gf->makeHero(): 0;myMaze = gf? gf->makeMaze(): 0;myHero->setRoom(myMaze->getRoom(0, 0));

}private:

string execute(const string& cmmd);void help();Hero* myHero;Maze* myMaze;

};

ui.cpp

As required by the Console base class, we must provide an implementation for the

execute() function:

string GameConsole::execute(const string& cmmd){

if (cmmd == "move") {

string dir;cin >> dir;if (dir == "N" || dir == "n") myHero->move(N);else if (dir == "S" || dir == "s") myHero->move(S);else if (dir == "E" || dir == "e") myHero->move(E);else if (dir == "W" || dir == "w") myHero->move(W);else throw AppError("Illegal direction");

}else if (cmmd == "drink") myHero->invigorate();else if (cmmd == "scan") myHero->getRoom()->describe();else if (cmmd == "check") myHero->describe();else if (cmmd == "fight") myHero->fight();else if (cmmd == "grab") myHero->takeKeys();else if (cmmd == "map") myMaze->describe();else if (cmmd == "super") myHero->setEnergy(10000);else throw AppError(unrecognized + cmmd);return "done";

}

We can also redefine the help() function to display the command menu.

place.cpp

The definition and initialization of the static member variable from the Maze class is placed

near the top of place.cpp:

3-54

3. Frameworks, Kits, and Polymorphism

Maze* Maze::theMaze = 0;

The rest of the file contains implementations of the Maze and Room member functions. For

example, here's the function that adds rooms to the maze:

void Maze::add(Room* room, int i, int j){

if (i < 0 || ROWS <= i || j < 0 || COLS <= j)throw AppError("room index out of range");

rooms[i][j] = room;if (0 < j && rooms[i][j - 1]) {

room->setNeighbor(W, rooms[i][j - 1]);rooms[i][j - 1]->setNeighbor(E, room);

}if (j + 1 < COLS && rooms[i][j + 1]) {

room->setNeighbor(E, rooms[i][j + 1]);rooms[i][j + 1]->setNeighbor(W, room);

}if (0 < i && rooms[i - 1][j]){

room->setNeighbor(N, rooms[i - 1][j]);rooms[i - 1][j]->setNeighbor(S, room);

}if (i + 1 < ROWS && rooms[i + 1][j]){

room->setNeighbor(S, rooms[i + 1][j]);rooms[i + 1][j]->setNeighbor(N, room);

}}

character.cpp

Here is how the hero moves to another room:

void Hero::move(Direction d){

if (!energy) throw AppError(name + DEAD);if (!room) throw AppError(name + LOST);if (!room->getNeighbor(d)) throw AppError("That door is locked");room = room->getNeighbor(d);room->enter(this);

}

The invigorate() function is called when the hero drinks energy bowls:

3-55

Pattern Oriented Programming with C++/Pearce

void Hero::invigorate(){

if (!energy) throw AppError(name + DEAD);if (!room) throw AppError(name + LOST);energy = min(energy + room->takeEnergy(this), 100);

}

If the hero wants to, he can pick a fight with the monsters in his current room:

void Hero::fight(){

if (!energy) throw AppError(name + DEAD);if (!room) throw AppError(name + LOST);room->attack(this);

}

Other character.cpp functions are relatively straight forward and are left to the reader.

Problems

Problem 3.1

Create and test a template version of the UIToolKit.

Problem 3.2

Complete the FUN framework. (A few function definitions are missing.) Test FUN by

using it to create and play an adventure game set in outer space.

3.1 Problem: A Two Player Game Framework

Most two player card and board games follow the same basic pattern:

Player 1 and player 2 alternate turns. Usually, points are scored on each turn. When one player accumulates enough points, he wins and the game is over.

We can capture this pattern in a generic algorithm, which can be the basis for a mini

framework for developing two player computer games. The framework allows tournaments

consisting of multiple games, so it keeps track of the total number of player 1 and player 2

wins, which it displays with the showStats() function. The generic algorithm is play()5:

5 We follow the MacApp convention of prefixing functions that must be defined in the derived class by "do".

3-56

3. Frameworks, Kits, and Polymorphism

void Game::play(){

bool again = true;

while (again){

score1 = score2 = 0;doInitGame(); // reinitialize game environment

while(!doGameOver())try{

cout << "\nPlayer 1's turn ...\n";score1 += doPlayer1();if (!doGameOver()){

cout << "\nPlayer 2's turn ...\n";score2 += doPlayer2();

}}catch(runtime_error e){

cerr << "error: " << e.what() << endl;}

switch (doWinner()) // determine the winner{case 1:

cout << "\nPlayer 1 wins!\n";player1wins++;break;

case 2:cout << "\nPlayer 2 wins!\n";player2wins++;break;

default:cout << "\ndraw\n";

}

showStats(); // print totalsagain = getResponse("Again?"); // declared in utils.cpp

} // while again} // play()

The generic algorithm's while loop allows tournament play. After each game, the total

number of player 1 and player 2 wins is displayed, and the user is prompted for another

game. Actual play of the game is risky, so doPlayer1() and doPlayer2() are called inside of

a try-catch block.

3-57

Pattern Oriented Programming with C++/Pearce

3.1.1.: Tic Tac Toe

Although player 1 and player 2 could both be humans, player 2 is more commonly the

computer. Customize the framework to allow users to play Tic Tac Toe with the computer.

The computer will be player 2. Here's the output produced near the end of one game, player

1 is X, and player 2 is O:

Player 1's turn ...O _ O_ X _X _ _Pick a row -> 2You entered 2Pick a column -> 2You entered 2

Player 2's turn ...O O O_ X _X _ X

Player 2 wins!# of player 1 wins = 0# of player 2 wins = 1Again? (y/n) ->

3.1.2. Problem: Blackjack

Customize the game framework a different way to create a program that allows users to

play Blackjack with the computer. You may not alter the framework.

A game of Blackjack consists of dealing a hand of two random cards to the user, then

printing the value of the hand. The value is the sum of the card values, where:

ACE = 1 or 11TWO = 2etc.NINE = 9TEN = JACK = QUEEN = KING = 10

The user is repeatedly asked if he wants another card until he says no (holds) or until the

value of the hand exceeds 21, in which case the game is over and the user looses.

If the user holds, then the computer deals two random cards to itself and displays the sum.

The computer continues to deal itself a card until the sum of the cards is greater than or

3-58

3. Frameworks, Kits, and Polymorphism

equal to 17. If the sum is greater than 21 or less than the sum of the user's hand, then the

computer looses, if the sums are the same, then the game ends in a draw. Otherwise, the

computer wins.

Hints

Represent a card as a structure consisting of three variables:

struct Card{

Value value;Suit suit;bool dealt; // = true if played

};

Represent suits and values as enumerations:

enum Suit {CLUB, DIAMOND, HEART, SPADE};

enum Value {

ACE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE, TEN, JACK, QUEEN, KING

};

And represent decks as arrays of 52 cards:

typedef Card Deck[52];

Here's a tricky way to initialize a deck:

void init(Deck d){

for(int i = 0; i < 4; i++)for(int j = 0; j < 13; j++){

int n = 13 * i + j; // 0 <= n < 52d[n].suit = Suit(i); // why is explicit cast needed?d[n].value = Value(j); d[n].dealt = false;

}}

Shuffling a deck is just a matter of clearing the dealt flags:

3-59

Pattern Oriented Programming with C++/Pearce

void shuffle(Deck d) // why is value param okay here?{

for(int i = 0; i < 52; i++)d[i].dealt = false;

}

To pick a card, generate a random number between 0 and 51. If the card at that position is

dealt, then increment the number modulo 52 until an undealt card is found:

Card pickCard(Deck d) {

int i = rand() % 52; // don't forget to seed using srand()int j = i;while (d[j].dealt){

j = (j + 1) % 52;if (i == j)

throw runtime_error("No more cards");}d[j].dealt = true;return d[j];

}

3.2. Problem: Using Factory Method Templates

The adventure game framework can also use factory method templates. Assume M, H, and

R are template parameters representing the Monster, Hero, and Robot constructors,

respectively. We can implement most of the factory methods in the game factory template:

template <typename M, typename H, typename R> class Maze; // forward reference

template <typename M, typename H, typename R>class GameFactory{public:

Monster* makeMonster(string nm = "???") { return new M(nm); }Hero* makeHero(string nm = "???") { return new H(nm); }Room* makeRoom() { return new R(); }virtual Maze<M, H, R>* makeMaze() = 0;

};

Finish this implementation of the adventure game framework. Customize the framework to

create two adventure games different from the games created in my example.

3.3. Problem: C++ Review

Is the declaration:

3-60

3. Frameworks, Kits, and Polymorphism

Shape *s1 = new Circle(), *s2 = new Rectangle(), *s3 = new Triangle();

equivalent to the following declaration:

Shape* s1 = new Circle(), s2 = new Rectangle(), s3 = new Triangle();

3.4. Problem

In the example of the singleton pattern, use an instance counter to prove only one instance

is created. Recall that an instance counter is a private static variable that keeps track of the

number of instances a class has at any given moment.

3.5. Problem

How could we make multiple singletons if the default constructor was protected instead of

private?

3.6. Problem

We gave examples of association, creation, and aggregation relationships between a

framework class A, and an abstract class or template parameter B representing a class to be

specified later in a customization. Give a useful example where class A might be derived

from a template parameter B:

template <typename B> class A: public B { ... };

3.7. Problem

Re implement the adventure game framework to allow users to control one hero. For

example, the Hero::defend() function could accept keyboard input to determine how much

damage the hero does to the monster, and the Maze::action() function should allow the hero

to decide which room to enter. You may make any modifications you deem necessary, but

keep the framework general.

3-61

Pattern Oriented Programming with C++/Pearce

3.8. Problem: A Framework for Finite State Machines

Let FSM be the class of all finite state machines:

class FSM { ... };

A finite state machine, M, is a mathematical model of a simple computational device that is

always in one of finitely many states:

q0, q1, ... qmax

We will assume q0 is M's initial state. We will also assume one of M's states is designated

as a final state, qfin. For our purposes it is sufficient to assume states are simply consecutive

integers:

typedef int State;

M has a control function called run(), which expects a string as input and returns a bool:

bool FSM::run(const string& s);

The control function examines each character in s, once, from beginning to end. Examining

a character causes M to change state. The new state is given by a function called next():

/* c = current character

s = current statereturn value = new state

*/ State FSM::next(char c, State s);

If M is in the final state, qfin, after it examines the last character in s, then run() returns true

and we say M accepts s. Otherwise, run() returns false and we say M rejects s6.

M also has a user interface function called test(), which perpetually resets M's current state

to q0, prompts the user for an input string, s, then prints an acceptance message if run(s)

returns true, and a rejection message otherwise.

6 Intuitively, we can think of M's state, qi, as the current content of its RAM, run() is M's CPU (which runs the fetch-execute cycle), s is an input file, and next() is M's current program.

3-62

3. Frameworks, Kits, and Polymorphism

Here's a sample output using a three state machine, M. M is in state qi if it has examined the

character '0' k times, and k % 3 == i. M's final state is q0. Thus, M accepts s if and only if

the number of zeroes in s is divisible by 3:

current state = 0final state = 0enter input string: 011001state = 1state = 1state = 1state = 2state = 0state = 0String acceptedcurrent state = 0final state = 0enter input string: catfishstate = 0state = 0state = 0state = 0state = 0state = 0state = 0String acceptedcurrent state = 0final state = 0enter input string: 00001state = 1state = 2state = 0state = 1state = 1String rejected

Implement a framework for building finite state machines. There are two approaches:

Either next() can be specified in a user-defined derived class, or the user can pass a

function object (i.e., a functor) representing next() to the finite state machine constructor.

Use the first approach for this exercise. Of course run() and next() should throw exceptions

if they get into trouble, and test() should catch these exceptions.

Test your framework by building and testing a finite state machine that accepts all strings

that contain the same number of zeroes as ones.

3-63

Pattern Oriented Programming with C++/Pearce

3.9. Problem

Repeat the last problem, but use the second approach: the user specifies next() by passing a

function object to the FSM constructor. (Function objects were introduced in problem

2.12.).

3.10. Problem: Push Down Automata7

Obviously a finite state machine can be quite powerful, depending on the complexity of its

"program", the next() function. A push down automaton (PDA) is a finite state machine

with a simple next() function and a stack, which can hold an unlimited number of

characters8 but is initially empty:

stack<char> theStack;

Basically, next() is driven by a control table of the form:

Of course the table can have any number of rows. The rows shown are just sample entries.

The first row of the table can be interpreted as follows: if the character on top of the stack

is 'b', then next('a', 0) returns 1, pops 'b' off the stack, pushes 'c' onto the stack, then pushes

'a' onto the stack. The second entry means: if the character on top of the stack is 'c', then

next('a', 1) returns 1, and pops c off the stack. The third entry means: if the stack is empty,

then next('c', 1) returns 2 and doesn't do anything to the stack. (Equivalently, next() pops 'c'

off the stack, then pushes 'c' onto the stack.).

In addition to all of the user-specified states, a PDA has a special dead state, -1. The dead

state may never be a final state, and once a PDA enters the dead state, it's stuck there:

For all x, next(x, -1) = -1

7 See [LEW] for a discussion of the different types of finite state machines.8 Note that unlike the stack, the RAM only has finite capacity = max + 1.

3-64

3. Frameworks, Kits, and Polymorphism

Create a kit for building push down automata by deriving PDA from FSM.

class PDA: public FSM { ... };

Users only need to specify the control table in a formatted text file (you choose the format).

Internally, the control table should be represented as an STL map.

class Configuration { ... }; // current state, char, & topclass Action { ... }; // next state & push stringtypedef map<Configuration, Action> ControlTable;

Test your kit by building a PDA that accepts strings that have a prefix of n zeroes and a

suffix of n ones, where n can be any value.

3.11. Problem: Finite Automata

A Finite Automaton (FA) is a PDA with no stack (hence users only need to specify a three-

column control table). Create a kit for building finite automata by specializing the PDA

class from the last problem:

class FA: public PDA { ... };

Test your kit by building a finite automaton that accepts all strings containing an even

number of substrings of the form "01".

2.11. Problem: Demand Driven Pipeline Project

Repeat the data driven example using demand driven filters. Use the same test driver. The

only difference is that f1->start() is replaced by f4->start().

Obviously filters subscribe to their output pipes instead of their input pipes in a demand

driven pipeline. When a down stream filter reads from its input pipe, the pipe automatically

notifies the upstream filter that data is needed.

Two special problems arise in the demand driven case. First, what happens when data is

demanded from the output pipe of a tester? The tester needs to repeatedly read from its

input pipe until the pipeline is empty or until it receives a data item that passes its test and

can be written to its output pipe.

3-65

Pattern Oriented Programming with C++/Pearce

Second, how does a demand drive pipeline shut down? In the data driven case this was easy

because the producer controlled the while loop in start() and the producer knew when it

was finished producing data. When produce() returned true, the loop control variable was

set to true and the start() function, hence the pipeline, terminated. In a demand-driven

pipeline the consumer controls the while loop in start(). How can the producer signal the

consumer that it's done producing data? The answer is by throwing a ProducerQuitting

exception. This means the while loop in start() must contain a try-catch block:

while(!fail){

try{

???}catch(ProducerQuitting pqe){

fail = true;}

}

Try declaring ProducerQuitting as a derived class from the exception class defined in the

C++ standard library:

class ProducerQuitting: public exception {};

This could also be an inner class of PipelineKit.

2.12. Problem: Using Functors

A functor or function object, is an object that can be called like a function. This is

accomplished by making the function call operator a member function. For example:

class Square{public:

double operator()(double x) { return square(x); }// etc.

};

We can now define some Square functors:

Square f, g, h;

3-66

3. Frameworks, Kits, and Polymorphism

Like ordinary objects, these objects can be passed as parameters, returned as values, or

assigned to variables. But they can also be called like functions:

cout << g(2) * h(3) << endl; // prints 36

The C++ standard library supplies some predefined functor templates (you'll need to

include <functional>). For example:

pointer_to_unary_function<double, double> s(square);

creates a functor called s that implements operator() using the square function, just like f, g,

and h:

cout << s(7) <, endl; // prints 49

Using functors instead of function pointers makes our programs a bit more object oriented.

Re implement the pipeline toolkit to use functors to process messages instead of function

pointers.

Problem

Implement the data-driven Pipeline Toolkit. Test your implementation by constructing a

pipeline that reads a text file through standard input, eliminates all punctuation marks, and

writes the result to a file through standard output.

Problem

We can imitate LISP using the algorithms from the standard C++ library. Assume isEven()

and square() are ordinary global functions:

bool isEven(int x) { return x % 2 == 0; }int square(int x) { return x * x; }

The declarations of remove_if() and transform() are declared in the <algorithm> header

file:

3-67

Pattern Oriented Programming with C++/Pearce

list<int> filterEvens(list<int>& nums){

nums.erase(remove_if(nums.begin(), nums.end(), isEven), nums.end());

return nums;}

list<int> mapSquare(list<int>& nums){

transform(nums.begin(), nums.end(), nums.begin(), square);return nums;

}

3-68