The book "Design Patterns" by Laurent DEBRAUWER is so well written that a lot of people recommended it. Today I have read it and found a simple way to understand all the 23 design patterns illustrated in the book.
So the global context is based on an online car-selling website “SpeedCars”, and Emily is the Java developer.
1. Singleton Pattern
Story Background
At SpeedCars, the company uses a database to store all vehicle and user information. To ensure efficient operation, the database connection object must be globally unique.
Problem
Multiple parts of the system were trying to create database connection objects, leading to resource wastage and management difficulties.
Need
Ensure there is only one database connection object in the system, providing a global access point.
Solution
Emily used the Singleton Pattern to create a single, globally unique database connection object.
class DatabaseConnection {
private static DatabaseConnection instance;
private DatabaseConnection() {
// Initialize database connection
}
public static DatabaseConnection getInstance() {
if (instance == null) {
instance = new DatabaseConnection();
}
return instance;
}
}
2. Factory Pattern
Story Background
SpeedCars has different types of users, such as buyers, sellers, and administrators, each with different characteristics and behaviors.
Problem
Creating user objects required knowing the specific class each time, making the code hard to maintain and extend.
Need
Create different types of user objects based on input parameters without specifying the concrete class in the client code.
Solution
Emily used the Factory Pattern to create different types of user objects.
class UserFactory {
public static User createUser(String userType) {
switch (userType) {
case "buyer":
return new Buyer();
case "seller":
return new Seller();
case "admin":
return new Admin();
default:
throw new IllegalArgumentException("Unknown user type");
}
}
}
3. Abstract Factory Pattern
Story Background
SpeedCars sells various brands of cars, each with its own set of parts, such as engines and tires.
Problem
Creating a series of related or dependent objects required knowing their specific classes, making the code complex and hard to maintain.
Need
Create a set of related or dependent objects without specifying their concrete classes.
Solution
Emily used the Abstract Factory Pattern to create objects for different car brands and their parts.
interface CarFactory {
Engine createEngine();
Tire createTire();
}
class ToyotaFactory implements CarFactory {
public Engine createEngine() {
return new ToyotaEngine();
}
public Tire createTire() {
return new ToyotaTire();
}
}
4. Builder Pattern
Story Background
SpeedCars offers custom car services where customers can choose different configurations, such as engines, interiors, and colors.
Problem
Building complex car objects required a flexible step-by-step process, which traditional constructors couldn’t handle well.
Need
Build complex car objects step-by-step in a flexible manner.
Solution
Emily used the Builder Pattern to build complex car objects.
class CarBuilder {
private Car car;
public CarBuilder() {
car = new Car();
}
public CarBuilder setEngine(String engine) {
car.setEngine(engine);
return this;
}
public CarBuilder setInterior(String interior) {
car.setInterior(interior);
return this;
}
public Car build() {
return car;
}
}
5. Prototype Pattern
Story Background
Customers at SpeedCar often want to quickly create multiple copies of an existing car model and make slight adjustments to each copy.
Problem
Initializing new objects from scratch every time was cumbersome.
Need
Quickly create new objects by copying existing ones.
Solution
Emily used the Prototype Pattern to create new car objects by cloning existing ones.
class Car implements Cloneable {
private String engine;
private String color;
public Car clone() throws CloneNotSupportedException {
return (Car) super.clone();
}
}
6. Adapter Pattern
Story Background
SpeedCars needs to integrate car data interfaces from different suppliers, each with different designs and standards.
Problem
Incompatible interfaces made system integration complex.
Need
Adapt different interfaces to a common standard.
Solution
Emily used the Adapter Pattern to make different supplier interfaces compatible with the system.
interface CarData {
void getCarInfo();
}
class ExternalCarData {
public void fetchCarDetails() {
// Fetch car details
}
}
class CarDataAdapter implements CarData {
private ExternalCarData externalCarData;
public CarDataAdapter(ExternalCarData externalCarData) {
this.externalCarData = externalCarData;
}
public void getCarInfo() {
externalCarData.fetchCarDetails();
}
}
7. Bridge Pattern
Story Background
SpeedCars sells different brands and models of cars, each with its own features and implementations.
Problem
The coupling of brands and models made the system difficult to extend.
Need
Separate the implementation of brands and models to allow independent extension.
Solution
Emily used the Bridge Pattern to separate car brands from car models.
interface Brand {
void showBrand();
}
class Toyota implements Brand {
public void showBrand() {
System.out.println("Toyota");
}
}
abstract class Car {
protected Brand brand;
public Car(Brand brand) {
this.brand = brand;
}
abstract void showDetails();
}
class Sedan extends Car {
public Sedan(Brand brand) {
super(brand);
}
public void showDetails() {
brand.showBrand();
System.out.println("Sedan");
}
}
8. Composite Pattern
Story Background
SpeedCars’ car parts have a complex hierarchical structure, such as engines, and tires, each made up of smaller components.
Problem
Handling the hierarchical structure required treating leaf and composite objects uniformly.
Need
Handle the hierarchical structure of car parts, treating leaf and composite objects the same way.
Solution
Emily used the Composite Pattern to manage the hierarchical structure of car parts.
interface CarComponent {
void showDetails();
}
class Engine implements CarComponent {
public void showDetails() {
System.out.println("Engine");
}
}
class CarComposite implements CarComponent {
private List<CarComponent> components = new ArrayList<>();
public void addComponent(CarComponent component) {
components.add(component);
}
public void showDetails() {
for (CarComponent component : components) {
component.showDetails();
}
}
}
9. Decorator Pattern
Story Background
SpeedCars offers various upgrade services, such as adding a navigation system, upgrading the audio system, etc.
Problem
Need to dynamically add different features to car objects without affecting other objects.
Need
Dynamically add features to car objects.
Solution
Emily used the Decorator Pattern to add different features to cars.
class Car {
public String getDescription() {
return "Basic Car";
}
public int getCost() {
return 5000;
}
}
class CarDecorator extends Car {
protected Car car;
public CarDecorator(Car car) {
this.car = car;
}
public String getDescription() {
return car.getDescription();
}
public int getCost() {
return car.getCost();
}
}
class NavigationSystem extends CarDecorator {
public NavigationSystem(Car car) {
super(car);
}
public String getDescription() {
return car.getDescription() + ", Navigation System";
}
public int getCost() {
return car.getCost() + 1500;
}
}
10. Facade Pattern
Story Background
SpeedCars’ system involves multiple complex subsystems for car search and purchase processes.
Problem
Users need to know the details of multiple subsystems, making the operation complex.
Need
Provide a unified interface to simplify the user operations for car search and purchase processes.
Solution
Emily used the Facade Pattern to simplify user operations.
class CarSearch {
public void searchCar() {
System.out.println("Searching Car");
}
}
class CarPurchase {
public void buyCar() {
System.out.println("Buying Car");
}
}
class CarFacade {
private CarSearch carSearch;
private CarPurchase carPurchase;
public CarFacade() {
carSearch = new CarSearch();
carPurchase = new CarPurchase();
}
public void searchAndBuyCar() {
carSearch.searchCar();
carPurchase.buyCar();
}
}
11. Flyweight Pattern
Story Background
SpeedCars displays many cars with similar features, such as color, model, etc., which require a lot of memory to store.
Problem
Duplicated car features consume a lot of memory, affecting system performance.
Need
Share the same feature data to reduce memory consumption.
Solution
Emily used the Flyweight Pattern to share common car features.
class CarModel {
private String model;
public CarModel(String model) {
this.model = model;
}
public String getModel() {
return model;
}
}
class CarModelFactory {
private static final Map<String, CarModel> models = new HashMap<>();
public static CarModel getCarModel(String model) {
CarModel carModel = models.get(model);
if (carModel == null) {
carModel = new CarModel(model);
models.put(model, carModel);
}
return carModel;
}
}
12. Proxy Pattern
Story Background
Some high-end car data at SpeedCars needs authorization before it
can be accessed.
Problem
Directly accessing high-end car data poses security risks.
Need
Perform authorization checks before accessing high-end car data.
Solution
Emily used the Proxy Pattern to perform authorization checks.
interface CarData {
void display();
}
class RealCarData implements CarData {
public void display() {
System.out.println("Displaying car data");
}
}
class CarDataProxy implements CarData {
private RealCarData realCarData;
private String userRole;
public CarDataProxy(String userRole) {
this.userRole = userRole;
realCarData = new RealCarData();
}
public void display() {
if ("ADMIN".equals(userRole)) {
realCarData.display();
} else {
System.out.println("Access Denied");
}
}
}
13. Chain of Responsibility Pattern
Story Background
SpeedCars’ customer service system needs to handle different types of customer requests, such as general inquiries, technical support, and complaints.
Problem
Each type of request requires different handling, making the logic complex and difficult to extend.
Need
Handle different types of customer requests flexibly.
Solution
Emily used the Chain of Responsibility Pattern to handle different types of customer requests.
abstract class Handler {
protected Handler nextHandler;
public void setNextHandler(Handler nextHandler) {
this.nextHandler = nextHandler;
}
public abstract void handleRequest(String request);
}
class GeneralInquiryHandler extends Handler {
public void handleRequest(String request) {
if (request.equals("general")) {
System.out.println("Handling general inquiry");
} else if (nextHandler != null) {
nextHandler.handleRequest(request);
}
}
}
class TechnicalSupportHandler extends Handler {
public void handleRequest(String request) {
if (request.equals("technical")) {
System.out.println("Handling technical support");
} else if (nextHandler != null) {
nextHandler.handleRequest(request);
}
}
}
14. Command Pattern
Story Background
SpeedCars’ car display page needs to support various user actions, such as adding to favorites, purchasing, and sharing.
Problem
The implementation code for user actions is scattered, making it difficult to manage and maintain.
Need
Encapsulate user requests as objects to parameterize and record user actions.
Solution
Emily used the Command Pattern to encapsulate user actions.
interface Command {
void execute();
}
class AddToFavoritesCommand implements Command {
private Car car;
public AddToFavoritesCommand(Car car) {
this.car = car;
}
public void execute() {
System.out.println("Adding " + car.getDescription() + " to favorites");
}
}
15. Interpreter Pattern
Story Background
SpeedCars wants to provide an advanced search function, allowing users to search for cars using query strings.
Problem
Parsing and handling query strings is complex and difficult to maintain.
Need
Parse and execute user query strings.
Solution
Emily used the Interpreter Pattern to parse and execute query strings.
interface Expression {
boolean interpret(String context);
}
class BrandExpression implements Expression {
private String brand;
public BrandExpression(String brand) {
this.brand = brand;
}
public boolean interpret(String context) {
return context.contains(brand);
}
}
16. Iterator Pattern
Story Background
SpeedCars needs to traverse and operate on a large number of car objects, such as displaying all available cars on a display page.
Problem
The logic for traversing car objects is scattered, making it difficult to maintain and extend.
Need
Provide a unified way to traverse and operate on car objects.
Solution
Emily used the Iterator Pattern to unify the traversal and operation of car objects.
interface Iterator {
boolean hasNext();
Object next();
}
class CarCollection {
private Car[] cars;
private int index = 0;
public void addCar(Car car) {
cars[index++] = car;
}
public CarIterator iterator() {
return new CarIterator(cars);
}
}
class CarIterator implements Iterator {
private Car[] cars;
private int position = 0;
public CarIterator(Car[] cars) {
this.cars = cars;
}
public boolean hasNext() {
return position < cars.length && cars[position] != null;
}
public Car next() {
return cars[position++];
}
}
17. Mediator Pattern
Story Background
Different modules at SpeedCars, such as user management, order management, and inventory management, need to communicate and coordinate.
Problem
Direct communication between modules leads to high system coupling, making it difficult to maintain and extend.
Need
Decouple module communication, centrally managing their interactions.
Solution
Emily used the Mediator Pattern to manage module communication centrally.
interface Mediator {
void sendMessage(String message, Colleague colleague);
}
class ConcreteMediator implements Mediator {
private UserManagement userManagement;
private OrderManagement orderManagement;
public void setUserManagement(UserManagement userManagement) {
this.userManagement = userManagement;
}
public void setOrderManagement(OrderManagement orderManagement) {
this.orderManagement = orderManagement;
}
public void sendMessage(String message, Colleague colleague) {
if (colleague == userManagement) {
orderManagement.receiveMessage(message);
} else if (colleague == orderManagement) {
userManagement.receiveMessage(message);
}
}
}
18. Memento Pattern
Story Background
SpeedCars offers a car configuration service, allowing customers to save and restore previous configuration states.
Problem
Unable to save and restore customer configuration states, leading to poor user experience.
Need
Save and restore customer configuration states.
Solution
Emily used the Memento Pattern to save and restore customer configuration states.
class CarConfigMemento {
private String configuration;
public CarConfigMemento(String configuration) {
this.configuration = configuration;
}
public String getConfiguration() {
return configuration;
}
}
class CarConfig {
private String configuration;
public void setConfiguration(String configuration) {
this.configuration = configuration;
}
public CarConfigMemento save() {
return new CarConfigMemento(configuration);
}
public void restore(CarConfigMemento memento) {
configuration = memento.getConfiguration();
}
}
19. Observer Pattern
Story Background
Users at SpeedCars want to receive notifications when new cars are listed or when prices change.
Problem
Unable to timely notify users about new listings or price changes.
Need
Notify users about new listings or price changes.
Solution
Emily used the Observer Pattern to notify users.
interface Observer {
void update(String message);
}
class User implements Observer {
private String name;
public User(String name) {
this.name = name;
}
public void update(String message) {
System.out.println(name + " received update: " + message);
}
}
class CarStore {
private List<Observer> observers = new ArrayList<>();
public void addObserver(Observer observer) {
observers.add(observer);
}
public void removeObserver(Observer observer) {
observers.remove(observer);
}
public void notifyObservers(String message) {
for (Observer observer : observers) {
observer.update(message);
}
}
public void newCarArrived() {
notifyObservers("New car arrived!");
}
}
20. State Pattern
Story Background
The order processing system at SpeedCars needs to handle different order states, such as new orders, in-process, and completed.
Problem
Handling order states is complex and difficult to maintain.
Need
Simplify and organize the logic for handling order states.
Solution
Emily used the State Pattern to simplify and organize order state handling.
interface OrderState {
void handleOrder();
}
class NewOrderState implements OrderState {
public void handleOrder() {
System.out.println("Handling new order");
}
}
class ProcessingOrderState implements OrderState {
public void handleOrder() {
System.out.println("Processing order");
}
}
class Order {
private OrderState state;
public void setState(OrderState state) {
this.state = state;
}
public void processOrder() {
state.handleOrder();
}
}
21. Strategy Pattern
Story Background
SpeedCars offers various payment methods, such as credit card, PayPal, and bank transfer.
Problem
Different payment methods’ implementation logic is scattered, making it difficult to maintain and extend.
Need
Manage and switch between different payment methods uniformly.
Solution
Emily used the Strategy Pattern to manage and switch between payment methods.
interface PaymentStrategy {
void pay(int amount);
}
class CreditCardPayment implements PaymentStrategy {
public void pay(int amount) {
System.out.println("Paid " + amount + " using Credit Card");
}
}
class PayPalPayment implements PaymentStrategy {
public void pay(int amount) {
System.out.println("Paid " + amount + " using PayPal");
}
}
class BankTransfer implements PaymentStrategy {
public void pay(int amount) {
System.out.println("Paid " + amount + " using Bank Transfer");
}
}
class PaymentContext {
private PaymentStrategy strategy;
public void setPaymentStrategy(PaymentStrategy strategy) {
this.strategy = strategy;
}
public void pay(int amount) {
strategy.pay(amount);
}
}
22. Template Method Pattern
Story Background
The after-sales service at SpeedCars requires a series of standardized checks and maintenance steps, but different models might have special steps.
Problem
There is a lot of duplicated code, and managing specific steps for different car models is difficult.
Need
Define a standardized process and allow customization of certain steps for different car models.
Solution
Emily uses the Template Method pattern to define a standardized process and allow some steps to be customized.
abstract class CarService {
public final void serviceCar() {
checkTires();
checkEngine();
additionalService();
}
protected void checkTires() {
System.out.println("Checking tires");
}
protected void checkEngine() {
System.out.println("Checking engine");
}
protected abstract void additionalService();
}
class SedanService extends CarService {
protected void additionalService() {
System.out.println("Performing additional service for Sedan");
}
}
23. Visitor Pattern
Story Background
The SpeedCar's object structure is complex and requires multiple operations such as generating reports, calculating prices, and assessing residual values.
Problem
Every time a new operation needs to be added to the car object, these classes must be modified, increasing the complexity of the code and maintenance difficulty.
Need
Add new operations to car objects without modifying their classes.
Solution
Emily uses the Visitor pattern to add new operations to car objects without modifying their classes.
interface CarVisitor {
void visit(Sedan sedan);
void visit(SUV suv);
void visit(Truck truck);
}
abstract class Car {
abstract void accept(CarVisitor visitor);
}
class Sedan extends Car {
void accept(CarVisitor visitor) {
visitor.visit(this);
}
}
class SUV extends Car {
void accept(CarVisitor visitor) {
visitor.visit(this);
}
}
class Truck extends Car {
void accept(CarVisitor visitor) {
visitor.visit(this);
}
}
class ReportVisitor implements CarVisitor {
public void visit(Sedan sedan) {
System.out.println("Generating report for Sedan");
}
public void visit(SUV suv) {
System.out.println("Generating report for SUV");
}
public void visit(Truck truck) {
System.out.println("Generating report for Truck");
}
}
By employing these design patterns, Emily successfully resolved various issues encountered by SpeedCar during the development of the car-selling website, making the system more flexible, maintainable, and scalable. Each design pattern addresses specific problems and requirements, offering elegant solutions.