This example illustrates the design and implementation of push-based flow systems via an assembly line applet that builds series of ``paintings'' in a style vaguely reminiscent of Piet Mondrian and Mark Rothko.
Box
, where every Box
has a color and a size,
can display itself when asked, and can be made to deeply clone
(duplicate
) itself. The color mechanics are
default-implemented. Others are left abstract, defined differently in
different subclasses:
The overall theme of this example is to start off with sources that
produce simple basic boxes, and then push them through stages that
paint, join, flip, and embed them to form the
paintings. BasicBoxes
are the raw material:
Two fancier kinds of boxes can be made by joining side-by-side two
existing boxes, adding a line-based border surrounding them. Joined
boxes can also flip themselves. All this can be done in either of two
ways, horizontally or vertically. The two resulting classes can be
made subclasses of JoinedPair
to allow sharing of some
common code:
JoinedPair.java HorizontallyJoinedPair.java VerticallyJoinedPair.java
The final kind of fancy box wraps one Box
within a border:
BasicBoxSource
: Create a new BasicBox
.
Painter
: Change the color of any kind of
Box
.
Flipper
: Flip (up-down or left-right) a
JoinedPair
.
HorizontalJoiner
: Combine two Boxes
left-right, creating a new composite.
VerticalJoiner
: Combine two Boxes
up-down, creating a new composite.
Wrapper
: Wrap a Box
inside a border,
creating a new composite.
Cloner
: Make a copy of a Box
; pass the
original to one successor, and the clone to another.
Alternator
: Pass alternate inputs through to
alternate successors.
Screener
: Pass some kinds of Boxes
to
one stage, and others to another.
Collector
: Direct the results of two independent
assembly lines to a single successor.
Painters
and Flippers
)
change the states of their sources using methods supported by the
represented objects themselves, and then pass them on to other
stages. Others accept zero (BasicBoxSource
), one
(Wrapper
) or two (Joiners
) incoming objects
to create new kinds of Boxes. Both Cloners
and
Alternators
are kinds of
Splitters. Collectors
and related stages come into play
as utilities to help with some of the plumbing.
Since we are doing push-based flow, these interfaces mainly describe
put-style methods. In fact, we could just call them all
put
, except that this doesn't work very well for Combiner
stages. For example, a VerticalJoiner
needs two put
methods, one supplying the top Box
, and one for the
bottom Box
. We could evade this by designing
Joiners
to take alternate incoming Boxes as the tops and
bottoms, but this would make them harder to control. Instead, we'll
use the somewhat ugly but easily extensible names putA
,
putB
, and so on:
DualInputPushStage.java
We can make the ``B'' channels of DualInputPushStages
completely transparent to other stages by defining a simple Adapter
class that accepts a putA
but relays it to the intended
recipient's putB
. This way, most stages can be built to
invoke putA
, without knowing or caring that it is being
fed into some successor's B channel:
And, while we are focused on interfaces and adapters, here is a
simple Runnable
Adapter that helps perform any
putA
in a new Thread
:
More interesting sinks require more interesting code. For example, in the applet used to produce the image shown at the beginning of this section, the Applet
subclass itself was defined to implement
PushStage
. It served as the ultimate sink by displaying the assembled objects.
DevNull
, each stage has at least one successor. There
are several implementation options, including:
attach1
, attach2
):
Now we can build all sorts of useful classes that extend either of these base classes, simultaneously implementing any of the standard interfaces.
Painters
, Wrappers
,
and Flippers
are just:
The Painter
and Wrapper
stages apply to
any kind of Box
. But Flippers
only make
sense for JoinedPairs
. If a Flipper
receives
something other than a JoinedPair
, it will just pass it
through. In a more ``strongly typed'' version, we might instead choose
to drop boxes other than JoinedPairs
by sending them to
DevNull
.
Collector
, that accepts messages on either channel, and
relays them on to a single successor:
Collector.java
We have two kinds of Combiners, horizontal and vertical
Joiners
. As did the representation classes, the stage
classes share enough in common to build a common superclass. Joiner
stages block further inputs until they are able to combine one item
each from putA
and putB
. This can be
implemented via the usual guard mechanics.
Joiner.java HorizontalJoiner.java VerticalJoiner.java
Joiners
).
Alternators
output alternate inputs on alternate channels.
Cloners
multicast the same element to both successors:
A Screener
is a stage that directs all inputs obeying
some predicate to one channel, and all others to the other. We can
build a generic one by encapsulating the BoxPredicate
to
check in an interface, and implementing it for example with a class
that makes sure that a Box
fits within a given
(symmetric, in this case) bound. The Screener
itself
accepts a BoxPredicate
and uses it to direct outputs:
BoxPredicate.java MaxSizePredicate.java Screener.java
Here is a sample source, one that produces BasicBoxes
of
random sizes. For convenience, it is also equipped with an autonomous
loop repeatedly invoking start
, interspersed with random
production delays:
Here's a fragment of the flow used in the applet that produced the image displayed at the beginning of this section.