An Assembly Line Example

by Doug Lea.

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:
typedef const char* Name;
typedef float Weight;

enum Color { 
  red, orange, yellow, green, blue, violet, 
  white, black, mixedColor, unknownColor 
};


In an even slightly better example, we'd probably use a String class for Names.

Also, while we are at it, some miscellaneous stuff so we can print color names ...


static const unsigned int NColors = 10;

const char* colorNames[NColors] = {
  "red", "orange", "yellow", "green", "blue", "violet",
  "white", "black", "mixed", "unknown"
};

... 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:
  1. 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.
  2. Note the convention of appending internal slot names with underscores.
  3. The virtual ... = 0 stuff says that each subclass defines the body differently, and that there is no default implementation.
  4. Making the constructor protected means that it can only be invoked from other subclasses.
  5. Making the copy constructor Part(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 get the real thing, you need PtrSeq.h, VSSeq.h, and VSSeq.cc.

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:
  1. 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'
  2. 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:

The corresponding C++ declarations are:


class Producer;

class Consumer {
public:
  Consumer() {}
  virtual ~Consumer() {}
  virtual bool put(Part* p) = 0;
  virtual bool attachProducer(Producer* s) = 0;
  virtual bool detachProducer(Producer* s) = 0;
};

class Producer  {
public:
  Producer() {}
  virtual ~Producer() {}
  virtual Part* take(Color wanted) = 0;
  virtual bool attachConsumer(Consumer* s) = 0;
  virtual bool detachConsumer(Consumer* s) = 0;
};


class Pipe : public virtual Producer, public virtual Consumer {
public:
  Pipe() {}
};


bool connect(Producer* src, Consumer* snk) {
  return src->attachConsumer(snk) && snk->attachProducer(src);
}

bool disconnect(Producer* src, Consumer* snk) {
  return src->detachConsumer(snk) && snk->detachProducer(src);
}

Notes:
  1. 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.
  2. 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 .
  3. The predeclaration of class Producer before the real declaration of Consumer is needed to keep the C++ compiler happy.
  4. 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.
  5. 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.
  6. 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.
  7. 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:

typedef PtrSeq ProducerSeq;
typedef PtrSeq ConsumerSeq;

class MultiplyAttachedConsumer : public virtual Consumer {
protected:
  ProducerSeq producers_;
public:
  MultiplyAttachedConsumer() {}

  bool attachProducer(Producer* s) { 
    producers_.appendIfAbsent(s); return 1;
  }

  bool detachProducer(Producer* s) { 
    producers_.removeFirstOccurrence(s); return 1;
  }
};



class MultiplyAttachedProducer : public virtual Producer {
protected:
  ConsumerSeq   consumers_;
public:
  MultiplyAttachedProducer() {}

  bool attachConsumer(Consumer* s)     { 
    consumers_.appendIfAbsent(s); return 1; 
  }

  bool detachConsumer(Consumer* s)     { 
    consumers_.removeFirstOccurrence(s); return 1; 
  }

};

General-Purpose Classes

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:

class Printer : public virtual Sink, public virtual SinglyAttachedConsumer {
public:
  Printer(int portNumber) :number_(portNumber) {}
 
  bool put(Part* p) { doPrint(p); return 1; }
  bool consume() {
    Part* p = producer_->take(unknownColor);
    if (p != 0) { doPrint(p); return 1;  }
    else return 0;
  }

private:
  int     number_; 
  
  void doPrint(Part* p) {
    cout << "S" << number_ << ":";
    p->print();
    cout << "\n";
  }

};

Assembler

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


class AssemblyLine {
public:
  AssemblyLine() {}
  virtual void push(int nsteps) = 0;
  virtual void pull(int nsteps) = 0;
};

Configuration

For illustrative purposes, let's put together a special subclass that builds a particular configuration within it's constructor. How about:

Implemented as:

typedef PtrSeq SourceSeq;
typedef PtrSeq SinkSeq;

class DemoAssemblyLine1 : public AssemblyLine {
  SourceSeq sources_;
  SinkSeq sinks_;
public:

  void push(int nsteps) {
    for (int i = 0; i < nsteps; ++i) {
      for (unsigned int j = 0; j < sources_.length(); ++j)
        sources_[j]->produce();
    }
  }

  void pull(int nsteps) {
    for (int i = 0; i < nsteps; ++i) {
      for (unsigned int j = 0; j < sinks_.length(); ++j)
        sinks_[j]->consume();
    }
  }

  DemoAssemblyLine1() {

    sources_.append(new BasicPartSource("P1", red, 1.0));
    sources_.append(new BasicPartSource("P2", red, 3.0));
    sources_.append(new BasicPartSource("P3", red, 5.0));
    sources_.append(new BasicPartSource("P4", green, 2.0));
    sources_.append(new BasicPartSource("P5", green, 4.0));

    Conduit* c1 = new Conduit;
    Conduit* c2 = new Conduit;

    Buffer* b1 = new Buffer(10);
    Buffer* b2 = new Buffer(3);
    
    Assembler* a1 = new Assembler("A1");
    a1->setNumberNeeded(red, 4);

    Assembler* a2 = new Assembler("A2");
    a2->setNumberNeeded(green, 3);

    Assembler* a3 = new Assembler("A3");
    a3->setNumberNeeded(red, 2);
    a3->setNumberNeeded(green, 1);
    
    Printer* o1 = new Printer(1);
    sinks_.append(o1);

    for (int i = 0; i < sources_.length(); ++i) 
      connect(sources_[i], c1);

    connect(c1, b1);
    connect(c1, b2);
    connect(b1, a1);
    connect(b2, a2);
    connect(a1, c2);
    connect(a2, c2);
    connect(c2, a3);
    connect(a3, o1);

  }

};

Execution

To run this version, we just need to set up a trivial main. For example:


void main() {
  AssemblyLine* p = new DemoAssemblyLine1;
  p->push(100);
}


Last update Thu Apr 13 17:03:03 1995 Doug Lea (dl at gee)