Design Patterns in Java
- The core of solutions to common problems
- All patterns have: name, the problem they solve (in context), solution, consequences
Types of patterns:
- Creational - abstraction of instantiation process
- Structural - composition of classes and objects in formation of larger structures
- Behavioral - abstraction of algorithms and assignment of responsibility between objects
Why reinvent the wheel when you can follow a field-tested blueprint for solving your not-so-unique problem?
Many of the design patterns in use today and described here originate from the famous Gang of Four book: "Design Patterns: Elements of Reusable Object-Oriented Software" by Eric Gamma, Richard Helm, Ralph Johnson, and John Vlissides (Addison-Wesley Publishing Co., 1995; ISBN: 0201633612)
Allow only one instance of a given class
- The class holds a reference to its only instance
- Lazy (on request) or static initialization (on load)
- Private constructor to restrict external instantiation
Provide global point of access to the instance
public static ClassName getInstance()
Support multiple instances in the future if the requirements change
-
Update
getInstance
to return multiple (possibly cached) instances if needed
-
Update
public class MySingleton { private static final MySingleton instance = new MySingleton(); public static MySingleton getInstance() { return instance; } private MySingleton() {} // no client can do: new MySingleton() public void doX() {…} public void doY() {…} public String getA() {return …;} public int getB() {return …;} } public class Client { public void doSomething() { // doing something MySingleton.getInstance().doX(); // doing something else int b = MySingleton.getInstance().getB(); // do something with b } }
- Define an interface for creating an object
- Defer the actual creation to subclasses
- Localizes the knowledge of concrete instances
- Use when clients cannot anticipate the class of objects to create
public interface Logger { public void error(String msg); public void debug(String msg); } public class ConsoleLogger implements Logger { public void error(String msg) {System.err.println("ERROR: "+ msg);} public void debug(String msg) {System.err.println("DEBUG: "+ msg);} } public class FileLogger implements Logger { private PrintStream out; private FileLogger(String file) throws IOException { this.out = new PrintStream(new FileOutputStream(file), true); } public void error(String msg) {out.println("ERROR: "+ msg);} public void debug(String msg) {out.println("DEBUG: "+ msg);} } public abstract class LoggerFactory { public abstract Logger getLogger(); public static LoggerFactory getFactory(String f) throws Exception { return (LoggerFactory ) Class.forName(f).newInstance(); } } public class ConsoleLoggerFactory extends LoggerFactory { public Logger getLogger() { return new ConsoleLogger(); } } public class FileLoggerFactory extends LoggerFactory { //ignoring expt private Logger log = new FileLogger(System.getProperty("log.file")); public Logger getLogger() { return log; } }
- Interface for creating family of related objects
- Clients are independent of how the objects are created, composed, and represented
- System is configured with one of multiple families of products
public interface Restaurant { public Appetizer getAppetizer(); public Entree getEntree(); public Dessert getDessert(); } public interface Appetizer { public void eat(); } public interface Entree { public void eat(); } public interface Dessert { public void enjoy(); } public class AmericanRestaurant implements Restaurant { public Appetizer getAppetizer() { return new Oysters(); } public Entree getEntree() { return new Steak(); } public Dessert getDessert() { return new CheeseCake(); } } public class ItalianRestaurant implements Restaurant { public Appetizer getAppetizer() { return new Pizzette(); } public Entree getEntree() { return new Pasta(); } public Dessert getDessert() { return new Gelato(); } } public class Oysters implements Appetizer { public void eat() { System.out.println("Eating Rocky Mountain Oysters"); } } public class Gelato implements Dessert { public void enjoy() { System.out.println("Enjoying ice cream"); } }
- Convert the interface of a class into another interface clients expect
- Allow classes to work together despite their incompatible interfaces
- Typically used in development to glue components together, especially if 3rd party libraries are used
- Also known as wrapper
/** * Adapts Enumeration interface to Iterator interface. * Does not support remove() operation. */ public class EnumerationAsIterator implements Iterator { private final Enumeration enumeration; public EnumerationAsIterator(Enumeration enumeration) { this.enumeration = enumeration; } public boolean hasNext() { return this.enumeration.hasMoreElements(); } public Object next() { return this.enumeration.nextElement(); } /** * Not supported. * @throws UnsupportedOperationException if invoked */ public void remove() { throw new UnsupportedOperationException("Cannot remove"); } }
- Compose objects into three structures to represent part-whole hierarchies
- Treat individual objects and compositions of objects uniformly
public interface Executable { // Component public void execute(); } public class XExecutable implements Executable { // Leaf X public void execute() { /* do X */ } } public class YExecutable implements Executable { // Leaf Y public void execute() { /* do Y */ } } public class ExecutableCollection implements Executable { // Composite protected Collection executables = new LinkedList(); public void addExecutable(Executable executable) { this.executables.add(executable); } public void removeExecutable(Executable executable) { this.executables.remove(executable); } public void execute() { for (Iterator i = this.executables.iterator(); i.hasNext();) { ((Executable) i.next()).execute(); } } }
- Attach additional responsibilities to an object dynamically and transparently (run-time)
- An alternative to sub classing (compile-time)
- Remove responsibilities no longer needed
A typical example of the decorator pattern can be found in java.io
: FilterOutputStream
, FilterInputStream
, FilterReader
, and FilterWriter
.
For example, observe how we can decorate an OutputStream
:
OutputStream out = new FileOutputStream("somefile"); out = new BufferedOutputStream(out); // buffer before writing out = CipherOutputStream(out, // encrypt all data Cipher.getInstance("DES/CBC/PKCS5Padding")); out = new ZipOutputStream(out); // compress all data To write our own decorator, we could do: public ByteCounterOutputStream extends FilterOutputStream { private int counter = 0; public ByteCounterOutputStream(OutputStream out) {super(out);} public void write(int b) throws IOException { super.write(b); this.counter++; } public int getCounter() {return this.counter;} }
We could then wrap out with a byte counter decorator:
ByteCounterOutputStream bout = new ByteCounterOutputStream(out); for (int b = in.read(); b != -1; b = in.read()) { bout.write(b); } System.out.println("Written "+ bout.getCounter() + " bytes");
- Decouple senders of requests from receivers
- Allow more than one object to attempt to handle a request
- Pass the request along a chain of receivers until it is handled
Only one object handles the request
-
Servlet
Filter
andFilterChain
bend this rule
-
- Some requests might not be handled
public abstract class MessageHandler { private MessageHandler next; public MessageHandler(MessageHandler next) { this.next = next; } public void handle(Message message) { if (this.next != null) {this.next.handle(message); } } } public class SpamHandler extends MessageHandler { public SpamHandler(MessageHandler next) { super(next); } public void handle(Message message) { if (isSpam(message)) { // handle spam } else { super.handle(message); } } ... }
If a client was given an instance of a handler (created at run time):
MessageHandler handler = new BlackListHandler(new SpamHandler(new ForwardingHandler(new DeliveryHandler(null))));
The client would simply do:
handler.handle(emailMessage);
- Define a one-to-many dependency between objects
- When the observed object (subject) changes state, all dependents get notified and updated automatically
-
java.util.Observable
,java.util.Observer
public class Employee extends Observable { ... public void setSalary(double amount) { this.salary = amount; super.setChanged(); super.notifyObservers(); } } public class Manager extends Employee implements Observer { public void update(Observable o) { if (o instanceof Employee) { // note employee's salary, vacation days, etc. } } } public class Department implements Observer { public void public void update(Observable o) { if (o instanceof Employee) { // deduct employees salary from department budget } } } public class Accounting implements Observer { public void public void update(Observable o) { if (o instanceof Employee) { // verify that monthly expenses are in line with the forcast } } }
- Defined family of interchangeable and encapsulated algorithms
- Change algorithms independently of clients that use them
-
E.g:
java.util.Comparator
,java.io.FileFilter
public interface Comparator { public int compare(Object o1, Object o2); } public class DateComparator implements Comparator { public int compare(Object o1, Object o2) { return ((Date) o1).compareTo((Date) o2); } } public class StringIntegerComparator implements Comparator { public int compare(Object o1, Object o2) { return Integer.parseInt((String) o1) - Integer.parseInt((String) o2); } } public class ReverseComparator implements Comparator { private final Comparator c; public ReverseComparator(Comparator c) {this.c = c; } public int compare(Object o1, Object o2) { return c.compare(o2, o1); } }
To sort integers represented as strings in descending order, you could do:
Arrays.sort(stringArray, new ReverseComparator(new StringIntegerComparator()));
The sort algorithm is independent of the comparison strategy.
- Define a skeleton of an algorithm
- Defer some steps to subclasses
- Redefine other steps in subclasses
- Often a result of refactoring common code
public interface Calculator { public void calculate(double operand); public double getResult(); } public abstract CalculatorTemplate implements Calculator() { private double result; private boolean initialized = false; public final void calculate(double operand) { if (this.initialized) { this.result = this.doCalculate(this.result, operand); } else { this.result = operand; this.initialized = true; } } public final double getResult() { return this.result; // throw exception if !initialized } protected abstract doCalculate(double o1, double o2); } public class AdditionCalculator extends CalculatorTemplate { protected doCalculate(double o1, double o1) {return o1 + o2; } } public class SubtractionCalculator extends CalculatorTemplate { protected doCalculate(double o1, double o1) {return o1 - o2; } }
- Abstracts and encapsulates all access to a data source
- Manages the connection to the data source to obtain and store data
- Makes the code independent of the data sources and data vendors (e.g. plain-text, xml, LDAP, MySQL, Oracle, DB2)
public class Customer { private final String id; private String contactName; private String phone; public void setId(String id) { this.id = id; } public String getId() { return this.id; } public void setContactName(String cn) { this.contactName = cn;} public String getContactName() { return this.contactName; } public void setPhone(String phone) { this.phone = phone; } public String getPhone() { return this.phone; } } public interface CustomerDAO { public void addCustomer(Customer c) throws DataAccessException; public Customer getCustomer(String id) throws DataAccessException; public List getCustomers() throws DataAccessException; public void removeCustomer(String id) throws DataAccessException; public void modifyCustomer(Customer c) throws DataAccessException; } public class MySqlCustomerDAO implements CustomerDAO { public void addCustomer(Customer c) throws DataAccessException { Connection con = getConnection(); PreparedStatement s = con.createPreparedStatement("INSERT ..."); ... s.executeUpdate(); ... } ... }