The Open-Closed Principle (OCP) states:
A component should be open to extension but closed to modification.
A component can be anything from a single class to an entire program.
Assume a Calculator class is available that provides operations for doing basic arithmetic. We can add additional operations by creating extension classes. These classes inherit the super class operations. They can declare additional operations. They can even redefine (override) inherited methods.
See sublasses.htm for details.
Variables contain values.
In Java a value is a primitive or a reference.
Variables and values have types.
In Java a type is a primitive type, interface, class, or array type. (Strictly, a class isn't a type, rather it's the public interface of a class that's the type.)
The type of a reference is the class or array type of the corresponding object or array.
A is a subtype of B (A <: B) if
A = B
A extends or implements B
A extends or implements a subype of B
In Java, we also have:
byte <: char <: int <: long <: float <: double
short <: int
A <: Object for any class, interface or array type A
The Subsumption Rule:
A variable of type A can hold a value of type B for any B <: A
Dynamic Dispatch Rule:
Objects, not compilers, deterime which methods to execute.
Taken together these principles imply that the code:
var.meth(a, b, c);
might invoke different methods each time it is called. This could happen because at different times var can hold references to instances of different subclasses of its declared type, and these subclasses may override the meth method.
Combining Polymorphism with Dynamic Dispatch opens the door to a style of programming called Data-Driven Programming. In Data-Driven Programming, sequence control is determined by the data, not the programmer. This contrasts with the older Control-Driven Programming in which programmers device and maintain complex control structures.
Assume NASA wants to build a flight simulator. Toward this goal NASA purchases a library of simulated aircraft from CyberAir Corp:
The flight package contains a hierarchy of aircraft models:
Note that the subclasses override the inherited methods. This is because the algorithms for taking off, flying, and landing are quite different for airplanes, helicopters, and blimps. (See Aircraft.java.)
NASA's sim package contains classes representing pilots, airports, control towers, and fleets. For example:
Here's a sketch of the Fleet class:
public class Fleet {
private int cap = 100;
private int size = 0;
private Aircraft[] members = new
Aircraft[cap];
public void add(Aircraft a) {
if (size < cap) {
members[size++] = a;
}
}
public void takeoff() {...}
public void fly() {...}
public void land() {...}
public static void main(String[] args)
{
Fleet united = new Fleet();
united.add(new Aircraft());
united.add(new Airplane());
united.add(new Helicopter());
united.add(new Blimp());
united.takeoff();
System.out.println("+++++");
united.land();
}
}
Fleet is implemented in Fleet.java.
Here is the output of main:
An aircraft is taking off
An airplane is taking off
A helicopter is taking off
A blimp is taking off
+++++
An aircraft is landing
An airplane is landing
A helicopter is landing
A blimp is landing
First notice that although the parameter for the add method has type Aircraft, the actual arguments supplied by main are helicopters, blimps, and airplanes. This is an example of the subsumption rule. Since Airplane, Helicopter, and Blimp are sublcasses of Aircraft, Airplane, Helicopter, and Blimp objects can be stored in variables (and parameters) of type Aircraft.
Let's implement takeoff in the Data-Driven style and land in the Control-Driven style:
public void takeoff() {
for(int i = 0; i < size; i++) {
members[i].takeoff();
}
}
public void land() {
for(int i = 0; i < size; i++) {
if (members[i] instanceof Blimp)
((Blimp)members[i]).land();
else if (members[i] instanceof
Airplane)
((Airplane)members[i]).land();
else if (members[i] instanceof
Helicopter)
((Helicopter)members[i]).land();
else if (members[i] instanceof
Aircraft)
members[i].land();
else
System.err.println("unrecognized
aircraft");
}
}
Both methods work. Certainly the Data-Driven style is much shorter. But there are several other advantages.
Suppose CyberAir wants to add a new type of aircraft into their Aircraft hierarchy:
class Saucer extends Aircraft {
void takeoff() {
System.out.println("A flying
saucer is taking off");
}
void takeoff() {
System.out.println("A flying
saucer is flying");
}
void takeoff() {
System.out.println("A flying
saucer is landing");
}
}
The control-driven programmer must now add a line to Fleet.land():
else if (members[i] instanceof Saucer)
((Saucer)members[i]).land();
There are two problems with this. First, Fleet is a client of the Aircraft hierarchy. Our design requires a modification of the client code each time the Aircraft hierarchy is extended. What if the client code isn't available, or what if it is mission-critical code found in an air traffic control tower. Second, how many places in the client code must be modified? How many places did the client assume there were only three types of aircraft? This multi-way conditional could occur hundreds of times in the client code. If we forget to modify just one of these occurrences, then we will have introduced a bug into the client's code!
On the other hand, note that no changes to Fleet.takeoff() are required. In a sense, Fleet.takeoff() is dumber than Fleet.land() because Fleet.takeoff() knows less about the different types of aircraft than Fleet.land() does. But this is one situation where dumber is better. Any time new data is added to the system, smart methods need to be updated. Dumb methods don't.
Returning to the Data-Driven implementation of Fleet.takeoff(), what happens when we call
members[i].takeoff()
and members[i] refers to a flying saucer, and yet the implementer of the Saucer class forgot to provide a takeoff() method? No problem, the Saucer class inherited the default takeoff() method from the Aircraft class. So in this case Aircraft.takeoff() will be called.
But is this desirable? Our Aircraft.takeoff() method simply prints a message. In a real-world application calling this method could be a disaster. We can remedy this situation by not providing a default takeoff method in the Aircraft class. But now the compiler won't compile
members[i].takeoff()
The problem is that there's no guarantee that every aircraft will have a takeoff() method.
The correct remedy is to provide the Aircraft class with an abstract takeoff() method. Abstract methods have no bodies:
abstract class Aircraft {
public abstract void takeoff();
// etc.
}
Of course we can no longer instantiate the Aircraft class:
Aircraft a = new Aircraft(); // ERROR!
a.takeoff(); // because what happens here?
Now the compiler requires subclasses to implement a takeoff() method. If the implementer of Saucer forgets to provide a takeoff() method, then Saucer also becomes an abstract class, and attempting to instantiate the saucer class is a compile-time error:
united.add(new Saucer()); // COMPILER ERROR!
Names of abstract classes and virtual functions are italicized in UML:
When it is time to upgrade or replace a chip in a computer, the old chip is simply popped out of the motherboard, and the new chip is plugged in. It doesn't matter if the new chip and old chip have the same manufacturer or the same internal circuitry, as long as they both "look" the same to the motherboard and the other chips in the computer. The same is true for car, television, and sewing machine components. Open architecture systems and "Pluggable" components allow customers to shop around for cheap, third-party generic components, or expensive, third-party high-performance components.
An interface is a collection of operator specifications. An operator specification may include a name, return type, parameter list, exception list, pre-conditions, and post-conditions. A class implements an interface if it implements the specified operators. A software component is an object that is known to its clients only through the interfaces it implements. Often, the client of a component is called a container. If software components are analogous to pluggable computer chips, then containers are analogous to the motherboards that contain and connect these chips. For example, an electronic commerce server might be designed as a container that contains and connects pluggable inventory, billing, and shipping components. A control panel might be implemented as a container that contains and connects pluggable calculator, calendar, and address book components. Java Beans and ActiveX controls are familiar examples of software components.
Modelers can represent interfaces in UML class diagrams using class icons stereotyped as interfaces. The relationship between an interface and a class that realizes or implements it is indicated by a dashed generalization arrow:
Notice that the container doesn't know the type of components it uses. It only knows that its components realize or implement the IComponent interface.
For example, imagine that a pilot flies an aircraft by remote control from inside of a windowless hangar. The pilot holds a controller with three controls labeled: TAKEOFF, FLY, and LAND, but he has no idea what type of aircraft the controller controls. It could be an airplane, a blimp, a helicopter, perhaps it's a space ship. Although this scenario may sound implausible, the pilot's situation is analogous to the situation any container faces: it controls components blindly through interfaces, without knowing the types of the components. Here is the corresponding class diagram:
Notice that all three realizations of the Aircraft interface support additional operations: airplanes can bank, helicopters can hover, and blimps can deflate. However, the pilot doesn't get to call these functions. The pilot only knows about the operations that are specifically declared in the Aircraft interface.
We can create new interfaces from existing interfaces using generalization. For example, the Airliner interface specializes the Aircraft and (Passenger) Carrier interfaces. The PassengerPlane class implements the Airliner interface, which means that it must implement the operations specified in the Aircraft and Carrier interfaces as well. Fortunately, it inherits implementations of the Aircraft interface from its Airplane super-class:
An interface is closely related to the idea of an abstract data type (ADT). In addition to the operator prototypes, an ADT might also specify the pre- and post-conditions of these operators. For example, the pre-condition for the Aircraft interface's takeoff() operator might be that the aircraft's altitude and airspeed are zero, and the post-condition might be that the aircraft's altitude and airspeed are greater than zero.
Java allows programmers to explicitly declare interfaces:
interface Aircraft {
void takeoff();
void fly();
void land();
}
Notice that the interface declaration lacks private and protected members. There are no attributes, and no implementation information is provided.
A Pilot uses an Aircraft reference to control various types of aircraft:
class Pilot {
private Aircraft myAircraft;
public void fly() {
myAircraft.takeoff();
myAircraft.fly();
myAircraft.land();
}
public void setAircraft(Aircraft a) {
myAircraft = a;
}
// etc.
}
Java also allows programmers to explicitly declare that a class implements an interface:
class Airplane implements Aircraft {
public void takeoff() { /* Airplane
takeoff algorithm */ }
public void fly() { /* Airplane fly
algorithm */ }
public void land() { /* Airplane land
algorithm */ }
public void bank(int degrees) { /* only
airplanes can do this */ }
// etc.
}
The following code shows how a pilot flies a blimp and a helicopter:
Pilot p = new Pilot("Charlie");
p.setAircraft(new Blimp());
p.fly(); // Charlie flies a blimp!
p.setAircraft(new Helicopter());
p.fly(); // now Charlie flies a helicopter!
It is important to realize that Aircraft is an interface, not a class. As such, it cannot be instantiated:
p.setAircraft(new Aircraft()); // error!
Java also allows programmers to create new interfaces from existing interfaces by extension:
interface Airliner extends Aircraft, Carrier {
void serveCocktails();
}
Although a Java class can only extend at most one class (multiple inheritance is forbidden in Java), a Java interface can extend multiple interfaces and a Java class can implement multiple interfaces. A Java class can even extend and implement at the same time:
class PassengerPlane extends Airplane implements Airliner {
public void add(Passenger p) { ... }
public void rem(Passenger p) { ... }
public void serveCocktails() { ... }
// etc.
}
Frameworks and containers are examples of extensible applications. Both frameworks and containers specify critical interfaces that must be implemented by people wishing to extend functionality.
A generic class or interface has one or more type parameters.
class Node<Data> {
private Data info;
private Node<Data> parent;
public Node(Data i, Node<Data> p)
{
info = i;
parent = p;
}
public Node(Data i) { this(i, null); }
public Node() { this(null); }
public Data getInfo() { return info; }
public void setInfo(Data i) { info = i;
}
public Node<Data> getParent() {
return parent; }
public void setParent(Node<Data>
p) { parent = p; }
public String toString() { return
"[" + info + "]"; }
}
Suppose we wanted to implement binary search trees. In this case the values for Data must be restricted to subtypes of the Comparable interface:
class Node<Data implements Comparable> { ... }
Warning:
S <: T does NOT imply Node<S> <: Node<T>
However:
Node<S> <: Node<?> for every S
For example:
class TreeUtils {
public static void
displayPath(Node<?> node) {
while(node != null) {
System.out.print(node + "
");
node = node.getParent();
}
System.out.println();
}
// etc.
}
Usage:
class TestTree {
public static void main(String[] args)
{
Node<String> nfl = new
Node<String>("NFL");
Node<String> afc = new
Node<String>("AFC", nfl);
Node<String> nfc = new
Node<String>("NFC", nfl);
Node<String> raiders = new
Node<String>("Raiders", afc);
Node<String> jets = new
Node<String>("Jets", afc);
TreeUtils.displayPath(raiders);
}
}
Output:
[Raiders] [AFC] [NFL]
class Student { ... }
public class Course {
private Set<Student> roster =
new HashSet<Student>();
private Map<Student,
List<Integer>> grades =
new Hashtable<Student, List<Integer>>();
public void add(Student s) {
roster.add(s);
grades.put(s, new
LinkedList<Integer>());
}
public void remove(Student s) {
roster.remove(s);
grades.remove(s);
}
public Iterator<Student>
iterator() { return roster.iterator(); }
public void addScore(Student s,
Integer score) {
List<Integer> scores =
grades.get(s);
scores.add(score);
}
public double average(Student s) {
List<Integer> scores =
grades.get(s);
double result = 0;
for(Integer score: scores) {
result += score;
}
return result/scores.size();
}
// usage:
public static void main(String[]
args) {
Student s1 = new Student();
Student s2 = new Student();
Student s3 = new Student();
Course cs1 = new Course();
cs1.add(s1);
cs1.add(s2);
cs1.add(s3);
cs1.addScore(s1, 100);
cs1.addScore(s1, 90);
cs1.addScore(s1, 80);
System.out.println("avg =
" + cs1.average(s1)); // prints 90
}
}
An open system can be extended at runtime using plug-in components. The only trouble is that the container or platform must discover the functionality of plug-ins. This can be done in Java using reflection.
Several design patterns specifically provide for extending systems: