Fading Coder

One Final Commit for the Last Sprint

Home > Tools > Content

A Comprehensive Guide to the 23 Classic Gang of Four Design Patterns

Tools 1

Core Software Design Principles


Open/Closed Principle

Core Idea: Open for extension, closed for modification

When extending application functionality, you should avoid modifying existing working production code. Instead, use abstractions (interfaces or abstract classes) to achieve pluggable behavior. Abstraction provides high flexibility and extensibility, and varying requirements can be implemented by adding new concrete implementations without touching stable code. For example, if your application originally used RabbitMQ as a message broker, and you later need to switch to Kafka, if you coded against an abstract MessageBroker interface with a RabbitMQ implementation, you only need to add a new Kafka implementation class, with no changes required to existing working code.

Liskov Substitution Principle

Core Idea: Any instance of a base class can be replaced by any subclass without breaking functionality; subclasses should avoid overriding base class non-abstract methods.

Subclasses can extend base class functionality, but overriding base methods can create significant confusion, especially in deep inheritance hierarchies. It becomes difficult to track which method implementation is actually being called at runtime, leading to hard-to-diagnose bugs. For example: A extends Object, B extends A, C extends B. Class A defines a test() method that B overrides. If C expects to use the original test() implementation from A, it will end up calling B's overridden version by default, reducing code clarity and introducing unexpected bugs.

Dependency Inversion Principle

Core Idea: Depend on abstractions, not concrete implementations.

Directly depending on concrete details leads to tightly coupled code that is hard to extend and causes unnecessary code redundancy. Depending on abstractions keeps code loose, clean, and flexible. For example, if you need to create Car objects, creating separate concrete classes like RedElectricCar, BlackGasCar would lead to an explosion of classes. Instead, create an abstract Car class that holds references to abstract Color and PowerSource objects. Users get a custom car just by composing the required Color and PowerSource instances, with no new classes needed.

Interface Segregation Principle

Core Idea: The principle of minimal focused interfaces: only implement methods you actually use, avoid forcing implementation of unused methods with empty stubs.

If a large monolithic interface forces you to implement methods you don't need, you end up with redundant dead code. Split large interfaces into smaller, single-responsibility interfaces, so each implementation only implements the methods it actually needs. For example, a door manufacturer produces doors with optional anti-theft, fire-proof, and water-proof features. Customer A only needs a fire-proof door, while customer B needs all three features. If you split each feature into its own separate interface, you can compose exactly the required interfaces for each customer's use case with no unused code.

Law of Demeter

Core Idea: The principle of least knowledge.

If two classes do not need to communicate directly, they should not have direct dependencies. Instead, let a third party mediate the interaction to reduce coupling between classes. For example, a celebrity holding a concert. The celebrity only needs to communicate with their agent. The agent handles all coordination with ticket vendors, venue staff, and other stakeholders. The celebrity just performs as instructed, and never needs to interact directly with third-party vendors, with the agent acting as the middleman.

Composition Over Inheritance

Core Idea: Prefer composition or aggregation for code reuse, use inheritance only when appropriate.

While inheritance reduces code duplication, it has significant drawbacks: any change to the parent class affects all child classes, and overriding parent methods can introduce unexpected unsafe behavior. Composition/aggregation avoids this: you only depend on the public API of the objects you use, you don't need to care about their internal implementation, or how they might change internally, you are just a consumer of their functionality.


Creational Design Patterns

Creational patterns focus on how objects are created, and separate object creation logic from object usage. This reduces system coupling, and consumers don't need to know the details of how objects are created.

Singleton Pattern

Ensures a class has only one instance, and provides a single global access point to that instance.

Eager Initialization (Thread-Safe)

Static Variable
public class Singleton {
    private static Singleton uniqueInstance = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return uniqueInstance;
    }
}
Static Block
public class Singleton {
    private static Singleton uniqueInstance;

    static {
        uniqueInstance = new Singleton();
    }

    private Singleton() {}

    public static Singleton getInstance() {
        return uniqueInstance;
    }
}

Lazy Initialization

Unsafe Lazy Creation
public class Singleton {
    private static Singleton uniqueInstance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}
Thread-Safe Synchronized Method
public class Singleton {
    private static Singleton uniqueInstance;

    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}
Double-Checked Locking (Thread-Safe)
public class Singleton {
    private static volatile Singleton uniqueInstance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (uniqueInstance == null) {
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}
Static Inner Class (Thread-Safe)
public class Singleton {
    private Singleton() {}

    private static class SingletonHolder {
        private static Singleton uniqueInstance = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.uniqueInstance;
    }
}
Enum Singleton (Thread-Safe)
public enum Singleton {
    INSTANCE;
}

Enum singletons are inherently protected against reflection attacks.

Preventing Reflection Breakage

public class Singleton {
    private static volatile Singleton uniqueInstance;
    private static boolean constructorCalled = false;

    private Singleton() {
        synchronized (Singleton.class) {
            if (constructorCalled) {
                throw new RuntimeException("Singleton instance already exists");
            }
            constructorCalled = true;
        }
    }

    public static Singleton getInstance() {
        if (uniqueInstance == null) {
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

Real-World Examples

JDK Runtime Class (Eager Singleton)
public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    public static Runtime getRuntime() {
        return currentRuntime;
    }

    private Runtime() {}
}
Custom Shared Thread Pool (Static Inner Class)
import java.util.concurrent.*;

public class SharedThreadPool {
    private static class PoolHolder {
        private static volatile ThreadPoolExecutor INSTANCE = new ThreadPoolExecutor(
            Runtime.getRuntime().availableProcessors(),
            5 * Runtime.getRuntime().availableProcessors(),
            0L,
            TimeUnit.SECONDS,
            new SynchronousQueue<Runnable>(true),
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.CallerRunsPolicy()
        );
    }

    private SharedThreadPool() {}

    public static ThreadPoolExecutor getInstance() {
        return PoolHolder.INSTANCE;
    }
}

Simple Factory Pattern

Multiple concrete products share a common interface, and a single factory class uses conditional logic to return the correct product instance based on input.

Roles:

  • Concrete Factory: Creates product instances
  • Abstract Product: Defines common product interface
  • Concrete Product: Implements the abstract product interface

Pros: Get the required object by passing a parameter, separates object creation from business logic. Adding a new product only requires adding a new product class and updating the factory's conditional logic, no changes to existing product code. Cons: Adding new products requires modifying the factory class code, which violates the Open/Closed Principle.

// Abstract Product
abstract class Coffee {
    public abstract void brew();
}

// Concrete Product 1
class AmericanoCoffee extends Coffee {
    @Override
    public void brew() {
        // Implementation
    }
}

// Concrete Product 2
class LatteCoffee extends Coffee {
    @Override
    public void brew() {
        // Implementation
    }
}

// Concrete Factory
public class SimpleCoffeeFactory {
    public Coffee createCoffee(String type) {
        Coffee coffee = null;
        if("americano".equals(type)) {
            coffee = new AmericanoCoffee();
        } else if("latte".equals(type)) {
            coffee = new LatteCoffee();
        }
        return coffee;
    }
}

Static Factory Variant

Simply make the factory method static:

public class SimpleCoffeeFactory {
    public static Coffee createCoffee(String type) {
        // Same logic as above
        Coffee coffee = null;
        if("americano".equals(type)) {
            coffee = new AmericanoCoffee();
        } else if("latte".equals(type)) {
            coffee = new LatteCoffee();
        }
        return coffee;
    }
}

Configuration-Decoupled Simple Factory

Store product mappings in a properties file to avoid modifying factory code when adding new products: bean.properties:

americano=com.example.pattern.AmericanoCoffee
latte=com.example.pattern.LatteCoffee

Factory implementation:

public class CoffeeFactory {
    private static Map<String, Coffee> coffeeCache = new HashMap<>();

    static {
        Properties props = new Properties();
        try (InputStream is = CoffeeFactory.class.getClassLoader().getResourceAsStream("bean.properties")) {
            props.load(is);
            for (Object key : props.keySet()) {
                String className = props.getProperty((String) key);
                Class<?> clazz = Class.forName(className);
                Coffee coffee = (Coffee) clazz.newInstance();
                coffeeCache.put((String) key, coffee);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static Coffee createCoffee(String name) {
        return coffeeCache.get(name);
    }
}

This approach loads all products once on startup, and adding new products only requires adding an entry to the properties file, no code changes needed.

Factory Method Pattern

For related products of the same category. To fix the Open/Closed Principle violation in Simple Factory, define an abstract factory interface, and each concrete product gets its own corresponding concrete factory.

Definition: Defines an interface for creating objects, lets concrete factories decide which product class to instantiate. It delays product instantiation to concrete factory classes.

Roles:

  • Abstract Factory: Defines the product creation method
  • Concrete Factory: Creates specific product instances
  • Abstract Product: Defines common product functionality
  • Concrete Product: Implements product-specific functionality

Pros: Adding a new product only requires adding the product class and its corresponding concrete factory, no changes to existing code. Cons: Adds more classes to the codebase, one per product.

// Abstract Factory
public interface CoffeeFactory {
    Coffee createCoffee();
}

// Concrete Factory 1
public class LatteCoffeeFactory implements CoffeeFactory {
    @Override
    public Coffee createCoffee() {
        return new LatteCoffee();
    }
}

// Concrete Factory 2
public class AmericanoCoffeeFactory implements CoffeeFactory {
    @Override
    public Coffee createCoffee() {
        return new AmericanoCoffee();
    }
}

Real-World Example: Java's ArrayList iterator:

  • Collection = Abstract Factory
  • ArrayList = Concrete Factory
  • Iterator = Abstract Product
  • ArrayList.Itr = Concrete Product

Abstract Factory Pattern

For production of entire product families (a set of related products of different types from the same vendor/line).

Definition: An abstract factory defines creation methods for all products in a product family, and each concrete factory produces one entire product family. For example, Haier produces Haier TV, Haier Fridge, Haier Washing Machine, while Samsung produces Samsung TV, Samsung Fridge, Samsung Washing Machine.

Pros: Encapsulates entire product families, easy to switch between product families. Cons: Adding a new product type to the product family requires changes to the abstract factory and all concrete factories.

// Abstract Factory
public interface DessertFactory {
    Coffee createCoffee();
    Dessert createDessert();
}

// Concrete Factory 1: American Style Desserts
public class AmericanDessertFactory implements DessertFactory {
    @Override
    public Coffee createCoffee() {
        return new AmericanoCoffee();
    }

    @Override
    public Dessert createDessert() {
        return new MatchaMousse();
    }
}

// Concrete Factory 2: Italian Style Desserts
public class ItalianDessertFactory implements DessertFactory {
    @Override
    public Coffee createCoffee() {
        return new LatteCoffee();
    }

    @Override
    public Dessert createDessert() {
        return new Tiramisu();
    }
}

Prototype Pattern

Create new objects by cloning an existing prototype instance, instead of creating new instances from scratch. Supports two types of cloning:

  • Shallow Copy: Creates a new object, but all reference-type fields still point to the original object's references
  • Deep Copy: Creates a new object, and clones all reference-type fields aswell
// Shallow Copy Example
public class Main {
    public static void main(String[] args) throws CloneNotSupportedException {
        CountryInfo original = new CountryInfo();
        original.setName("Canada");

        CountryInfo clone = original.clone();
        clone.setName("Australia");

        System.out.println(original.getName());
        System.out.println(clone.getName());
    }
}

class CountryInfo implements Cloneable {
    private String name;

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    @Override
    protected CountryInfo clone() throws CloneNotSupportedException {
        return (CountryInfo) super.clone();
    }
}
// Deep Copy Example
public class Main {
    public static void main(String[] args) throws CloneNotSupportedException {
        UserProfile original = new UserProfile();
        original.setCountry("China");
        UserDetails details = new UserDetails();
        details.setFullName("Li Hua");
        details.setAge(18);
        original.setDetails(details);

        UserProfile clone = original.clone();
        clone.setCountry("USA");
        clone.getDetails().setFullName("Tom");
        clone.getDetails().setAge(20);

        System.out.println(original.getDetails());
        System.out.println(clone.getDetails());
    }
}

class UserProfile implements Cloneable {
    private String country;
    private UserDetails details;

    // Getters and Setters omitted

    @Override
    protected UserProfile clone() throws CloneNotSupportedException {
        UserProfile copy = (UserProfile) super.clone();
        // Deep copy the reference type field
        copy.setDetails(this.details.clone());
        return copy;
    }
}

class UserDetails implements Cloneable {
    private String fullName;
    private Integer age;

    // Getters and Setters, toString omitted

    @Override
    protected UserDetails clone() throws CloneNotSupportedException {
        return (UserDetails) super.clone();
    }
}

Output:

UserDetails{name='Li Hua', age=18}
UserDetails{name='Tom', age=20}

Builder Pattern

For constructing complex objects that require multiple step-by-step creation. Separate the construction logic from the final product, so the same construction process can create different representations.

Roles:

  • Director: Controls the order of construction steps, delegates actual work to builders
  • Abstract Builder: Defines all required construction steps
  • Concrete Builder: Implements the actual construction steps for a specific product
  • Product: The final constructed object
public class Client {
    public static void main(String[] args) {
        Director director = new Director(new MobikeBuilder());
        System.out.println(director.constructBike());
    }
}

// Director
class Director {
    private Builder builder;

    public Director(Builder builder) {
        this.builder = builder;
    }

    public Bicycle construct() {
        builder.buildFrame();
        builder.buildSeat();
        return builder.createBike();
    }
}

// Abstract Builder
abstract class Builder {
    protected Bicycle bike = new Bicycle();
    public abstract void buildFrame();
    public abstract void buildSeat();
    public abstract Bicycle createBike();
}

// Concrete Builder 1
class MobikeBuilder extends Builder {
    @Override
    public void buildFrame() {
        bike.setFrame("Mobike Aluminum Frame");
    }

    @Override
    public void buildSeat() {
        bike.setSeat("Mobike Comfort Seat");
    }

    @Override
    public Bicycle createBike() {
        return bike;
    }
}

// Concrete Builder 2
class OfoBuilder extends Builder {
    @Override
    public void buildFrame() {
        bike.setFrame("Ofo Steel Frame");
    }

    @Override
    public void buildSeat() {
        bike.setSeat("Ofo Basic Seat");
    }

    @Override
    public Bicycle createBike() {
        return bike;
    }
}

// Product
class Bicycle {
    private String frame;
    private String seat;

    // Getters, Setters, toString omitted
}

Output:

Bicycle{frame='Mobike Aluminum Frame', seat='Mobike Comfort Seat'}

Structural Design Patterns

Structural patterns describe how to combine classes and objects into larger, more flexible structures. They are split into class structural patterns (use inheritance to combine interfaces) and object structural patterns (use composition/aggregation to combine objects). Composition has lower coupling than inheritance, so object structural patterns are generally preferred.

Proxy Pattern

When you cannot or should not reference a target object directly, create a proxy object that stands in for the target, and can add additional functionality before/after forwarding calls to the target.

Proxy types:

  • Static Proxy: Proxy class is created at compile time
  • Dynamic Proxy: Proxy class is generated at runtime
    • JDK Dynamic Proxy: Requires target class to implement interfaces
    • CGLIB Dynamic Proxy: Works for non-interface classes, generates a proxy subclass of the target, requires target and methods not to be final

Static Proxy Example (Buying Train Tickets):

public class Main {
    public static void main(String[] args) {
        ErrandBoy proxy = new ErrandBoy();
        proxy.buyTicket();
    }
}

interface TicketVendor {
    void buyTicket();
}

// Real Subject
class TrainStation implements TicketVendor {
    @Override
    public void buyTicket() {
        System.out.println("Purchased ticket from train station");
    }
}

// Proxy
class ErrandBoy implements TicketVendor {
    private TrainStation station = new TrainStation();

    @Override
    public void buyTicket() {
        System.out.println("Errand boy goes to the station...");
        station.buyTicket();
        System.out.println("Errand boy delivers ticket to customer.");
    }
}

Adapter Pattern

Convert the interface of an existing adaptee class into the interface that clients expect, so incompatible interfaces can work together without modifying the original adaptee code.

Common example: A phone charger takes 220V from the wall and converts it to 5V for the phone, the charger is the adapter.

Types:

  • Class Adapter: Adapter inherits from the adaptee class
  • Object Adapter: Adapter holds a reference to an adaptee instance (more flexible, preferred)
  • Interface Adapter: An abstract adapter class implements all methods of a large interface with empty defaults, so subclasses only need to override the methods they need

Object Adapter Example (Voltage Conversion):

public class Main {
    public static void main(String[] args) {
        Phone phone = new Phone();
        phone.charge();
    }
}

// Adaptee: 220V Outlet
interface WallOutlet220V {
    void output220V();
}

class HomeOutlet implements WallOutlet220V {
    @Override
    public void output220V() {
        System.out.println("Outlet outputs 220V");
    }
}

// Target Interface: 5V for phone
interface Output5V {
    void output5V();
}

// Adapter
class ChargerAdapter implements Output5V {
    private WallOutlet220V outlet = new HomeOutlet();

    @Override
    public void output5V() {
        outlet.output220V();
        System.out.println("Adapter converts 220V to 5V");
    }
}

// Client
class Phone {
    private Output5V adapter = new ChargerAdapter();

    public void charge() {
        adapter.output5V();
        System.out.println("Phone charges with 5V");
    }
}

Output:

Outlet outputs 220V
Adapter converts 220V to 5V
Phone charges with 5V

Decorator Pattern

Dynamically add additional functionality to an existing object without modifying its original structure, by wrapping the original object in a decorator class. Avoids class explosion from inheritance when adding multiple combinations of features.

public class Main {
    public static void main(String[] args) {
        Garnish order = new Bacon(new Egg(new Noodle()));
        System.out.println("Order: " + order.getDescription());
        System.out.println("Total Price: " + order.getPrice() + " USD");
    }
}

// Abstract Component
abstract class FastFood {
    protected int price;
    protected String description;

    public abstract int getPrice();
    public abstract String getDescription();

    public FastFood() {}
    public FastFood(int price, String description) {
        this.price = price;
        this.description = description;
    }
}

// Concrete Component 1
class Noodle extends FastFood {
    public Noodle() {
        super(5, "Noodle");
    }

    @Override
    public int getPrice() { return price; }

    @Override
    public String getDescription() { return description; }
}

// Abstract Decorator
abstract class Garnish extends FastFood {
    private FastFood base;

    public Garnish(int price, String description, FastFood base) {
        super(price, description, base);
        this.base = base;
    }

    @Override
    public int getPrice() {
        return super.getPrice() + base.getPrice();
    }

    @Override
    public String getDescription() {
        return base.getDescription() + " + " + super.description;
    }
}

// Concrete Decorator 1
class Bacon extends Garnish {
    public Bacon(FastFood base) {
        super(2, "Bacon", base);
    }
}

// Concrete Decorator 2
class Egg extends Garnish {
    public Egg(FastFood base) {
        super(1, "Egg", base);
    }
}

Output:

Order: Noodle + Egg + Bacon
Total Price: 8 USD

Real-World Example: Java's BufferedWriter is a decorator for FileWriter, adding buffered IO functionality without changing the original FileWriter API.

Bridge Pattern

When a class has multiple independent dimensions of variation, inheritance leads to class explosion. Use composition to combine the dimensions, decouple the different dimensions so they can vary independently.

For example, shapes can have different colors: if you use inheritance, you get RedCircle, BlueCircle, RedSquare, BlueSquare etc. With bridge, you have Shape that holds a Color reference, so adding a new color only requires one new class, adding a new shape only requires one new class, no explosion.

public class Main {
    public static void main(String[] args) {
        OS windows = new Windows(new Mp4Decoder());
        windows.playVideo();
    }
}

// Implementor: Video Format (one dimension)
interface VideoFormat {
    void decode();
}

class Mp4Decoder implements VideoFormat {
    @Override
    public void decode() {
        System.out.println("Decode video in MP4 format");
    }
}

class AviDecoder implements VideoFormat {
    @Override
    public void decode() {
        System.out.println("Decode video in AVI format");
    }
}

// Abstraction: Operating System (second dimension)
abstract class OperatingSystem {
    protected VideoFormat decoder;

    public OperatingSystem(VideoFormat decoder) {
        this.decoder = decoder;
    }

    public abstract void playVideo();
}

class Windows extends OperatingSystem {
    public Windows(VideoFormat decoder) {
        super(decoder);
    }

    @Override
    public void playVideo() {
        System.out.println("Playing video on Windows OS");
        decoder.decode();
    }
}

Output:

Playing video on Windows OS
Decode video in MP4 format

Facade Pattern

Provide a unified simplified entry point to a set of complex subsystems, making the subsystem easier to use, and reducing coupling between clients and individual subsystems. Follows the Law of Demeter.

Example: A smart home speaker that turns on all your appliances when you say "I'm home". The speaker is the facade, client just sends one command, no need to call each appliance individually.

public class Main {
    public static void main(String[] args) {
        SmartSpeaker facade = new SmartSpeaker();
        facade.executeCommand("welcome home");
    }
}

// Facade
class SmartSpeaker {
    private Light light = new Light();
    private AirConditioner ac = new AirConditioner();
    private Tv tv = new Tv();

    public void executeCommand(String command) {
        if (command.contains("welcome home")) {
            turnOnAll();
            System.out.println("All appliances turned on");
        } else if (command.contains("good night")) {
            turnOffAll();
            System.out.println("All appliances turned off");
        }
    }

    private void turnOnAll() {
        light.turnOn();
        ac.turnOn();
        tv.turnOn();
    }

    private void turnOffAll() {
        light.turnOff();
        ac.turnOff();
        tv.turnOff();
    }
}

// Subsystems
class Light {
    public void turnOn() { System.out.println("Turning on light"); }
    public void turnOff() { System.out.println("Turning off light"); }
}

class AirConditioner {
    public void turnOn() { System.out.println("Turning on air conditioner"); }
    public void turnOff() { System.out.println("Turning off air conditioner"); }
}

class Tv {
    public void turnOn() { System.out.println("Turning on TV"); }
    public void turnOff() { System.out.println("Turning off TV"); }
}

Output:

Turning on light
Turning on air conditioner
Turning on TV
All appliances turned on

Composite Pattern

Compose objects into tree structures to represent part-whole hierarchies. Clients treat individual objects and compositions of objects uniformly. Commonly used for tree-shaped structures like file systems, menus.

Types:

  • Transparent Composite: All management methods (add/remove/get child) are defined in the abstract root, all nodes have the same interface. Standard but unsafe, since leaf nodes don't need these methods and will throw exceptions if called.
  • Safe Composite: Only composite (branch) nodes have add/remove methods, leaf nodes don't implement them. Safer, but less transparent.
// Transparent Composite Example: Menu Structure
abstract class MenuComponent {
    protected String name;
    protected int level;

    public void add(MenuComponent component) {
        throw new UnsupportedOperationException();
    }

    public void remove(MenuComponent component) {
        throw new UnsupportedOperationException();
    }

    public MenuComponent getChild(int index) {
        throw new UnsupportedOperationException();
    }

    public String getName() { return name; }

    public abstract void print();
}

// Branch (Composite) Node: Menu
class Menu extends MenuComponent {
    private List<MenuComponent> children = new ArrayList<>();

    public Menu(String name, int level) {
        this.name = name;
        this.level = level;
    }

    @Override
    public void add(MenuComponent component) {
        children.add(component);
    }

    @Override
    public void remove(MenuComponent component) {
        children.remove(component);
    }

    @Override
    public MenuComponent getChild(int i) {
        return children.get(i);
    }

    @Override
    public void print() {
        for (int i = 1; i < level; i++) System.out.print("--");
        System.out.println(name);
        for (MenuComponent child : children) {
            child.print();
        }
    }
}

// Leaf Node: Menu Item
class MenuItem extends MenuComponent {
    public MenuItem(String name, int level) {
        this.name = name;
        this.level = level;
    }

    @Override
    public void print() {
        for (int i = 1; i < level; i++) System.out.print("--");
        System.out.println(name);
    }
}

Flyweight Pattern

Share reusable objects to reduce memory usage when you need many similar objects. Flyweight pattern extracts shared immutable state (intrinsic state) into a flyweight pool, and reuses flyweight instances instead of creating new ones for every request.

Example: Tetris game only has 7 distinct block shapes, so you just reuse the same 7 objects instead of creating a new one every time a block spawns. Another example: Java's Integer caches all instances from -128 to 127, since these are commonly used.

public class Main {
    public static void main(String[] args) {
        AbstractBlock block = BlockFactory.getBlock("I");
        System.out.println("Block shape: " + block.getShape());
    }
}

// Flyweight Factory
class BlockFactory {
    private static final Map<String, AbstractBlock> blockPool = new HashMap<>();

    static {
        blockPool.put("I", new IBlock());
        blockPool.put("L", new LBlock());
        blockPool.put("O", new OBlock());
    }

    public static AbstractBlock getBlock(String shape) {
        return blockPool.get(shape);
    }
}

// Abstract Flyweight
abstract class AbstractBlock {
    public abstract String getShape();
}

// Concrete Flyweights
class IBlock extends AbstractBlock {
    @Override
    public String getShape() { return "I"; }
}

class LBlock extends AbstractBlock {
    @Override
    public String getShape() { return "L"; }
}

class OBlock extends AbstractBlock {
    @Override
    public String getShape() { return "O"; }
}

Behavioral Design Patterns

Behavioral patterns focus on communication between objects, and how responsibilities are distributed between them.

Template Method Pattern

Define the skeleton of an algorithm (sequence of steps) in an abstract class, and defer implementation of varying steps to subclasses. Subclasses can redefine specific steps of the algorithm without changing the algorithm's structure.

Example: Cooking two different dishes: the steps are always pour oil → add ingredients → stir fry. Only the add ingredients step varies, so that's left to subclasses to implement.

public class Main {
    public static void main(String[] args) {
        AbstractCooking recipe = new PotatoPepperStirFry();
        recipe.cook();
    }
}

// Abstract Class with Template Method
abstract class AbstractCooking {
    // Template method: defines step order, final to prevent overriding
    public final void cook() {
        pourOil();
        addIngredients();
        stirFry();
    }

    // Concrete step: same for all recipes
    protected void pourOil() {
        System.out.println("Pour cooking oil into pan");
    }

    // Abstract step: varies per recipe
    protected abstract void addIngredients();

    // Concrete step: same for all recipes
    protected void stirFry() {
        System.out.println("Stir fry ingredients");
    }
}

// Concrete Subclass 1
class PotatoPepperStirFry extends AbstractCooking {
    @Override
    protected void addIngredients() {
        System.out.println("Add potatoes and green peppers");
    }
}

// Concrete Subclass 2
class TomatoEgg extends AbstractCooking {
    @Override
    protected void addIngredients() {
        System.out.println("Add tomatoes and eggs");
    }
}

Output:

Pour cooking oil into pan
Add potatoes and green peppers
Stir fry ingredients

Real-World Example: Java's InputStream uses template method: read(byte[] b) is the template method that calls the abstract read() method, which is implemented by concrete subclasses like FileInputStream.

Strategy Pattern

Define a family of interchangeable algorithms, encapsulate each one, and make them interchangeable. The algorithm can vary independently from the client that uses it.

Eliminates messy chains of if-else statements by turning each branch into a separate strategy class.

public class Main {
    public static void main(String[] args) {
        MessageBroker broker = new RabbitMqBroker();
        broker.bindUser("user_123");
    }
}

// Abstract Strategy
interface MessageBroker {
    void bindUser(String... userIds);
    void bindGroup(String... groupIds);
}

// Concrete Strategy 1
class KafkaBroker implements MessageBroker {
    @Override
    public void bindUser(String... userIds) {
        System.out.println("Bind user using Kafka");
    }

    @Override
    public void bindGroup(String... groupIds) {
        System.out.println("Bind group using Kafka");
    }
}

// Concrete Strategy 2
class RabbitMqBroker implements MessageBroker {
    @Override
    public void bindUser(String... userIds) {
        System.out.println("Bind user using RabbitMQ");
    }

    @Override
    public void bindGroup(String... groupIds) {
        System.out.println("Bind group using RabbitMQ");
    }
}

Real-World Example: Java's Comparator interface: different comparators are different sorting strategies, you can pass any comparator to Collections.sort() to change how the list is sorted.

Command Pattern

Encapsulate a request as an object, which decouples the requester of the request from the receiver that executes it. The requester only knows how to send the command, it doesn't need to know how the request is executed.

Roles:

  • Abstract Command: Declares the execute method
  • Concrete Command: Implements execute, usually holds a reference to the receiver, and calls receiver methods to execute the request
  • Invoker: Holds command objects, invokes commands when requested
  • Receiver: Executes the actual business logic of the command

Example: Restaurant ordering: Waiter (invoker) takes orders (commands), each order tells the chef (receiver) to cook the requested food.

public class Main {
    public static void main(String[] args) {
        Map<String, Integer> order1Items = new HashMap<>();
        order1Items.put("Hulatang", 2);
        order1Items.put("Youtiao", 2);
        Order order1 = new Order("Table 1", order1Items);

        OrderCommand cmd1 = new OrderCommand(new Chef(), order1);
        List<OrderCommand> commands = new ArrayList<>();
        commands.add(cmd1);

        Waiter waiter = new Waiter(commands);
        waiter.submitOrders();
    }
}

// Abstract Command
interface Command {
    void execute();
}

// Concrete Command
class OrderCommand implements Command {
    private Chef chef;
    private Order order;

    public OrderCommand(Chef chef, Order order) {
        this.chef = chef;
        this.order = order;
    }

    @Override
    public void execute() {
        System.out.println(">>> Start cooking for " + order.getTableNumber() + "");
        for (Map.Entry<String, Integer> entry : order.getItems().entrySet()) {
            chef.cook(entry.getKey(), entry.getValue());
        }
        System.out.println(">>> Finished cooking for " + order.getTableNumber() + "\n");
    }
}

// Invoker
class Waiter {
    private List<OrderCommand> commands;

    public Waiter(List<OrderCommand> commands) {
        this.commands = commands;
    }

    public void submitOrders() {
        for (OrderCommand cmd : commands) {
            cmd.execute();
        }
    }
}

// Receiver
class Chef {
    public void cook(String food, int quantity) {
        System.out.println(quantity + " serving(s) of '

Related Articles

Efficient Usage of HTTP Client in IntelliJ IDEA

IntelliJ IDEA incorporates a versatile HTTP client tool, enabling developres to interact with RESTful services and APIs effectively with in the editor. This functionality streamlines workflows, replac...

Installing CocoaPods on macOS Catalina (10.15) Using a User-Managed Ruby

System Ruby on macOS 10.15 frequently fails to build native gems required by CocoaPods (for example, ffi), leading to errors like: ERROR: Failed to build gem native extension checking for ffi.h... no...

Resolve PhpStorm "Interpreter is not specified or invalid" on WAMP (Windows)

Symptom PhpStorm displays: "Interpreter is not specified or invalid. Press ‘Fix’ to edit your project configuration." This occurs when the IDE cannot locate a valid PHP CLI executable or when the debu...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.