We'd like to model an assembly line. Not much of a model, really;
the example is mostly meant to present some common C++ constructs that
you might see and use in implementing object-oriented designs.
Parts
The intent is ultimately to model a full procdution line. In a
production line, basic Parts are put together to make more complex
parts, and yet more complex parts, and so on. In this set of
examples, we only represent Parts in terms of their name, color, and
weight. Time for a quick (freehand) drawing:
Preliminaries
To start out, we need to choose representation types for the three
primary attributes, names, weights, and colors. To keep things simple,
we'll just use basic C types:
... and use just a few standard C and C++ library features:
#include
extern "C" char* strdup(const char*);
Classes
The best way to represent Parts turns out to be a well-known design
pattern, Composite from the Design
Patterns book.
Here we start with a mostly-abstract base class, Part. Class Part
defines the general interface for Parts, but not it's
implementation. Well, not quite. Because we'd like to stick to
a simple representation type here for Part names, no matter how
they are otherwise composed, we actually hard-wire the code
for dealing with names. In fact we really hard-wire
it -- the access method isn't even declared as virtual, which
means that it cannot be safely overridden.
Also, for now, we need a simple way to print out information
about parts.
class Part {
public:
virtual Color color() = 0;
virtual Weight weight() = 0;
virtual void print() = 0;
Name name() const { return name_; }
protected:
Part(Name nm) :name_(strdup(nm)) {}
virtual ~Part() {}
private:
Name name_;
Part(const Part&) {}
void operator=(const Part&) {}
};
Notes:
For illustrative purposes this and all other classes have all
methods defined entirely in their declarations. In practice,
you can't normally get away with this.
Note the convention of appending internal slot names with
underscores.
The virtual ... = 0 stuff says that each subclass defines
the body differently, and that there is no default implementation.
Making the constructor protected means that it can only
be invoked from other subclasses.
Making the copy constructorPart(const Part&)
and assignment operator private disables them. In general,
we don't know how to make one Part from another. Similar
tactics should be applied to nearly all other classes in
this set of examples, but are sometimes elided for brevity.
A BasicPart is our sole subclass for representing primitive parts.
In a better example, there would probably be several different subclasses,
each used for a particular kind of primitive part:
class BasicPart : public Part {
public:
BasicPart(Name nm, Color color, Weight weight)
: Part(nm), color_(color), weight_(weight) {}
Color color() { return color_; }
Weight weight() { return weight_; }
void print() {
cout << name() << "(";
cout << colorNames[int(color())] << ", ";
cout << weight() << ")";
}
private:
Color color_;
Weight weight_;
};
The strategy for representing a Composite part is to internally
house some kind of List object that holds the component Parts.
For all that a Composite knows, each of its Parts might iteself
be a Composite. The Composite preserves the same interface
as any other kind of Part, but performs all operations in the
aggregate. So, for example weight() is computed by summing
the weights of all parts.
To implement Composite, we need a collection class of some
sort. It would be pretty stupid to write one from scratch here.
C++ libraries always contain such things. I used one that I
had hanging around from a CORBA application I once wrote.
This class, PtrSeq is a template that can contain elements
of any kind of pointer. It uses array semantics,
which means that elements can be traversed and used pretty much
like those in basic C arrays. Here is a simplified version of
the declaration for it:
template class PtrSeq { // PTR must be a pointer type!
public:
PtrSeq();
PtrSeq(unsigned int initialCapacity);
PtrSeq(PTR* d, unsigned long l, unsigned long c, bool copydata);
PtrSeq(const PtrSeq& a);
PtrSeq& operator =(const PtrSeq& a);
~PtrSeq() {}
unsigned long length() const;
const PTR at(unsigned long i) const;
PTR at(unsigned long i);
const PTR operator[] (unsigned long i) const;
PTR operator[] (unsigned long i);
long index(const PTR p) const;
void appendIfAbsent(const PTR p);
void append(const PTR p);
void prepend(const PTR p);
void insert(const PTR p, unsigned long i);
void removeFirstOccurrence(const PTR p);
void removeAt(unsigned long i);
void removeAll();
bool invCheck();
};
To reduce awkardness a bit, we rename the version specialized
on pointers to Parts as:
typedef PtrSeq PartSeq;
... And CompositePart is now pretty easy:
class CompositePart : public Part {
public:
CompositePart(Name nm) :Part(nm) {}
void addPart(Part* p) { parts_.append(p); }
void delPart(Part* p) { parts_.removeFirstOccurrence(p); }
Weight weight() {
Weight w = 0;
for (unsigned int i = 0; i < parts_.length(); ++i)
w += parts_[i]->weight();
return w;
}
Color color() {
Color c = unknownColor;
for (unsigned int i = 0; i < parts_.length(); ++i) {
Color cc = parts_[i]->color();
if (c == unknownColor) c = cc;
else if (c != cc) c = mixedColor;
}
return c;
}
void print() {
cout << name() << "[";
for (unsigned int i = 0; i < parts_.length(); ++i) {
parts_[i]->print();
}
cout << "]";
}
private:
PartSeq parts_;
};
Notes:
The code for color(), like many things in this example,
is pretty arbitrary -- if all are the same color,
it reports that color; otherwise `mixed'
CompositePart also defines two operations that aren't available
for Parts in general -- these add or remove a component.
Stages
An assembly line consists of lots of different kinds of objects
that create, transform, and move Parts. The first step in defining the
associated classes is to standardize a set of interfaces for the kinds
of operations that can occur at each stage. In turn, this requires us
to figure out what those operations are. These are the beginnings of
a the creation of a general-purpose application framework for
assembly-line models in general, not just the particular models used
in this set of examples.
To figure out these interfaces, we need to settle on a flow
policy. A quick digression on flow policies: In most process
control systems or models (as well as a lot of other kinds of
applications), you have to choose a small set of policies that govern
which objects invoke operations in others. This is for example, one of
the central design patterns used in the construction of Avionics Control
Systems. In general you have to decide upon some small combination
of push, pull, or shared resource protocols:
In this set of examples, we do something a little weird: we lay
out capabilities so that the assembly line can work in either
push-mode or pull-mode. (No shared-resource mode though). In the push
version, a series of BasicParts are continually produced, and sent on
to downstream stages via method bool put(Part*). In pull-mode,
whenever a final product is required, the consumer invokes
Part* take(), that in turn invokes take's from all
of the associated upstream stages. The structure and implementations
in the current examples suffice for demostration purposes, but if
this were targeted for a distributed implementation in which
different stages resided on different nodes, we'd have a lot more
control flow issues and mechanics to deal with.
Interfaces
We need a collection of interfaces that cover basic operations.
In C++, this is an exercise in idiomatic techniques for mixing and
matching pure abstract classes using virtual public inheritance.
To start with, we need to separate Producer-side and
Consumer-side capablities. Some stages only produce, some
only consume, and some do both. Every Producer is able to
put a Part to its attached Consumer. (As we'll see, there
may be more than one Consumer per Producer.) Conversely, each Consumer
must be able to take a Part from its attached
Producer(s). A Pipe can serve as both a Producer and a Consumer:
Conceptually, you might think of these classes all as subclasses
of abstract class Stage. But there aren't
any operations that apply to every stage regardless of
it being a Producer or Consumer, so the class wouldn't
have any body, so it isn't listed. If a GUI animation
is attached to these classes, we might change our minds,
and list a base class having a display method.
These examples use the newly built-in type bool.
But the code is written in a way that if you don't have
a compiler supporting bool, everything should
still work if you just typedef int bool .
The predeclaration of class Producer before the real declaration
of Consumer is needed to keep the C++ compiler happy.
For the sake of being able to write simple example code,
take is parameterized with a Color argument
describing the desired Color of the Part being taken.
The intended semantics are that if a Part of the
requested color isn't available, a null pointer is
returned. The special value unknownColor is taken
as a wildcard, matching any color.
The put method returns a bool as a success
indicator. A put can fail, for example if there
are no Consumers attached to a Producer.
The attach/detach methods return bool as a success flag.
It may be that a Stage cannot accept a new attachment,
or that a detach request doesn't correspond to an
existing attachment. We do NOT associate any code
mechanics with these yet though, since there are several
ways to represent connections, according to the type of Stage.
The connect and disconnect procedures
are top-level utilities, unattached to any particular
class. They just ensure that links are made and broken
consistently. In fact, it might be a good idea to disable
direct access to the attach/detach methods, and require that
all invocations go through these helpers. Further, this code
is an example of a something that should be written in the
style of a transaction: Either both attaches should
occur or neither should occur. It would be a bad idea if only
the first one held. This could be accomplished via exceptions.
Source
There are two other special interfaces, for end-points.
A Source is a special kind of Producer that has
an externally callable method produce that initiates
a series of puts.
class Source : public virtual Producer {
public:
Source() {}
virtual bool produce() = 0;
};
Sink
A Sink is a special Consumer with a consume method that
initiates a series of takes:
class Sink : public virtual Consumer {
public:
Sink() {}
virtual bool consume() = 0;
};
Scaffolding for Code Inheritance
These interfaces are fine, but don't set up any opportunity for
exploiting code inheritance to share implementations of basic
functionality. For example, we'd like to write code exactly once for
all Producers that are linked to a single Consumer, as represented by
a Consumer* member variable. And similarly for Consumers
attached to single Producers. The following classes set up these
mechanisms. These classes are still not themselves instantiable since
they don't define put or take, they just nail down the attach/detach
mechanics:
class SinglyAttachedConsumer: public virtual Consumer {
protected:
Producer* producer_;
SinglyAttachedConsumer() :producer_(0) {}
public:
bool attachProducer(Producer* s) {
if (producer_ == 0) { producer_ = s; return 1; }
else return 0;
}
bool detachProducer(Producer* s) {
if (producer_ == s) { producer_ = 0; return 1;}
else return 0;
}
};
class SinglyAttachedProducer : public virtual Producer {
protected:
Consumer* consumer_;
SinglyAttachedProducer() :consumer_(0) {}
public:
bool attachConsumer(Consumer* s) {
if (consumer_ == 0) { consumer_ = s; return 1; }
else return 0;
}
bool detachConsumer(Consumer* s) {
if (consumer_ == s) { consumer_ = 0; return 1;}
else return 0;
}
};
class SinglyAttachedPipe : public virtual Pipe,
public virtual SinglyAttachedProducer,
public virtual SinglyAttachedConsumer {
public:
SinglyAttachedPipe() {}
};
An alternative set of classes lays down mechanics for Producers
that can be attached to multiple Consumers and vice versa. To
accomplish this we again rely on the PtrSeq class:
We're ready to start writing some classes that can actually be
instantiated. We'll start with two that we might need in just about
any kind of application using Producer and Consumer classes.
Buffer
The first is a Buffer, that holds some number of Parts
on their way from a Producer to a Consumer. In a real assembly
line, these buffers would correspond to Piles of Parts
awaiting processing at the next stage. (I originally called this
class Pile, but it sounded funny...):
To implement it, we again rely on our standard collection class.
The mechanics are about what you'd expect: On a put, if the object
can't propagate to the next stage, the Part is placed in an internal
buffer. On a take, the buffer is cleared before pulling more parts
from any producers. Since the buffer has a fixed capacity, the system
can still stall out when the buffer fills up.
class Buffer : public SinglyAttachedPipe {
private:
unsigned int cap_;
PartSeq buffer_;
public:
Buffer(unsigned int cap) :cap_(cap), buffer_(cap) {}
unsigned int capacity() { return cap_; }
bool put(Part* p) {
if (consumer_ != 0 && consumer_->put(p))
return 1;
else if (buffer_.length() < cap_) {
buffer_.append(p);
return 1;
}
else
return 0;
}
Part* take(Color c) {
for (unsigned int i = 0; i < buffer_.length(); ++i) {
if (c == unknownColor || buffer_[i]->color() == c) {
Part* p = buffer_[i];
buffer_.removeAt(i);
return p;
}
}
if (producer_ != 0)
return producer_->take(c);
else
return 0;
}
};
Conduit
The second utility class is a Conduit. A Conduit is a
connector with potentially multiple connections on both the
Producer and the Consumer side:
On the put side we implement a Conduit as a
round-robin demultiplexer, scanning through all available
sources. Similarly in reverse on the output side:
class Conduit : public virtual Pipe,
public virtual MultiplyAttachedProducer,
public virtual MultiplyAttachedConsumer {
private:
unsigned int lastConsumer_; // roving indices
unsigned int lastProducer_;
public:
Conduit() :lastConsumer_(0), lastProducer_(0) {}
bool put(Part* p) {
int n = consumers_.length();
if (n > 0) { // circularly sweep
if (lastConsumer_ >= n) lastConsumer_ = 0;
int i = lastConsumer_;
for (;;) {
if (consumers_.at(i)->put(p)) {
lastConsumer_ = i + 1;
return 1;
}
else {
++i;
if (i >= n) i = 0;
if (i == lastConsumer_) break;
}
}
}
return 0;
}
Part* take(Color wanted) {
int n = producers_.length();
if (n > 0) { // circularly sweep
if (lastProducer_ >= n) lastProducer_ = 0;
int i = lastProducer_;
for (;;) {
Part* p = producers_.at(i)->take(wanted);
if (p != 0) {
lastProducer_ = i + 1;
return p;
}
else {
++i;
if (i >= n) i = 0;
if (i == lastProducer_) break;
}
}
}
return 0;
}
};
Application Classes
We now have the infrastructure together to build some classes for this
particular application. All of the following classes are the simplest
ones I could think of to get this rolling.
BasicPartSource
First, a special Source subclass that just builds BasicParts.
This class also illustrates an application of the Factory
Design Pattern. A BasicPartSource encapsulates all of the information
needed to construct a certain kind of Part:
class BasicPartSource : public virtual Source,
public virtual SinglyAttachedProducer {
public:
BasicPartSource(Name name, Color color, Weight weight)
: name_(name), color_(color), weight_(weight) {}
Part* take(Color wanted) {
return (wanted == color_ || wanted == unknownColor)? newPart() : 0; }
bool produce() { return consumer_->put(newPart()); }
private:
Name name_;
Color color_;
Weight weight_;
Part* newPart() { return new BasicPart(name_, color_, weight_); }
};
Printer
The only Sink objects in this set of examples are Printers.
Their consume action just invokes print() for the constructed
Part it receives from previous stages. To help decode output,
each Printer is assigned a ``port number' that it uses to
prefix each line of output:
We have only one kind of Part Assembler. In a more serious set of
examples there might be many different kinds. Here, the Assemblers
need to be preset with the number of parts of each different kind
Color they need to produce their composite output. The mechanics are
arbitrary and mostly boring. (The main tricky part of this code is
coping with the possibility that a previous put request failed
for a constructed Part. In this case the Assembler must stall
until it can successfully get rid of the one it is holding.)
class Assembler : public SinglyAttachedPipe {
private:
typedef int PerColorQuantityArray[NColors];
Name productName_;
PerColorQuantityArray need_;
PerColorQuantityArray have_;
PartSeq parts_;
CompositePart* assembled_;
protected:
bool enoughParts() {
for (int i = 0; i < NColors; ++i) if (have_[i] < need_[i]) return 0;
return 1;
}
void addPart(Part* p) {
if (p != 0) {
parts_.append(p);
have_[int(p->color())]++;
}
}
void delPart(Part* p) {
if (p != 0) {
have_[int(p->color())]--;
parts_.removeFirstOccurrence(p);
}
}
bool canUse(Part* p) {
int cindex = int(p->color());
return have_[cindex] < need_[cindex];
}
bool holdingAssembled() { return assembled_ != 0; }
bool tryPut() {
if (assembled_ != 0 && consumer_ != 0 && consumer_->put(assembled_)) {
assembled_ = 0;
return 1;
}
return 0;
}
virtual void assemble() {
if (!enoughParts() || holdingAssembled()) return;
assembled_ = new CompositePart(productName_);
for (int i = 0; i < NColors; ++i) {
for (int j = 0; j < need_[i]; ++j) {
for (unsigned int k = 0; k < parts_.length(); k++) {
Part* p = parts_[k];
if (int(p->color()) == i) {
assembled_->addPart(p);
delPart(p);
}
}
}
}
}
public:
Assembler(Name productName)
: productName_(productName), assembled_(0) {
for (int i = 0; i < NColors; ++i) need_[i] = have_[i] = 0;
}
void setNumberNeeded(Color c, int quantity) { need_[int(c)] = quantity; }
int numberNeeded(Color c) { return need_[int(c)]; }
bool put(Part* p) {
if (!canUse(p) || (holdingAssembled() && !tryPut())) return 0;
addPart(p);
if (enoughParts()) {
assemble();
tryPut();
}
return 1;
}
Part* take(Color wanted) {
if (!holdingAssembled()) {
for (int i = 0; i < NColors; ++i) {
while (have_[i] < need_[i]) {
Part* p = producer_->take(Color(i));
if (p == 0) return 0;
else addPart(p);
}
}
assemble();
}
if (holdingAssembled() &&
(assembled_->color() == wanted || wanted == unknownColor)) {
Part* p = assembled_;
assembled_ = 0;
return p;
}
else
return 0;
}
};
Putting it All Together
An AssemblyLine object may be constructed by building any number
of any of these kinds of Stages, connected together in any way. Each
system will be different. In a more ambitious example, we might want
to put together an interactive configuration and scripting tool that
selects and attaches components. But for now, we rely on a simple
Facade-style class that can be used as the basis for lots of other
special ones.
The base interface just has methods for running the system
for a given number of steps in either push-mode or pull-mode.
(Mixtures aren't supported here.)