The Union Design Pattern: Abstracting and Hoisting

Let us consider the Entry system from the other day (OrigCityEntry.java). Below is a Unified Modeling Language ("UML") class diagram of that system, originally:

 

 

An examination of the 3 concrete classes involved, GovernmentEntry, ResidentialEntry and BusinessEntry shows us that there appears to be some commonality:

  1. All three have a name, address and phone fields that are Strings.
  2. All three have "getter" methods called getName(), getAddress() and getPhone() that return the values in name, address and phone respectively.

What does this mean? What does this say about what a CityEntry really is?

The reason the name, address and phone fields and associated getter methods appear in all the concrete subclasses of CityEntry is because name, address and phone are intrinsic to being a CityEntry.

On the hand, state, government and city are not intrinsic to being a CityEntry. These extrinsic qualities are unique to specific concrete subclasses.

Another way of saying this is that

name, address and phone (and their associated getters) are invariant invariant properties of all CityEntry's sublclasses (that is, of all CityEntries), while state, government and city are variant properties.

The class definition of CityEntry should thus contain all the invariant properties that define what all CityEntries (which includes all its subclasses) should have. CityEntry is thus said to describe the union of all its subclasses, or equivalently, CityEntry is an abstract representation of any of its subclasses.

To implement this, we want to "hoist" (move upwards) the common, duplicated code up from the subclasses into the superclass (NewCityEntry.java):

This concept of using an abstract superclass to represent the abstraction or union of all its subclasses, is a foundational notion in object-oriented programming. This sort of superclass-sublclasses relationship is so common that we can talk about it as "design pattern" called the Union Design Pattern.

Now this all seems all well and good, but hoisting is not without its pitfalls. Let's consider another example:

Cats, monkeys and whales, while diverse creatures, are all mammals. Hence to model such a system in the computer, it makes sense to make Cat, Monkey and Whale all subclasses of an abstract Mammal superclass. Each species has many behaviors (methods) but I will only concentrate on 3 in particular:

  1. boolean givesMilk() : returns true if the animal can give milk to feed its young, false otherwise
  2. String makeSound() : returns a String represenation of a common sound the animal makes.
  3. boolean givesLiveBirth(): returns true if the animal bears live young.

In the table below are the methods and what happens when each species executes that method:

boolean givesMilk()
String makeSound()
boolean givesLiveBirth()
Cat
true
"Meow"
true
Monkey
true
"Screech"
true
Whale
true
"[whale song]"
true

We could start out with the following class implemenation (Mammal0.java):

Let's start our analysis:

This is what we have so far (Mammal1.java):

Before we go charging ahead, let's stop for a moment and review what we've done: Cats, monkeys, and whales do represent a wide spectrum of mammals, but remember, the abstract Mammal class is a representation of ALL mammals, not just the ones we have so far. The correlation of like behavior with all our represented animals does not imply its inclusionin their abstract representation!

For instance, one day, in our wanderings through Australia, we encounter a Duckbilled Platypus. Let's see how it behaves with respect to our 3 methods:

boolean givesMilk()
String makeSound()
boolean givesLiveBirth()
DuckBilledPlatypus
true
"Growl"
false

Duckbilled platypus lay eggs!!

Giving live birth is not part of the definition of a mammal. On the other hand, the question of whether or not the animal gives live birth can always be asked of any animal, including all mammals. The result may be true or false however, so the method must be abstract at the Mammal level.

Our class structure should look like this (Mammal2.java):

 

Hoisting does not guarantee proper abstraction. Hoisting should be driven by a need for abstraction, not by coincidence.

 

(written by Stephen Wong)