CHAPTER 12: WHITE BOX AND BLACK BOX

Not all frameworks are alike. One way to
distinguish between them is the notion of white box vs. black
box.

A white-box
framework requires the framework user to understand the
internals of the framework to use it effectively. In a white box
framework, you usually extend behavior by creating subclasses,
taking advantage of inheritance. A white box framework often comes
with source code.

A black-box
framework does not require a deep understanding of the
framework’s implementation in order to use it. Behavior is
extended by composing objects together, and delegating behavior
between objects.

A framework can be both white-box and
black-box at the same time. Your perception of how
“transparent” a framework is may depend on non-code
aspects such as documentation or tools.

Frameworks tend to change over their lifetime.
(See [Johnson & Roberts].) When a framework is new, it tends to
be white-box: you change things by subclassing, and you have to
peak at source code to get things done. As it evolves, it becomes
more black-box, and you find yourself composing structures of
smaller objects. Johnson and Roberts point out that frameworks can
evolve beyond black-box, perhaps becoming visual programming
environments, where programs can be created by interconnecting
components selected from a palette. (JavaBeans is an effort in that
direction.)

Visual environments and even black-box
frameworks sound so much easier to use than white-box frameworks -
why would we ever bother creating white-box frameworks in the first
place? The basic answer is “cost”. To develop a
black-box framework, we need a sense of which objects change the
most, so we can know where the flexibility is most needed. To
develop a visual environment, we need even more information: we
need to know how objects are typically connected together.
Discovering this costs time. White-box frameworks are easier to
create and have more flexibility.

White-Box Frameworks

The most common sign that a framework is
white-box is heavy use of inheritance. When you use the framework
by extending a number of abstract (or even concrete) classes, you
are dealing with a white-box framework. Inheritance is a closer
coupling than composition; an inherited class has more context it
must be aware of and maintain. This is visible even in Java’s
protection scheme: a subclass has access to the public and
protected parts of the class, while a separate object only sees the
public parts. Furthermore, a subclass can potentially
“break” a superclass even in methods it doesn’t
override, for example by changing a protected field in an
unexpected way.

What are the effects of an inheritance-based
approach?

·
We need to understand how the subclass and superclass
work together.

·
We have access to both the protected and the public parts
of the class.

·
To provide functionality, we can override existing
methods, and implement abstract methods.

·
We have access to the parent’s methods (by calling
super.method()).

Example: A simple applet

import
…

public
class MyApplet extends Applet {

public void paint(…) {…}
//TBD??

}

Notice how we depend directly on the
superclass, even to using its methods freely.

A subclass is coupled to its parents, and we
deal with the benefits and costs of that fact.

Black-Box Frameworks

Black-box frameworks are based on composition
and delegation rather than inheritance. Delegation is the idea that
instead of an object doing something itself, it gives another
object the task. When you delegate, the object you delegate to has
a protocol or interface it supports, that the main object can rely
on.

In black-box frameworks, objects tend to be
smaller, and there tend to be more of them. The intelligence of the
system comes as much from how these objects are connected together
as much as what they do in themselves.

Composition tends to be more flexible than
inheritance. Consider an object that uses inheritance versus one
that delegates. With inheritance, the object basically has two
choices: it can do the work itself, or it can call on the parent
class to do it. In a language like Java, the parent class is fixed
at compile time.

With delegation, an object can do the work
itself (or perhaps in its parent), or it can give the work to
another object. This object can be of any useful class (rather than
only the parent), but it can also change over time. (You can
delegate to an object, change the delegate to be another object,
and use the new object as the delegate next time.)

Example: TBD

[TBD compare to cutting grass - children vs.
lawn service?]

Converting Inheritance to Composition

In “Designing Reusable Classes,”
Johnson and Foote identify this rule:

Rule 12. Send messages to components instead of to self. An
inheritance-based framework can be converted into a component-based
framework black box structure by replacing over-ridden methods by
message sends to components.

Let’s apply their advice to an example.
Suppose we’ve got a class that sues the Template Method
design pattern like this:

public class ListProcessor {

final public void processList() {

doSetup();

while (hasMore()) {

doProcess1();

}

doTeardown();

}

protected void doSetup() {}

protected void doTeardown() {}

abstract protected boolean hasMore();

abstract protected void doProcess1();

// …

}

with this as a typical
subclass:

public class TestProcessor extends ListProcessor
{

int i =
0;

protected
boolean hasMore() {return i < 5;}

protected
void doProcess1() {System.out.println(i);}

}

In the simplest form of
the transformation, we can take each method designed to be
subclassed, and whenever it is called, replace it by a message to
the delegate. We’ll also add some code to set up the
delegate.

public class ListProcessor {

ListProcessor delegate;

public ListProcessor(lp) {delegate = lp;}

final public void processList() {

delegate.doSetup();

while (delegate.hasMore()) {

delegate.doProcess1();

}

delegate.doTeardown();

}

protected void doSetup() {}

protected void doTeardown() {}

abstract protected boolean hasMore();

abstract protected void doProcess1();

// …

}

We can extend this like
before. [TBD] You might create it like this:

ListProcessor lp = new ListProcessor (new
TestProcessor());

Compare how these two
situations look at run-time:

[LP] ß
[Test]
=>[:Test]

[LP]odelegate
=> [:LP] ---
[:Test]

^

[Test]

So far, this doesn’t
seem to be worth the trouble: ListProcessor already has a copy of
“processList()”; it doesn’t need the one in
Test.

The next step is to
introduce a new class for the delegate, restricted to just the
capabilities it needs. The methods called on the delegate define
its protocol. We could handle this via an abstract class, but I
prefer to use a Java interface:

public interface ListProcessAction {

void
doSetup();

boolean
hasMore();

void
doProcess1();

void
doTeardown();

}

These are the methods
intended to be over-ridden.

Now the main class can use
the ListProcessorAction for its delegate. Furthermore, as
ListProcessor is no longer intended to be subclassed, it no longer
has any need for those protected methods:

We could make our class
depend on this interface:

public class ListProcessor implements ListProcessorAction {

ListProcessorAction delegate;

public ListProcessor(ListProcessorAction a) {delegate = a;}

final public void processList() {

delegate.doSetup();

while (delegate.hasMore()) {

delegate.doProcess1();

}

delegate.doTeardown();

}

// …

}

with this concrete
implementation of the action:

public class ConcreteAction {

int i =
5;

public void
doSetup() {}

public
boolean hasMore() {return i < 5;}

public void
doProcess1() {System.out.println(i);}

public void
doTeardown() {}

}

[TBD: Typically when these
involve abstract methods, you might create an abstract class
version, which will be extended by the end class. Or if the
protocol is small and completely abstract, you don’t need
concrete classes.]

The structure looks like
this:

[LP] -delegate- [<<interface>> LPA] => [:LP] -delegate-
[:CA]

^

[ConcreteAction]

This runtime structure is
similar to the previous one, but now ListProcessor and
ConcreteAction are separate classes.

We have split one big
object, that knew both the algorithm and the individual steps, into
two classes, one for each concern. Look at the tradeoffs. In the
initial version, everything was in one place. To trace the new
version, you have to understand the delegation structure and how it
can vary at runtime. When you write a new action, it’s easier
to focus on it in isolation, but harder to see how it fits into the
big picture.

See how the design has
changed: [TBD]

[//TBD]

Step by Step

This is a systematic
description of how to convert

[Base] to [Base]
–delegate-- [<<int>>Action] <-
[ConcreteAction]

Cautions: [TBD]

·
Calls to super(). (Need to work through
ramifications.)

·
Recursion. (Need to eliminate or understand.)

This approach follows a
re-factoring style, moving in small steps and letting the compiler
do the work. (See [Fowler].)

1.
Create a new interface.

public
interface Action { }

(name it
appropriately).

2.
Create a new class implementing this interface:

public
class ConcreteAction implements Action {}

3.
In Base, add a delegate field, and modify each
constructor to take an Action as a parameter; use this to set the
delegate:

protected
Action delegate;

public Base
(Action a) {

delegate = a;

// rest as before

}

…

4.
Each protected method in Base is presumably called in
Base. For each such method:

·
Move the signature to Action and change it to
“public”

·
Move the routine itself to ConcreteAction (and make it
public there as well).

·
Replace each call to “method()” with
“delegate.method()”.

For example,

Base {…

protected method() { /*impl*/.}

… method(); …

}

becomes

Base {… delegate.method(); … }

Action { … public method(); …}

ConcreteAction {… public method() {/*impl*/} … }

Note: You can find the
call sites by temporarily changing the method name to
“xxx”, and seeing what breaks in base.

Moving protected methods
may force you to pull over some private methods as well, or perhaps
maintain a reference back to Base. Unfortunately, this is not a
fully mechanical process. Similarly, if Base’s methods
involve recursion or calls to super(), you will need insight into
how the class works and how you want to split it.

5.
Check whether methods in Base call any of Base’s
public methods. If so,

7.
Check out any subclasses of Base. Figure out whether they
should remain a subclass of Base, become a subclass of
ConcreteAction, or become an independent action implementing the
Action interface. (The class may need to be split.) Distribute the
subclass’ behavior where it should go. As usual, be careful
about recursion and super().

8.
Find each call to a constructor for Base or its
subclasses. (Let the compiler tell you where they are.) Add a new
parameter to the constructor, “new ConcreteAction()”
where Base wants it.

Conflicting Class Hierarchies

Java only supports
single inheritance, but sometimes you find yourself wanting
multiple inheritance. You can use interfaces and delegation to help
in many such situations.

Look at
java.util.Observable. In some ways, it could be a basis for an
implementation of the Observer design pattern. However, the fact
that it is a class and not an interface is a flaw.

Suppose you have a
Vector, and you’d like it be Observable (perhaps so a listbox
widget can watch it for changes). Because both Vector and
Observable classes are already classes, you’d like this:

[Vector] <- ObservableVector -> [Observable] // MI =>
!Java

Suppose Observable were an
interface instead. A class can implement as many interfaces as it
needs to, so we could do this:

There’s a reason
Observable is a class though: it maintains machinery for
notification. If we had a convenience implementation, we could
delegate to it.

[Vector] <- ObservableVector -- -- > [<<int>>
Observable]

--
delegate -- [ObservableHelper]

If Java had multiple
inheritance, one class could cover both the Vector behavior and the
Observable behavior. Without multiple inheritance, we can get the
effect by connecting together a pair of classes.

[TBD: How class can we get
to the 1.1 Listener event model?]

The Swing library
designers faced this problem. Their solution is to ignore
Observable, and instead create a ListModel that models the basics
of vector handling. In Swing, there is an AbstractListModel, that
handles notification, and can delegate to a concrete vector or list
class.

Inner Classes for Multiple Inheritance

Sometimes a class
implements several interfaces when it would be better served by
using Java's inner classes. Consider this example:

public class MyPanel extends JPanel implements MouseListener {

JButton b1 = new JButton("First");

JButton b2 = new JButton("Second");

public MyPanel() {

b1.addActionListener(this);

add(b1);

b2.addActionListener(this);

add(b2);

}

public void actionPerformed(ActionEvent e) {

if (e.getSource() == b1) {

System.out.println("b1 action");

} else {
// assume b2

System.out.println("b2 action");

}

}

}

Here, MyPanel acts
both like a JPanel and an listener. Notice the code for
actionPerformed(): it’s got an ugly “if”
statement that’s practically a case statement. Such a
construct is a sign that we’re not as OO as we could be, and
we can move intelligence around.

We’ll use a
pair of inner classes, cleaning up MyPanel a bit. This keeps it
from receiving unnecessary notifications, and avoids the need for a
test to see which button was clicked.

public class MyPanel extends JPanel implements MouseListener {

JButton b1 = new JButton("First");

JButton b2 = new JButton("Second");

public class FirstListener implements ActionListener {

public void actionPerformed(ActionEvent e) {

System.out.println(“b1”+e);

}

}

public class SecondListener implements ActionListener {

public void actionPerformed(ActionEvent e) {

System.out.println(“b2” + Date.getTime());

}

}

public MyPanel() {

b1.addActionListener(new FirstListener());

add(b1);

b2.addActionListener(new SecondListener());

add(b2);

}

}

The first version
was more in the style of JDK 1.0.2, where the event detection
hierarchy had to match the container hierarchy. The second version
is more in JDK 1.1 style.

You can carry this
a step further to use anonymous inner classes:

public class MyPanel extends JPanel implements MouseListener {

JButton b1 = new JButton("First");

JButton b2 = new JButton("Second");

public MyPanel() {

b1.addActionListener(new ActionListener() {

public
void actionPerformed(ActionEvent e) {

System.out.println(“b1”+e);

}

});

add(b1);

b2.addActionListener(new ActionListener() {

public
void actionPerformed(ActionEvent e) {

System.out.println(“b2” + Date.getTime());

}

});

add(b2);

}

}

Many people might
find this on the edge of readability - some on the near side and
some on the far side.