Review of Design Patterns
Strategy:
Intent: Define a family of algorithms, encapsulate each one, and make them interchangeable.
Allows algorithms to vary independently from clients using them.
Decorator:
Intent: Attach additional responsibilities to an object dynamically.
Offers a flexible alternative to subclassing for extending functionality.
Factory Method:
Intent: Define an interface for creating an object, but let subclasses decide which class to instantiate.
Lets a class defer instantiation to subclasses.
Template Method:
Intent: Define the skeleton of an algorithm in an operation, deferring some steps to subclasses.
Allows subclasses to redefine certain steps of an algorithm without changing the algorithm’s structure.
Composite:
Intent: Compose objects into tree structures to represent part-whole hierarchies.
Lets clients treat individual objects and compositions of objects uniformly.
Iterator:
Intent: Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.
Adapter
Observer
Command
State
Definition: A general reusable solution to a commonly occurring problem in software design.
Facilitates communication of expertise from experts to novices.
Provides a common vocabulary for communication between experts.
Solutions often represent best practices, minimizing rewriting when software changes.
Explain the purpose of each design pattern.
Explain how each design pattern achieves its intent.
Explain the difference between two design patterns.
Recognize when a pattern might be needed in a given situation.
Apply a pattern in a given situation by:
Instantiating UML diagrams or writing Java code (or both).
Incorporating existing classes and operations.
Explain in detail the consequences of alternatives to the pattern like duplicated code or switch statements and the consequent cost of subsequent modification.
Explain why a program conforms to a design pattern (or why not).
Demonstrate how code that implements a design pattern can be extended.
Intent: Change the interface of an object to match what a client expects.
Associates with the Adaptee, a separate object.
Accessing Adaptee requires an indirection, which introduces overhead.
Operation specificRequest
cannot be overridden directly; the Adapter would need to associate with a subclass of Adaptee.
Makes it possible to have many different Adaptee classes with different interfaces.
Allows many different Adapter classes with the same interface.
Makes it harder to override Adaptee behavior; requires subclassing Adaptee and making Adapter refer to the subclass.
Adapter inherits from Adaptee.
Operation specificRequest
can be overridden.
Commits to a particular Adaptee class, so we can’t adapt a class and all of its subclasses (unlike Object Adapter).
Intent: Ensure that when one object changes state, all its dependents are notified and updated automatically.
When a button is pressed, class TimeApp
updates just one class.
actionPerformed
method is simple.
If several display classes exist, each with different methods to call:
actionPerformed
would be complex.
It would have to change whenever a new class was added or an old class changed.
Solution: actionPerformed
only needs to update a list of display classes, all of which share the same interface.
These are Observer classes, all sharing the same interface.
There is also a Subject class, containing data to be displayed.
The coupling between the Observer and Subject is loose because Subject has to know very little about Observer.
Interface Observer
:
void update(Observable o, Object arg)
Here the new state is sent (push model) to the Observer, so it does not have to query the Observable (the pull model).
The new state must be constructed by the model.
Class Observable
:
void addObserver(Observer o){}
void deleteObserver(Observer o){}
void notifyObservers(){}
protected void setChanged(){}
Major drawback: unnecessary updates.
This can be mitigated by having different sorts of update.
Observable:
// This class extends Observable, allowing it to notify Observers of changes.
public class WeatherData extends Observable {
private float temperature, humidity, pressure;
public WeatherData() {}
// Call this method to signal that the measurements have changed.
public void measurementsChanged() {
setChanged(); // Mark the Observable object as changed
notifyObservers(); // Notify all Observers
}
// Method to set the new measurements and then notify Observers.
public void setMeasurements(float t, float h, float p){
this.temperature = t;
this.humidity = h;
this.pressure = p;
measurementsChanged(); // Notify observers that measurements have changed
}
}
Observer:
// This class implements the Observer interface to receive updates from an Observable.
public class CondsDisplay implements Observer, Display {
private Observable observable;
private float temperature, humidity;
// Constructor to register the Observer with the Observable.
public CondsDisplay(Observable observable){
this.observable = observable;
observable.addObserver(this); // Register as an Observer
}
// This method is called whenever the Observable's state changes.
public void update(Observable obs, Object arg){
// Check if the update is coming from a WeatherData object
if (obs instanceof WeatherData) {
WeatherData weatherData = (WeatherData) obs; // Cast to WeatherData
this.temperature = weatherData.getTemperature(); // Get the temperature
this.humidity = weatherData.getHumidity(); // Get the humidity
display(); // Display the updated information
}
}
}
Intent: Represent method calls as objects.
Issue requests to objects without knowing the operation or the receiver.
Used in user interface toolkits (by MenuItem, Button, etc.).
The invoker only needs to know how to run the command, commands can be undone or stored in a "log"
Using polymorphism makes it easier to add new commands.
Note similarity with Factory Method pattern.
*Compare with interface Runnable and method run() in the Java API
Invoker = Button, Command = PasteCommand, Receiver = Document
Invoker = MenuItem, Command = OpenCommand, Receiver = Application
Commands can be undone or stored in a log (e.g., ArrayList) for redoing.
Composing with the Composite pattern allows commands to be macros:
(Command * Composite)[Component = Command ∧ Composite = ConcreteCommand ∧ operation = execute]
where ∗ denotes superposition and [ ] denotes restriction
Fan Example Code
// This class implements the Command interface to encapsulate a fan's high speed setting.
public class FanHighCommand implements Command {
private Fan fan; // The receiver of the command
private int prevSpeed; // Stores the previous speed of the fan for undo operation
// Constructor to associate the command with a specific fan.
public FanHighCommand(Fan fan){
this.fan = fan;
}
// Executes the command by setting the fan to high speed.
public void execute() {
prevSpeed = fan.getSpeed(); // Save the current speed before changing it
fan.high(); // Set the fan to high speed
}
// Undoes the command by reverting the fan to its previous speed.
public void undo(){
fan.setSpeed(prevSpeed); // Restore the fan to its previous speed
}
}
Intent: Make the behavior of an object dependent on its state.
The state of an object is the values of all of its attributes.
Suppose the number of states is small, e.g., traffic lights (red, red-amber, green, amber).
Procedural approach will lead to excessive switch statements.
Represent each state by a subclass.
Changing state involves switching from one subclass to another.
State Pattern Code
// This class represents the context whose behavior changes based on its state.
public class Aeroplane {
private State landedState;
private State queueingState;//and others
private State state = skyState; // Initial state
// Constructor to initialize the different states.
public Aeroplane(){
landedState = new LandedState(this);
queueingState = new QueueingState(this);//and others
}
// Method to initiate landing; delegates to the current state.
public void land(){
state.land();
}
// Method to set the current state of the aeroplane.
public void setState(State state){
this.state = state;
}
// Example of a state implementation.
public class QueueingState implements State {
private Aeroplane aeroplane;
// Constructor to associate the state with an aeroplane.
public QueueingState (Aeroplane aeroplane){
this.aeroplane = aeroplane;
}
// Behavior when already in the queue.
public void queue(){
System.out.println("It is already in the queue!");
}
// Behavior to transition to the landed state.
public void land(){
// and others
aeroplane.setState(aeroplane.getLandedState()); // Transition to landed state
}
}
}
Further advantages are that state-specific behavior is localized, and state transitions are made explicit.
It would be slightly simpler to make each transition operation return an object representing the new state instead of setting it.
Simpler too to create each state object when needed (but wasteful of memory).
Class diagram is the same as that of Strategy, but the intent and code are different.
Adapter: Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn’t otherwise because of incompatible interfaces.
Observer: Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
Command: Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.
State: Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.