Facade Pattern
The Facade pattern provides a simplified interface to a complex subsystem. It encapsulates interactions with multiple components behind a single entry point, reducing coupling and improving usability.
Use cases:
- When a subsystem becomes too complex for direct client interaction
- In layered architectures where each layer exposes a facade to the one above
Benefits:
- Hides internal complexity
- Reduces dependencies between subsystems
- Supports the Law of Demeter (principle of least knowledge)
Drawbacks:
- May violate the Open/Closed Principle when modifying subsystem behavior
- Risk of becoming a god object if overused
Example: A points redemption service that internally coordinates inventory validation, points deduction, and logistics—all exposed through a single redeemReward() method.
Adapter Pattern
The Adapter pattern converts the interface of a class into another interface clients expect, enabling incomaptible classes to work together.
Use cases:
- Integrating legacy or third-party components with mismatched interfaces
- During system maintenance rather than initial design
Benefits:
- Promotes reuse without modifying existing code
- Decouples target and adaptee implementations
- Adheres to the Open/Closed Principle
Drawbacks:
- Increases system complexity
- Can reduce code readability
Class Adapter (via Inheritance)
interface Target {
void request();
}
class Adaptee {
public void specificRequest() {
System.out.println("Adaptee operation");
}
}
class ClassAdapter extends Adaptee implements Target {
@Override
public void request() {
specificRequest(); // Delegates to adaptee
}
}
Object Adapter (via Composition)
class ObjectAdapter implements Target {
private final Adaptee adaptee = new Adaptee();
@Override
public void request() {
adaptee.specificRequest();
}
}
Bridge Pattern
The Bridge pattern decouples an abstraction from its implementation so both can vary independently. It favors composition over inheritance to avoid class explosion.
Use cases:
- When both abstraction and implementation have multiple independent dimansions of variation
- To avoid permanent binding between interface and implementation
Benefits:
- Enables independent extension of abstraction and implementation
- Improves extensibility and maintainability
interface Account {
Account create();
void displayType();
}
class SavingsAccount implements Account {
@Override
public Account create() {
System.out.println("Savings account opened");
return this;
}
@Override
public void displayType() {
System.out.println("Savings Account");
}
}
abstract class Bank {
protected Account account;
public Bank(Account acc) { this.account = acc; }
abstract Account open();
}
class ICBC extends Bank {
public ICBC(Account acc) { super(acc); }
@Override
Account open() {
System.out.print("ICBC: ");
return account.create();
}
}
// Usage
Bank bank = new ICBC(new SavingsAccount());
Account acc = bank.open();
acc.displayType();
Composite Pattern
The Composite pattern composes objects into tree structures to represent part-whole hierarchies, allowing clients to treat individual and composite objects uniformly.
Use cases:
- Representing hierarchical structures like file systems or UI components
- When clients should ignore differences between leaf and composite objects
abstract class Component {
public void add(Component c) { throw new UnsupportedOperationException(); }
public void remove(Component c) { throw new UnsupportedOperationException(); }
public abstract void print();
}
class Leaf extends Component {
private final String name;
private final double price;
public Leaf(String name, double price) {
this.name = name;
this.price = price;
}
@Override
public void print() {
System.out.println("Course: " + name + ", Price: " + price);
}
}
class Composite extends Component {
private final List<Component> children = new ArrayList<>();
private final String title;
private final int depth;
public Composite(String title, int depth) {
this.title = title;
this.depth = depth;
}
@Override
public void add(Component c) { children.add(c); }
@Override
public void print() {
System.out.println(title);
for (Component child : children) {
for (int i = 0; i < depth; i++) System.out.print(" ");
child.print();
}
}
}
// Usage
Component javaCat = new Composite("Java Courses", 2);
javaCat.add(new Leaf("Intro to Java", 55));
javaCat.add(new Leaf("JavaWeb Basics", 66));
Component root = new Composite("Course Catalog", 1);
root.add(javaCat);
root.print();
Decorator Pattern
The Decorator pattern dynamically adds responsibilities to an object without subclassing. It provides a flexible alternative to inheritance for extending functionality.
Use cases:
- Adding features at runtime
- When combinations of extensions are needed
Drawbacks:
- Can lead to many small classes
- Nested decorators may complicate debugging
// Example: Building a pancake with toppings
Batter base = new PlainPancake();
base = new EggTopping(base);
base = new EggTopping(base); // Double egg
base = new SausageTopping(base);
System.out.println(base.getDescription() + " | Price: $" + base.cost());
Flyweight Pattern
The Flyweight pattern minimizes memory usage by sharing as much data as possible with similar objects. It distinguishes between intrinsic (shared) and extrinsic (unique) state.
Use cases:
- Applications with large numbers of fine-grained objects (e.g., text editors, game entities)
- When object creation is expensive
interface Employee {
void report();
}
class DepartmentManager implements Employee {
private final String title = "Manager"; // Intrinsic
private final String dept; // Intrinsic
private String content; // Extrinsic
public DepartmentManager(String department) {
this.dept = department;
}
public void setContent(String report) {
this.content = report;
}
@Override
public void report() {
System.out.println(content);
}
}
class EmployeePool {
private static final Map<String, Employee> cache = new HashMap<>();
public static Employee getManager(String dept) {
return cache.computeIfAbsent(dept, d -> {
DepartmentManager mgr = new DepartmentManager(d);
mgr.setContent(d + " department report: ...");
return mgr;
});
}
}
// Usage
String[] depts = {"Sales", "R&D", "HR"};
for (int i = 0; i < 10; i++) {
String d = depts[new Random().nextInt(depts.length)];
EmployeePool.getManager(d).report();
}
Proxy Pattern
The Proxy pattern provides a surrogate or placeholder for another object to control access to it.
Types:
- Static proxy: Hand-coded wrapper
- Dynamic proxy (JDK): Uses
java.lang.reflect.InvocationHandler - CGLIB proxy: Generates subclasses at runtime for non-interface types
interface GiftService {
void giveDoll();
}
class Suitor implements GiftService {
private final String recipient;
public Suitor(String name) { this.recipient = name; }
@Override
public void giveDoll() {
System.out.println(recipient + " receives a doll");
}
}
class GiftProxy implements GiftService {
private final Suitor suitor;
public GiftProxy(String name) {
this.suitor = new Suitor(name);
}
@Override
public void giveDoll() {
System.out.print("[Proxy] Delivering to ");
suitor.giveDoll();
System.out.println("... followed by a movie date");
}
}
// Usage
GiftService proxy = new GiftProxy("Alice");
proxy.giveDoll();