- Simple and elegant solutions to specific problems
in object-oriented software design
- Primary purpose is to increase software reuse
and flexibility
- Have developed and evolved over time
- First described systematically by Gamma, Helm,
Johnson and Vlissides
[GHJV95]
- Many of the patterns described here are from this book
We start with the reuse mechanisms built in to object-oriented
programming.
Patterns are classified by their scope and their purpose.
A design pattern's scope specifies whether it applies primarily
to
classes or
objects:
- Class patterns: Deal with relationships between classes
and their subclasses
- Achieved through inheritance
- Fixed at compile-time — static
- Object patterns: Deal with object
relationships
- Achieved through association and interfaces
- Can be changed at run-time — dynamic
Most patterns are object patterns.
A design pattern's purpose reflects what it does:
- Creational patterns: Concern the process of object
creation
- Structural patterns: Deal with the
composition of objects or classes
- Behavioral patterns: Characterize the ways
in which classes or objects interact and distribute responsibility
Patterns described by
GHJV95:
Cross-classifications:
- Creational class patterns: Defer some part
of object creation to subclasses
- Creational object patterns: Defer object
creation to another object
- Structural class patterns: Use inheritance to compose
classes
- Structural object patterns: Describe ways
to assemble objects
- Behavioral class patterns: Use inheritance to describe
algorithms and flow of control
- Behavioral object patterns: Describe how a
group of objects cooperate to perform a task that no single object
can carry out alone
Object-oriented design can reuse code through:
To understand design patterns one must understand the difference
between class inheritance and interface inheritance.
New classes can be defined in terms of existing classes using
class
inheritance (also called
implementation inheritance):
- When a subclass inherits from a parent
class, it includes the definitions of all the data and
operations that the parent class defines
- Objects that are instances of the subclass will
contain all data defined by the subclass and its parent
classes, and they'll be able to perform all operations
defined by this subclass and its parents
Class inheritance is basically just a mechanism for extending an
application's functionality by reusing functionality in parent
classes.
- Lets you define a new kind of object rapidly in
terms of an old one
- Lets you get new implementations almost for free,
inheriting most of what you need from existing classes
Class inheritance defines an object's
implementation in terms of
another object's implementation.
In short, it's a mechanism for
code and representation sharing.
Inheritance can also be used to define families of objects with
identical APIs by inheriting from a purely abstract class.
- In C++, purely abstract classes have only pure
virtual methods
- In Java, interfaces are used
Interface inheritance (or
subtyping) describes when an
object can be used in place of another.
- A class that implements an interface merely adds
or overrides operations and does not hide (override) operations of the
parent class
- All subclasses can then respond to the requests in
the interface of this abstract class, making them all subtypes of
the abstract class
- Clients remain unaware of the specific types of
objects they use, as long as the objects adhere to the interface
that clients expect
- Clients remain unaware of the classes that
implement these objects. Clients only know about the abstract
class(es) defining the interface
Parent y;
...
y = new Child1();
y.method1(); // some behavior
...
y = new Child2();
y.method1(); // different behavior
Interface inheritance so greatly reduces implementation dependencies
between subsystems that it leads to the following principle of
reusable object-oriented design:
Program to an interface, not to a class (or an implementation)
Don't declare a variable to be an instances of particular concrete
classes if you need to assign various implementations of an interface to
it.
Child1 y; // Bad?
...
y = new Child2(); // Error: Can't assign Child2
Instead, commit only to interface types.
Reuse by subclassing (class inheritance) is often referred to
as
white-box reuse:
- The internals of parent classes are often visible to
subclasses.
Code reuse can also be obtained by composing objects out of other objects,
i.e., by creating object associations (or aggregations).
Object association is referred to as
black-box reuse:
- Since an object controls the visibility of the
internals of its associated objects, no internal details of
objects may be visible
An example of how class inheritance is white-box reuse:
Since the internal details of the stack are exposed as a vector, stack
encapsulation has been broken
The preferred way to reuse the capabilities of a vector when
implementing a stack:
Favor object association over class inheritance.
Using association promotes
class cohesion: keeping classes
encapsulated and focused on one task.
This principle also promotes
smaller hierarchies and
dynamic
behavior.
Favoring association over inheritance promotes smaller hierarchies
because classes and class hierarchies will remain small and
less likely to grow unmanageably.
Here is an example of an inappropriate use of class inheritance:
Two problems with this:
- Classes are so similar there would be little difference in their operations
- Every time a new kind of recording is conceived, a new class must be
created.
When a new kind of recording is conceived, a
new
RecordingCategory object is created with an appropriate label.
New kinds of recording generate new objects and relationships, but no
new classes are created:
If the associated objects
are related by an interface type, many different objects can be
associated, allowing many kinds of behavior.
Recall graph search example:
- Search with a queue as a vertex
dispenser: breadth-first search
- Search with a stack as a vertex
dispenser: depth-first search
Dynamic behavior is a common objective in behavioral design patterns
(see the
Classification menu item).
Dynamic behavior involves a
Delegator participant and a
Delegatee
participant, with the Delegator delegating part of its responsibility to
the Delegatee.
Dynamic behavior enhances flexibility and easy reuse in several contexts,
including when:
-
A member of the Delegator class needs different behavior in different
programs, or
-
Different members of the Delegator class need different behavior in
the same program, or
-
A member of the Delegator class needs different behavior at different
times in the same program.
In any delegation the Delegator needs an instance variable to reference the
Delegatee.
To achieve the flexibility goals of delegation, "Delegatee" just
defines a common
interface for a variety of concrete classes that
implement different behaviors.
We refer to these concrete classes as
ConcreteDelegatees, as shown
below.
We will find that delegation plays a role of sub-pattern in several
of the design patterns to follow.
Some of the behavioral and creational design patterns
incorporate the general delegation idea.
Behavioral design patterns incorporating delegation include:
- Strategy
- State
- Observer
- Chain of Responsibility
Creational design patterns incorporating delegation include:
Following is a list of the GHJV design patterns labeled by purpose,
where the labels mean:
|
Behavioral |
|
Structural |
|
Creational |
Participants
-
Delegator: Handler
-
Delegatee: Handler
-
ConcreteDelegatee: ConcreteHandler1, ...
This example shows a chain of objects each of which knows how to handle
a certain kind of integer.
If an object is given an integer it doesn't know how to handle, it
passes responsibility to the next object in the chain.
Following are files that implement the structure.
- Handler.java: Interface type for an integer
handler
- EndHandler.java: Class implementing Handler and
providing default behavior for the end of the chain. Objects of
this type have no successors in the chain.
- SquareHandler.java: Class extending EndHandler
and providing behavior for handling integers that are
squares and passing off responsibility for non-squares
- EvenHandler.java: Class extending EndHandler
and providing behavior for handling integers that are
even and passing off responsibility for non-evens
- NegativeHandler.java: Class extending EndHandler
and providing behavior for handling integers that are
negative and passing off responsibility for non-negatives
- Main.java: Class that creates the objects, arranging
them into a chain of responsibility, and testing them on some input.
- Exception Handling
- Cascading Stylesheets
- Classless Object-oriented Languages
Exceptions may be implemented using the Chain of Responsibility pattern,
though the objects involved may not be visible to programmers:
- The Handler objects are activation records on the
runtime stack.
- Catch clauses attach a special kind of method to the activation record
that is active when the try statement is executed.
- This method is internally invoked when an exception of the appropriate
type occurs.
- If an activation record does not have a catch method of the appropriate
type then the exception is passed on to the parent activation record.
Document styles such as Content Style Sheets (CSS) often support
style inheritance:
- In the element hierarchy (a Composite pattern), style attributes
defined in one element may be inherited by its children.
- The method for locating an inherited style attribute for an element
checks an attribute table in that element.
- If the attribute is not found there, a message with the same method is
sent to the element's parent.
- The method recurs until the attribute value is found.
Inheritance mechanisms in classless OOLs may use a Chain of Responsibility
design pattern to minimize the memory footprint of objects.
- Search for an object member starts in its own member
table.
- If the member is not found there the search is forwarded to the object's
prototype, which may forward to its prototype, and so on.
You want toimplement commands that behave like objects, either because
you want to store additional information with commands, or you want to
collect commands.
Example
Provide a way to access the elements of an aggregate object
sequentially without exposing its underlying representation.
Participants
-
Aggregate:
defines an interface for creating an Iterator object.
-
Iterator:
defines an interface for accessing and traversing elements in an
Aggregate.
-
Concrete Aggregate:
implements the Iterator creation interface to return an instance of
the appropriate ConcreteIterator.
-
Concrete Iterator:
implements the Iterator interface, keeping track of its current
position in the Aggregate.
Java Iterable Classes
-
Aggregate:
java.lang.Iterable
-
Iterator:
java.util.Iterator
-
Concrete Aggregate:
ArrayList, HashMap, …
-
Concrete Iterator:
Every class that implements the Iterable interface defines its own
private concrete class that implements the Iterator interface.
It is returned by the iterator() method.
Without violating encapsulation, capture and externalize an object's
internal state so that the object can be restored to this state later.
Participants
-
Originator: provides a method that creates a Memento and a
method with a Memento parameter that can be invoked later.
-
Memento: stores all or part of the internal state of the
Originator object.
-
Caretaker: obtains the Memento from the Originator and later
invokes an Originator method to restore its state, using the Memento
as a parameter.
Example
Often used to support "undoable" operations in editors:
- The Memento object records information about the state of the
document that is being edited.
- The editor restores this state to undo an earlier operation.
See Swing's
UndoableEdit interface.
Participants in Delegation Pattern
-
Delegator: Subject
-
Delegatee: Observer
-
ConcreteDelegatee: ConcreteObserver
Example: Swing Listeners
-
Subject: shared interface for subjects is not needed
-
ConcreteSubject: various JComponent subclasses
-
Observer: various types of listener
-
ConcreteObserver: implementations of listener interfaces
Example: Model-View Separation
Participants
-
Delegator: Context
-
Delegatee: State
-
ConcreteDelegatee: StateA, ...
Example: Network Connections
Participants in Delegation Pattern
-
Delegator: Context
-
Delegatee: Strategy
-
ConcreteDelegatee: ConcreteStrategyA, ...
Example: LayoutManager
-
Context: JPanel
-
Strategy: LayoutManager
-
ConcreteStrategy: BorderLayout, FlowLayot, BoxLayout, …
Participants
-
Component: declares the interface for objects in the composition.
-
Leaf: defines behavior for components having children.
-
Composite: defines behavior for components having children.
Examples
Swing GUI classes provide an excellent example:
-
JComponent plays the role of Component.
-
JLabel and all of the Swing controls play the role of Leaf.
-
JPanel, JScrollPane, JSplitPane, and JTabbedPanePane play the role of
Composite.
Sometimes you want to encapsulate a
group of classes in order to
implement an abstract view of their combined functionality.
Participants
-
Facade: delegates client requests to the appropriate subsystem objects.
-
Subsystem classes: implement subsystem functionality.
Examples
Swing
JTable,
JTree, and
JEditorPane.
Each of these examples combines an implementation of
JComponent
methods with
Facade constructors and methods whose abstract view
hides complex model, view, and controller classes.
To make an object a (possibly lightweight) placeholder for another
object.
Example
Participants
-
Delegator: Client
-
Delegatee: AbstractFactory
-
ConcreteDelegatee: ConcreteFactory
Examples
Interface
javax.swing.text.ViewFactory:
A factory to create a view of some portion of document subject. This
is intended to enable customization of how views get mapped over a
document model.
Participants
-
Delegator: Director
-
Delegatee: Builder
-
ConcreteDelegatee: ConcreteBuilder
Examples
SAX parsers for XML.
How to supply a method that can be overridden to create objects of
varying types.
Example 1
Collection.iterator()
Example 2
Move.doMove(State)
The goal of
data abstraction in general is the clear separation
of data's use from its implementation.
Data abstraction is achieved in object-oriented languages
through
encapsulation of state and behavior: making an object's
internal state inaccessible directly, so that:
- It's representation is not visible from outside the object
- Data can only be accessed through available methods
Complex programs often need multiple levels of encapsulation,
provided by
encapsulation design patterns, that work by either:
-
Providing a new form of encapsulation, or
-
Protecting a high-level encapsulation by relaxing encapsulation at a
lower level
- Facade: provides a unified interface to a set of interfaces in a
subsystem making it easier to use
- Strategy: encapsulates an abstract behavior that has several
variations.
Sometimes you need to relax encapsulation at a low level in order to do
a better job of encapsulation at a higher level.
Although this sounds contradictory, the improvement arises from the fact
that allowing higher level classes to directly access data in lower
level classes or vice-versa avoids having to declare public methods for
the access.
For this reason, well-designed OOLs have mechanisms that allow one class
to have access to private members of another class.
- In C++ you can declare one class as a "friend" of another
class.
- In Java, a class can be nested inside of another class, which allows
both classes to access private members of the other class.
Three design patterns that employ relaxed encapsulation:
- Observer: often provides a public method for modifying an object
that it has private access to, but another object (a Subject)
decides when the modification will occur
- Iterator: provides sequential access to objects in an Aggregate
without exposing the implementation of the Aggregate
- Memento: carries state information about an Originator object,
allowing the Originator private access to that information