Add all 23 GoF design pattern implementations
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
out/
|
||||
*.class
|
||||
screenshots/*.png
|
||||
*.b64.txt
|
||||
30
01-creational/abstract-factory/Application.java
Normal file
30
01-creational/abstract-factory/Application.java
Normal file
@@ -0,0 +1,30 @@
|
||||
package abstractfactory;
|
||||
|
||||
public class Application {
|
||||
|
||||
// The client holds references to the Abstract Factory and Abstract Products.
|
||||
// It never imports LightButton, DarkButton, or any concrete class.
|
||||
private final UIFactory factory;
|
||||
private Button submitButton;
|
||||
private TextField usernameField;
|
||||
|
||||
public Application(UIFactory factory) {
|
||||
this.factory = factory; // theme is injected; client doesn't decide
|
||||
}
|
||||
|
||||
public void buildUI() {
|
||||
// Ask the factory — get consistent products from the same family
|
||||
submitButton = factory.createButton();
|
||||
usernameField = factory.createTextField();
|
||||
}
|
||||
|
||||
public void render() {
|
||||
System.out.println("Rendering login screen:");
|
||||
usernameField.render();
|
||||
submitButton.render();
|
||||
}
|
||||
|
||||
public void simulateSubmit() {
|
||||
submitButton.onClick();
|
||||
}
|
||||
}
|
||||
7
01-creational/abstract-factory/Button.java
Normal file
7
01-creational/abstract-factory/Button.java
Normal file
@@ -0,0 +1,7 @@
|
||||
package abstractfactory;
|
||||
|
||||
// Every button, regardless of theme, must be able to render and handle clicks
|
||||
public interface Button {
|
||||
void render(); // draw on screen
|
||||
void onClick(); // respond to a click event
|
||||
}
|
||||
11
01-creational/abstract-factory/DarkButton.java
Normal file
11
01-creational/abstract-factory/DarkButton.java
Normal file
@@ -0,0 +1,11 @@
|
||||
package abstractfactory;
|
||||
public class DarkButton implements Button {
|
||||
@Override
|
||||
public void render() {
|
||||
System.out.println("[Dark Button] Charcoal background, white text, no border");
|
||||
}
|
||||
@Override
|
||||
public void onClick() {
|
||||
System.out.println("[Dark Button] Bright highlight ripple effect");
|
||||
}
|
||||
}
|
||||
10
01-creational/abstract-factory/DarkTextField.java
Normal file
10
01-creational/abstract-factory/DarkTextField.java
Normal file
@@ -0,0 +1,10 @@
|
||||
package abstractfactory;
|
||||
public class DarkTextField implements TextField {
|
||||
private String value = "";
|
||||
@Override
|
||||
public void render() {
|
||||
System.out.println("[Dark TextField] Dark gray background, light placeholder text");
|
||||
}
|
||||
@Override
|
||||
public String getValue() { return value; }
|
||||
}
|
||||
14
01-creational/abstract-factory/DarkThemeFactory.java
Normal file
14
01-creational/abstract-factory/DarkThemeFactory.java
Normal file
@@ -0,0 +1,14 @@
|
||||
package abstractfactory;
|
||||
|
||||
public class DarkThemeFactory implements UIFactory {
|
||||
|
||||
@Override
|
||||
public Button createButton() {
|
||||
return new DarkButton(); // Only Dark products come from this factory
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextField createTextField() {
|
||||
return new DarkTextField();
|
||||
}
|
||||
}
|
||||
11
01-creational/abstract-factory/LightButton.java
Normal file
11
01-creational/abstract-factory/LightButton.java
Normal file
@@ -0,0 +1,11 @@
|
||||
package abstractfactory;
|
||||
public class LightButton implements Button {
|
||||
@Override
|
||||
public void render() {
|
||||
System.out.println("[Light Button] White background, dark text, 1px gray border");
|
||||
}
|
||||
@Override
|
||||
public void onClick() {
|
||||
System.out.println("[Light Button] Subtle gray ripple effect");
|
||||
}
|
||||
}
|
||||
10
01-creational/abstract-factory/LightTextField.java
Normal file
10
01-creational/abstract-factory/LightTextField.java
Normal file
@@ -0,0 +1,10 @@
|
||||
package abstractfactory;
|
||||
public class LightTextField implements TextField {
|
||||
private String value = "";
|
||||
@Override
|
||||
public void render() {
|
||||
System.out.println("[Light TextField] White background, dark placeholder text");
|
||||
}
|
||||
@Override
|
||||
public String getValue() { return value; }
|
||||
}
|
||||
14
01-creational/abstract-factory/LightThemeFactory.java
Normal file
14
01-creational/abstract-factory/LightThemeFactory.java
Normal file
@@ -0,0 +1,14 @@
|
||||
package abstractfactory;
|
||||
|
||||
public class LightThemeFactory implements UIFactory {
|
||||
|
||||
@Override
|
||||
public Button createButton() {
|
||||
return new LightButton(); // Only Light products come from this factory
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextField createTextField() {
|
||||
return new LightTextField();
|
||||
}
|
||||
}
|
||||
16
01-creational/abstract-factory/Main.java
Normal file
16
01-creational/abstract-factory/Main.java
Normal file
@@ -0,0 +1,16 @@
|
||||
package abstractfactory;
|
||||
|
||||
// Entry point: the ONLY place that knows the theme
|
||||
public class Main {
|
||||
public static void main(String[] args) {
|
||||
|
||||
// Production: read theme from config or OS preference
|
||||
String theme = System.getProperty("app.theme", "light");
|
||||
UIFactory factory = "dark".equals(theme) ? new DarkThemeFactory() : new LightThemeFactory();
|
||||
|
||||
Application app = new Application(factory);
|
||||
app.buildUI();
|
||||
app.render();
|
||||
app.simulateSubmit();
|
||||
}
|
||||
}
|
||||
33
01-creational/abstract-factory/README.md
Normal file
33
01-creational/abstract-factory/README.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Abstract Factory Design Pattern — Java Example
|
||||
|
||||
**Pattern:** Creational → Abstract Factory
|
||||
**Article:** https://ankurm.com/abstract-factory-design-pattern-java/
|
||||
|
||||
## What this example shows
|
||||
|
||||
A UI toolkit that produces families of related widgets — light theme or dark theme — without the client ever naming a concrete class. `Button` and `TextField` are abstract products; `LightButton`/`LightTextField` and `DarkButton`/`DarkTextField` are the two concrete product families. `UIFactory` declares the creation methods for the whole family; `LightThemeFactory` and `DarkThemeFactory` each produce one consistent family. `Application` is the client — it only depends on `UIFactory` and the abstract products, so swapping the entire theme is a one-line change. `Main` selects the factory via a system property.
|
||||
|
||||
## How to run
|
||||
|
||||
```bash
|
||||
javac abstract-factory/*.java -d out/abstract-factory
|
||||
java -cp out/abstract-factory abstractfactory.Main
|
||||
# or, for the dark theme family:
|
||||
java -Dapp.theme=dark -cp out/abstract-factory abstractfactory.Main
|
||||
```
|
||||
|
||||
Requires Java 25.
|
||||
|
||||
## Post Section ↔ File Mapping
|
||||
|
||||
| Post Section | File(s) |
|
||||
|---|---|
|
||||
| Part 1 — The Abstract Products: Button and TextField | `Button.java`, `TextField.java` |
|
||||
| Part 2 — The Concrete Products (Light family) | `LightButton.java`, `LightTextField.java` |
|
||||
| Part 2 — The Concrete Products (Dark family) | `DarkButton.java`, `DarkTextField.java` |
|
||||
| Part 3 — The Abstract Factory: UIFactory Interface | `UIFactory.java` |
|
||||
| Part 4 — The Concrete Factories | `LightThemeFactory.java`, `DarkThemeFactory.java` |
|
||||
| Part 5 — The Client: Application | `Application.java`, `Main.java` |
|
||||
|
||||
Article: https://ankurm.com/abstract-factory-design-pattern-java/
|
||||
All patterns: https://ankurm.com/design-patterns-java/
|
||||
7
01-creational/abstract-factory/TextField.java
Normal file
7
01-creational/abstract-factory/TextField.java
Normal file
@@ -0,0 +1,7 @@
|
||||
package abstractfactory;
|
||||
|
||||
// Every text field must be able to render and return its value
|
||||
public interface TextField {
|
||||
void render();
|
||||
String getValue();
|
||||
}
|
||||
13
01-creational/abstract-factory/UIFactory.java
Normal file
13
01-creational/abstract-factory/UIFactory.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package abstractfactory;
|
||||
|
||||
public interface UIFactory {
|
||||
|
||||
// Factory method for buttons — returns the abstract type, not LightButton or DarkButton
|
||||
Button createButton();
|
||||
|
||||
// Factory method for text fields — same principle
|
||||
TextField createTextField();
|
||||
|
||||
// Adding a new product type (e.g., Checkbox) means adding a method here
|
||||
// AND implementing it in every Concrete Factory. This is the one cost of the pattern.
|
||||
}
|
||||
27
01-creational/builder/HttpClientConfig.java
Normal file
27
01-creational/builder/HttpClientConfig.java
Normal file
@@ -0,0 +1,27 @@
|
||||
package builder;
|
||||
|
||||
public class HttpClientConfig {
|
||||
|
||||
// Director method 1: a health check probe — short timeout, no retries
|
||||
public HttpRequest buildHealthCheck(String baseUrl) {
|
||||
return new HttpRequest.Builder()
|
||||
.url(baseUrl + "/health")
|
||||
.method("GET")
|
||||
.timeoutMs(2_000) // fast fail for health checks
|
||||
.maxRetries(0)
|
||||
.build();
|
||||
}
|
||||
|
||||
// Director method 2: a resilient POST with standard JSON headers and retries
|
||||
public HttpRequest buildResilientPost(String url, String jsonBody) {
|
||||
return new HttpRequest.Builder()
|
||||
.url(url)
|
||||
.method("POST")
|
||||
.body(jsonBody)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Accept", "application/json")
|
||||
.timeoutMs(15_000)
|
||||
.maxRetries(3)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
76
01-creational/builder/HttpRequest.java
Normal file
76
01-creational/builder/HttpRequest.java
Normal file
@@ -0,0 +1,76 @@
|
||||
package builder;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class HttpRequest {
|
||||
|
||||
// All fields are final: the object is immutable after build() returns.
|
||||
private final String url;
|
||||
private final String method;
|
||||
private final String body;
|
||||
private final Map<String, String> headers;
|
||||
private final int timeoutMs;
|
||||
private final int maxRetries;
|
||||
|
||||
// Private: the only path to an HttpRequest is through Builder.build()
|
||||
private HttpRequest(Builder builder) {
|
||||
this.url = Objects.requireNonNull(builder.url, "URL is required");
|
||||
this.method = builder.method;
|
||||
this.body = builder.body;
|
||||
this.headers = Collections.unmodifiableMap(new HashMap<>(builder.headers));
|
||||
this.timeoutMs = builder.timeoutMs;
|
||||
this.maxRetries = builder.maxRetries;
|
||||
}
|
||||
|
||||
// Getters only — no setters, enforcing immutability
|
||||
public String getUrl() { return url; }
|
||||
public String getMethod() { return method; }
|
||||
public String getBody() { return body; }
|
||||
public Map<String, String> getHeaders() { return headers; }
|
||||
public int getTimeoutMs() { return timeoutMs; }
|
||||
public int getMaxRetries() { return maxRetries; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%s %s (timeout=%dms, retries=%d, headers=%s, body=%s)",
|
||||
method, url, timeoutMs, maxRetries, headers, body);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// THE BUILDER — static inner class so it can call the private constructor
|
||||
// -----------------------------------------------------------------------
|
||||
public static class Builder {
|
||||
|
||||
// Required field — no default, must be set
|
||||
private String url;
|
||||
|
||||
// Optional fields with sensible defaults
|
||||
private String method = "GET";
|
||||
private String body = null;
|
||||
private Map<String, String> headers = new HashMap<>();
|
||||
private int timeoutMs = 30_000; // 30 seconds
|
||||
private int maxRetries = 0;
|
||||
|
||||
// Fluent methods: each returns 'this' so calls can be chained
|
||||
public Builder url(String url) { this.url = url; return this; }
|
||||
public Builder method(String method) { this.method = method; return this; }
|
||||
public Builder body(String body) { this.body = body; return this; }
|
||||
public Builder header(String key, String v) { headers.put(key, v); return this; }
|
||||
public Builder timeoutMs(int ms) { this.timeoutMs = ms; return this; }
|
||||
public Builder maxRetries(int n) { this.maxRetries = n; return this; }
|
||||
|
||||
// Terminal method: validates and creates the immutable HttpRequest
|
||||
public HttpRequest build() {
|
||||
if (url == null || url.isBlank()) {
|
||||
throw new IllegalStateException("url() is required before calling build()");
|
||||
}
|
||||
if (maxRetries < 0) {
|
||||
throw new IllegalStateException("maxRetries must be >= 0");
|
||||
}
|
||||
return new HttpRequest(this); // calls the private constructor
|
||||
}
|
||||
}
|
||||
}
|
||||
42
01-creational/builder/Main.java
Normal file
42
01-creational/builder/Main.java
Normal file
@@ -0,0 +1,42 @@
|
||||
package builder;
|
||||
|
||||
public class Main {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
// Simple GET — only the required field
|
||||
HttpRequest ping = new HttpRequest.Builder()
|
||||
.url("https://api.example.com/health")
|
||||
.build();
|
||||
|
||||
System.out.println("Simple GET: " + ping);
|
||||
|
||||
// Full POST with authentication and retry
|
||||
HttpRequest post = new HttpRequest.Builder()
|
||||
.url("https://api.example.com/orders")
|
||||
.method("POST")
|
||||
.body("{\"item\":\"book\",\"qty\":1}")
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", "Bearer eyJ...")
|
||||
.timeoutMs(10_000) // 10 seconds — name is self-documenting
|
||||
.maxRetries(3)
|
||||
.build();
|
||||
|
||||
System.out.println("POST: " + post);
|
||||
|
||||
// build() throws if url is missing — validation at construction time
|
||||
try {
|
||||
HttpRequest bad = new HttpRequest.Builder().build(); // no URL!
|
||||
} catch (IllegalStateException e) {
|
||||
System.out.println("Caught: " + e.getMessage()); // url() is required
|
||||
}
|
||||
|
||||
// Using the Director for the two standard presets defined above
|
||||
HttpClientConfig config = new HttpClientConfig();
|
||||
HttpRequest healthCheck = config.buildHealthCheck("https://api.example.com");
|
||||
System.out.println("Health check: " + healthCheck);
|
||||
|
||||
HttpRequest order = config.buildResilientPost("https://api.example.com/orders", "{\"qty\":1}");
|
||||
System.out.println("Order: " + order);
|
||||
}
|
||||
}
|
||||
30
01-creational/builder/README.md
Normal file
30
01-creational/builder/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Builder Design Pattern — Java Example
|
||||
|
||||
**Pattern:** Creational → Builder
|
||||
**Article:** https://ankurm.com/builder-design-pattern-java/
|
||||
|
||||
## What this example shows
|
||||
|
||||
An immutable `HttpRequest` assembled step by step instead of through a constructor with a dozen optional parameters. `HttpRequest` is the product — built once, never mutated afterward. `HttpClientConfig` is a director-style helper that encapsulates common request configurations so callers don't repeat the same builder calls. `Main` wires it together and shows the fluent builder chain in action.
|
||||
|
||||
## How to run
|
||||
|
||||
```bash
|
||||
javac builder/*.java -d out/builder
|
||||
java -cp out/builder builder.Main
|
||||
```
|
||||
|
||||
Requires Java 25.
|
||||
|
||||
## Post Section ↔ File Mapping
|
||||
|
||||
| Post Section | File(s) |
|
||||
|---|---|
|
||||
| Part 1 — The Product: HttpRequest (Immutable) | `HttpRequest.java` |
|
||||
| Part 2 — The Director: Encapsulating Common Configurations | `HttpClientConfig.java` |
|
||||
| Part 3 — Using the Builder: Client Code | `Main.java` |
|
||||
|
||||
Note: the Lombok `@Builder` example and the JDK standard-library snippets (`HttpRequest.newBuilder()`, `StringBuilder`, `ProcessBuilder`) are illustrative only — they are not part of this repository's runnable example.
|
||||
|
||||
Article: https://ankurm.com/builder-design-pattern-java/
|
||||
All patterns: https://ankurm.com/design-patterns-java/
|
||||
13
01-creational/factory-method/EmailNotification.java
Normal file
13
01-creational/factory-method/EmailNotification.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package factorymethod;
|
||||
|
||||
public class EmailNotification implements Notification {
|
||||
|
||||
@Override
|
||||
public void send(String recipient, String message) {
|
||||
// In production: use JavaMail or Spring Mail
|
||||
System.out.printf(" [EMAIL] To: %s | Body: %s%n", recipient, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType() { return "EMAIL"; }
|
||||
}
|
||||
8
01-creational/factory-method/EmailSender.java
Normal file
8
01-creational/factory-method/EmailSender.java
Normal file
@@ -0,0 +1,8 @@
|
||||
package factorymethod;
|
||||
|
||||
public class EmailSender extends NotificationSender {
|
||||
@Override
|
||||
protected Notification createNotification() {
|
||||
return new EmailNotification();
|
||||
}
|
||||
}
|
||||
23
01-creational/factory-method/Main.java
Normal file
23
01-creational/factory-method/Main.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package factorymethod;
|
||||
|
||||
public class Main {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
// The client only knows about NotificationSender (the Creator)
|
||||
NotificationSender sender;
|
||||
|
||||
sender = new EmailSender();
|
||||
sender.notify("alice@example.com", "Your order has shipped!");
|
||||
|
||||
sender = new SmsSender();
|
||||
sender.notify("+1-555-0100", "OTP: 482910");
|
||||
|
||||
sender = new PushSender();
|
||||
sender.notify("device-token-xyz", "New message from Bob");
|
||||
|
||||
// Adding Slack: just swap the creator. Client code doesn't change.
|
||||
sender = new SlackSender(); // new class, nothing else touched
|
||||
sender.notify("#alerts", "CPU usage above 90%");
|
||||
}
|
||||
}
|
||||
11
01-creational/factory-method/Notification.java
Normal file
11
01-creational/factory-method/Notification.java
Normal file
@@ -0,0 +1,11 @@
|
||||
package factorymethod;
|
||||
|
||||
public interface Notification {
|
||||
|
||||
// Every notification must know how to deliver itself
|
||||
void send(String recipient, String message);
|
||||
|
||||
// Used by the Creator for logging — it needs to know the type
|
||||
// without knowing the concrete class
|
||||
String getType();
|
||||
}
|
||||
31
01-creational/factory-method/NotificationSender.java
Normal file
31
01-creational/factory-method/NotificationSender.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package factorymethod;
|
||||
|
||||
public abstract class NotificationSender {
|
||||
|
||||
// THE factory method — this is the one thing subclasses override.
|
||||
// It returns a Notification (the Product interface), never a concrete class.
|
||||
protected abstract Notification createNotification();
|
||||
|
||||
// The shared workflow — marked final so subclasses cannot change it.
|
||||
// It calls createNotification() to get the right notification,
|
||||
// then runs the same steps every time regardless of which channel was created.
|
||||
public final void notify(String recipient, String message) {
|
||||
Notification notification = createNotification(); // delegate creation
|
||||
System.out.println("Sending " + notification.getType() + " notification...");
|
||||
notification.send(recipient, message); // use the Product interface
|
||||
System.out.println("Delivered.");
|
||||
}
|
||||
|
||||
// Shared retry logic — works for all channels without change
|
||||
public void notifyWithRetry(String recipient, String message, int maxRetries) {
|
||||
for (int attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
notify(recipient, message);
|
||||
return; // success — exit
|
||||
} catch (Exception e) {
|
||||
System.err.printf("Attempt %d failed: %s%n", attempt, e.getMessage());
|
||||
}
|
||||
}
|
||||
throw new RuntimeException("All " + maxRetries + " attempts failed");
|
||||
}
|
||||
}
|
||||
24
01-creational/factory-method/NotificationService.java
Normal file
24
01-creational/factory-method/NotificationService.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package factorymethod;
|
||||
|
||||
public class NotificationService {
|
||||
|
||||
public void send(String type, String recipient, String message) {
|
||||
|
||||
// Creation logic mixed with business logic
|
||||
Notification notification;
|
||||
if ("EMAIL".equals(type)) {
|
||||
notification = new EmailNotification();
|
||||
} else if ("SMS".equals(type)) {
|
||||
notification = new SmsNotification();
|
||||
} else if ("PUSH".equals(type)) {
|
||||
notification = new PushNotification();
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown type: " + type);
|
||||
}
|
||||
|
||||
// Business logic that should never change
|
||||
System.out.println("Sending " + type + " to " + recipient);
|
||||
notification.send(recipient, message);
|
||||
System.out.println("Delivered.");
|
||||
}
|
||||
}
|
||||
13
01-creational/factory-method/PushNotification.java
Normal file
13
01-creational/factory-method/PushNotification.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package factorymethod;
|
||||
|
||||
public class PushNotification implements Notification {
|
||||
|
||||
@Override
|
||||
public void send(String recipient, String message) {
|
||||
// In production: use Firebase FCM
|
||||
System.out.printf(" [PUSH] To: %s | Alert: %s%n", recipient, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType() { return "PUSH"; }
|
||||
}
|
||||
8
01-creational/factory-method/PushSender.java
Normal file
8
01-creational/factory-method/PushSender.java
Normal file
@@ -0,0 +1,8 @@
|
||||
package factorymethod;
|
||||
|
||||
public class PushSender extends NotificationSender {
|
||||
@Override
|
||||
protected Notification createNotification() {
|
||||
return new PushNotification();
|
||||
}
|
||||
}
|
||||
32
01-creational/factory-method/README.md
Normal file
32
01-creational/factory-method/README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Factory Method Design Pattern — Java Example
|
||||
|
||||
**Pattern:** Creational → Factory Method
|
||||
**Article:** https://ankurm.com/factory-method-design-pattern-java/
|
||||
|
||||
## What this example shows
|
||||
|
||||
A notification system where the "send" logic decides which channel object to create, instead of the client doing it with a switch statement. `Notification` is the product interface; `EmailNotification`, `SmsNotification`, and `PushNotification` are concrete products. `NotificationSender` declares the factory method and a template `send()` step shared by every channel; `EmailSender`, `SmsSender`, and `PushSender` each override the factory method to produce their own notification type. `NotificationService` is shown separately as the anti-pattern this pattern replaces — it is not called by `Main`. `Main` demonstrates the cost of adding a new channel (Slack) by adding one new product and one new creator, with zero changes to existing classes.
|
||||
|
||||
## How to run
|
||||
|
||||
```bash
|
||||
javac factory-method/*.java -d out/factory-method
|
||||
java -cp out/factory-method factorymethod.Main
|
||||
```
|
||||
|
||||
Requires Java 25.
|
||||
|
||||
## Post Section ↔ File Mapping
|
||||
|
||||
| Post Section | File(s) |
|
||||
|---|---|
|
||||
| The Problem: Object Creation Leaks Into Business Logic | `NotificationService.java` (anti-pattern shown for contrast — not called by `Main`) |
|
||||
| Part 1 — The Notification Interface | `Notification.java` |
|
||||
| Part 2 — The Concrete Notification Classes | `EmailNotification.java`, `SmsNotification.java`, `PushNotification.java` |
|
||||
| Part 3 — The NotificationSender Class | `NotificationSender.java` |
|
||||
| Part 4 — The Concrete Sender Subclasses | `EmailSender.java`, `SmsSender.java`, `PushSender.java` |
|
||||
| Part 5 — Running It: Client Code and Output | `Main.java` |
|
||||
| Adding a New Channel (Slack) | `SlackNotification.java`, `SlackSender.java` |
|
||||
|
||||
Article: https://ankurm.com/factory-method-design-pattern-java/
|
||||
All patterns: https://ankurm.com/design-patterns-java/
|
||||
11
01-creational/factory-method/SlackNotification.java
Normal file
11
01-creational/factory-method/SlackNotification.java
Normal file
@@ -0,0 +1,11 @@
|
||||
package factorymethod;
|
||||
|
||||
// New Concrete Product — knows how to send to Slack
|
||||
public class SlackNotification implements Notification {
|
||||
@Override
|
||||
public void send(String recipient, String message) {
|
||||
System.out.printf(" [SLACK] Channel: %s | Message: %s%n", recipient, message);
|
||||
}
|
||||
@Override
|
||||
public String getType() { return "SLACK"; }
|
||||
}
|
||||
9
01-creational/factory-method/SlackSender.java
Normal file
9
01-creational/factory-method/SlackSender.java
Normal file
@@ -0,0 +1,9 @@
|
||||
package factorymethod;
|
||||
|
||||
// New Concrete Creator — knows to create a SlackNotification
|
||||
public class SlackSender extends NotificationSender {
|
||||
@Override
|
||||
protected Notification createNotification() {
|
||||
return new SlackNotification();
|
||||
}
|
||||
}
|
||||
13
01-creational/factory-method/SmsNotification.java
Normal file
13
01-creational/factory-method/SmsNotification.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package factorymethod;
|
||||
|
||||
public class SmsNotification implements Notification {
|
||||
|
||||
@Override
|
||||
public void send(String recipient, String message) {
|
||||
// In production: use Twilio SDK
|
||||
System.out.printf(" [SMS] To: %s | Text: %s%n", recipient, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType() { return "SMS"; }
|
||||
}
|
||||
8
01-creational/factory-method/SmsSender.java
Normal file
8
01-creational/factory-method/SmsSender.java
Normal file
@@ -0,0 +1,8 @@
|
||||
package factorymethod;
|
||||
|
||||
public class SmsSender extends NotificationSender {
|
||||
@Override
|
||||
protected Notification createNotification() {
|
||||
return new SmsNotification();
|
||||
}
|
||||
}
|
||||
8
01-creational/prototype/Copyable.java
Normal file
8
01-creational/prototype/Copyable.java
Normal file
@@ -0,0 +1,8 @@
|
||||
package prototype;
|
||||
|
||||
// Our Prototype interface: any class that can clone itself implements this.
|
||||
// The return type T allows each implementing class to return its own type,
|
||||
// not the raw interface type, which makes the client code cleaner.
|
||||
public interface Copyable<T> {
|
||||
T copy();
|
||||
}
|
||||
46
01-creational/prototype/Document.java
Normal file
46
01-creational/prototype/Document.java
Normal file
@@ -0,0 +1,46 @@
|
||||
package prototype;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class Document implements Copyable<Document> {
|
||||
|
||||
private String title; // String: immutable, safe to share on copy
|
||||
private String content; // String: immutable, safe to share on copy
|
||||
private List<String> tags; // List: mutable, MUST be deep-copied
|
||||
private DocumentMetadata metadata; // Mutable object, MUST be deep-copied
|
||||
|
||||
public Document(String title, String content, List<String> tags, DocumentMetadata metadata) {
|
||||
this.title = title;
|
||||
this.content = content;
|
||||
this.tags = new ArrayList<>(tags); // defensive copy on construction
|
||||
this.metadata = metadata;
|
||||
}
|
||||
|
||||
// Copy constructor: the preferred Prototype implementation in modern Java
|
||||
public Document(Document source) {
|
||||
this.title = source.title; // immutable: share
|
||||
this.content = source.content; // immutable: share
|
||||
this.tags = new ArrayList<>(source.tags); // mutable: new list
|
||||
this.metadata = source.metadata.copy(); // mutable: deep copy via Copyable
|
||||
}
|
||||
|
||||
@Override
|
||||
public Document copy() {
|
||||
return new Document(this); // delegates to copy constructor
|
||||
}
|
||||
|
||||
// Getters and setters
|
||||
public String getTitle() { return title; }
|
||||
public void setTitle(String title) { this.title = title; }
|
||||
public String getContent() { return content; }
|
||||
public void setContent(String content) { this.content = content; }
|
||||
public List<String> getTags() { return Collections.unmodifiableList(tags); }
|
||||
public void addTag(String tag) { tags.add(tag); }
|
||||
public DocumentMetadata getMetadata() { return metadata; }
|
||||
|
||||
@Override public String toString() {
|
||||
return String.format("Document{title='%s', tags=%s, %s}", title, tags, metadata);
|
||||
}
|
||||
}
|
||||
39
01-creational/prototype/DocumentMetadata.java
Normal file
39
01-creational/prototype/DocumentMetadata.java
Normal file
@@ -0,0 +1,39 @@
|
||||
package prototype;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
public class DocumentMetadata implements Copyable<DocumentMetadata> {
|
||||
|
||||
private String author;
|
||||
private LocalDate createdDate; // LocalDate is immutable — safe to share
|
||||
private String version;
|
||||
|
||||
public DocumentMetadata(String author, LocalDate createdDate, String version) {
|
||||
this.author = author;
|
||||
this.createdDate = createdDate; // safe: LocalDate is immutable
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
// Copy constructor: creates a new instance with the same values
|
||||
public DocumentMetadata(DocumentMetadata source) {
|
||||
this.author = source.author; // String: immutable, safe to share
|
||||
this.createdDate = source.createdDate; // LocalDate: immutable, safe to share
|
||||
this.version = source.version;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DocumentMetadata copy() {
|
||||
return new DocumentMetadata(this); // delegates to copy constructor
|
||||
}
|
||||
|
||||
public String getAuthor() { return author; }
|
||||
public LocalDate getCreatedDate(){ return createdDate; }
|
||||
public String getVersion() { return version; }
|
||||
public void setVersion(String v) { this.version = v; }
|
||||
public void setAuthor(String a) { this.author = a; }
|
||||
|
||||
@Override public String toString() {
|
||||
return String.format("Metadata{author='%s', date=%s, version='%s'}",
|
||||
author, createdDate, version);
|
||||
}
|
||||
}
|
||||
21
01-creational/prototype/DocumentRegistry.java
Normal file
21
01-creational/prototype/DocumentRegistry.java
Normal file
@@ -0,0 +1,21 @@
|
||||
package prototype;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class DocumentRegistry {
|
||||
|
||||
private final Map<String, Document> prototypes = new HashMap<>();
|
||||
|
||||
// Register a prototype under a name
|
||||
public void register(String name, Document prototype) {
|
||||
prototypes.put(name, prototype);
|
||||
}
|
||||
|
||||
// Return a fresh copy of the named prototype
|
||||
public Document get(String name) {
|
||||
Document prototype = prototypes.get(name);
|
||||
if (prototype == null) throw new IllegalArgumentException("No prototype: " + name);
|
||||
return prototype.copy(); // always return a copy, never the prototype itself
|
||||
}
|
||||
}
|
||||
47
01-creational/prototype/Main.java
Normal file
47
01-creational/prototype/Main.java
Normal file
@@ -0,0 +1,47 @@
|
||||
package prototype;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
public class Main {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
// Create the original document (imagine this was loaded from a database)
|
||||
DocumentMetadata metadata = new DocumentMetadata("Alice", LocalDate.of(2025, 1, 15), "1.0");
|
||||
Document original = new Document(
|
||||
"Q1 Report",
|
||||
"Revenue increased by 12%...",
|
||||
List.of("finance", "quarterly"),
|
||||
metadata
|
||||
);
|
||||
|
||||
System.out.println("Original: " + original);
|
||||
|
||||
// Clone the document and apply only the differences
|
||||
Document draft = original.copy();
|
||||
draft.setTitle("Q1 Report — DRAFT");
|
||||
draft.addTag("draft");
|
||||
draft.getMetadata().setAuthor("Bob"); // only the copy's metadata changes
|
||||
draft.getMetadata().setVersion("1.0-draft");
|
||||
|
||||
System.out.println("Draft: " + draft);
|
||||
System.out.println("Original: " + original); // unchanged — deep copy worked
|
||||
|
||||
// Verify independence
|
||||
System.out.println("\nSame object? " + (original == draft)); // false
|
||||
System.out.println("Same tags? " + (original.getTags() == draft.getTags())); // false
|
||||
System.out.println("Same meta? " + (original.getMetadata() == draft.getMetadata())); // false
|
||||
|
||||
// Registry demo: register a named template once, then hand out independent copies
|
||||
DocumentRegistry registry = new DocumentRegistry();
|
||||
registry.register("q-report-template", new Document(
|
||||
"Quarterly Report Template", "## Executive Summary\n...",
|
||||
List.of("quarterly", "finance"), metadata));
|
||||
|
||||
Document myReport = registry.get("q-report-template"); // a fresh, independent copy
|
||||
myReport.setTitle("Q2 2025 Report");
|
||||
myReport.addTag("q2");
|
||||
System.out.println("\nFrom registry: " + myReport);
|
||||
}
|
||||
}
|
||||
32
01-creational/prototype/README.md
Normal file
32
01-creational/prototype/README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Prototype Design Pattern — Java Example
|
||||
|
||||
**Pattern:** Creational → Prototype
|
||||
**Article:** https://ankurm.com/prototype-design-pattern-java/
|
||||
|
||||
## What this example shows
|
||||
|
||||
Documents cloned from existing instances instead of rebuilt from scratch. `Copyable` declares the cloning contract. `DocumentMetadata` is a nested object that must be copied correctly for a clone to be truly independent of its source. `Document` is the concrete prototype, implementing a real (not shallow) copy. `DocumentRegistry` stores a set of ready-made prototypes that callers clone on demand instead of constructing from raw parameters. `Main` demonstrates cloning from the registry and confirms the clone and original don't share mutable state.
|
||||
|
||||
## How to run
|
||||
|
||||
```bash
|
||||
javac prototype/*.java -d out/prototype
|
||||
java -cp out/prototype prototype.Main
|
||||
```
|
||||
|
||||
Requires Java 25.
|
||||
|
||||
## Post Section ↔ File Mapping
|
||||
|
||||
| Post Section | File(s) |
|
||||
|---|---|
|
||||
| Part 1 — The Prototype Interface: Copyable | `Copyable.java` |
|
||||
| Part 2 — The Nested Object: DocumentMetadata | `DocumentMetadata.java` |
|
||||
| Part 3 — The Concrete Prototype: Document | `Document.java` |
|
||||
| Part 4 — The Prototype Registry: DocumentRegistry | `DocumentRegistry.java` |
|
||||
| Part 5 — Using the Prototype: Client Code | `Main.java` |
|
||||
|
||||
Note: the `ShallowVsDeepDemo` snippet (shallow vs. deep copy) and the `Cloneable`-based example are illustrative only — they are not part of this repository's runnable example.
|
||||
|
||||
Article: https://ankurm.com/prototype-design-pattern-java/
|
||||
All patterns: https://ankurm.com/design-patterns-java/
|
||||
31
01-creational/singleton/AppConfigDCL.java
Normal file
31
01-creational/singleton/AppConfigDCL.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package singleton;
|
||||
|
||||
public class AppConfigDCL {
|
||||
|
||||
// volatile is MANDATORY for DCL to work correctly.
|
||||
// Without it, the JVM can reorder the writes inside the constructor
|
||||
// and return a partially-constructed object to a second thread.
|
||||
private static volatile AppConfigDCL instance;
|
||||
|
||||
private AppConfigDCL() { /* load config */ }
|
||||
|
||||
public static AppConfigDCL getInstance() {
|
||||
|
||||
// First check: no lock, instant return for the 99.99% case
|
||||
// (instance already created, just return it)
|
||||
if (instance == null) {
|
||||
|
||||
// Lock only when first check says we might need to create
|
||||
synchronized (AppConfigDCL.class) {
|
||||
|
||||
// Second check: re-verify under the lock.
|
||||
// Another thread might have created the instance between
|
||||
// our first check and acquiring the lock.
|
||||
if (instance == null) {
|
||||
instance = new AppConfigDCL();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
24
01-creational/singleton/AppConfigEager.java
Normal file
24
01-creational/singleton/AppConfigEager.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package singleton;
|
||||
|
||||
public class AppConfigEager {
|
||||
|
||||
// The JVM creates this instance when the class is first loaded.
|
||||
// static guarantees it belongs to the class, not any instance.
|
||||
// final guarantees it can never be reassigned.
|
||||
private static final AppConfigEager INSTANCE = new AppConfigEager();
|
||||
|
||||
// Private constructor: prevents any other class from calling new AppConfigEager()
|
||||
private AppConfigEager() {
|
||||
System.out.println("Reading configuration from file...");
|
||||
// load properties, set up values, etc.
|
||||
}
|
||||
|
||||
// The only way to get the instance
|
||||
public static AppConfigEager getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public String getProperty(String key) {
|
||||
return System.getProperty(key, "(not set)");
|
||||
}
|
||||
}
|
||||
19
01-creational/singleton/AppConfigEnum.java
Normal file
19
01-creational/singleton/AppConfigEnum.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package singleton;
|
||||
|
||||
public enum AppConfigEnum {
|
||||
|
||||
INSTANCE; // The JVM creates this exactly once, period.
|
||||
|
||||
// Enum instances can have fields and methods like any class
|
||||
private final String configPath;
|
||||
|
||||
AppConfigEnum() {
|
||||
// Enum constructors run once, when the constant is first used
|
||||
this.configPath = System.getProperty("config.path", "application.properties");
|
||||
System.out.println("Loading config from: " + configPath);
|
||||
}
|
||||
|
||||
public String getProperty(String key) {
|
||||
return System.getProperty(key, "(not set)");
|
||||
}
|
||||
}
|
||||
19
01-creational/singleton/AppConfigHolder.java
Normal file
19
01-creational/singleton/AppConfigHolder.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package singleton;
|
||||
|
||||
public class AppConfigHolder {
|
||||
|
||||
private AppConfigHolder() { /* load config */ }
|
||||
|
||||
// The JVM loads Holder only when getInstance() is first called.
|
||||
// Class loading is inherently thread-safe — the JVM ensures it happens once.
|
||||
// No synchronized, no volatile, no locks — just the JVM class loading guarantee.
|
||||
private static class Holder {
|
||||
static final AppConfigHolder INSTANCE = new AppConfigHolder();
|
||||
}
|
||||
|
||||
public static AppConfigHolder getInstance() {
|
||||
// This reference triggers Holder to load (if it hasn't already).
|
||||
// After first call, Holder is already loaded and this is just a field read.
|
||||
return Holder.INSTANCE;
|
||||
}
|
||||
}
|
||||
16
01-creational/singleton/AppConfigNaiveLazy.java
Normal file
16
01-creational/singleton/AppConfigNaiveLazy.java
Normal file
@@ -0,0 +1,16 @@
|
||||
package singleton;
|
||||
|
||||
public class AppConfigNaiveLazy {
|
||||
|
||||
private static AppConfigNaiveLazy instance; // null until first request
|
||||
|
||||
private AppConfigNaiveLazy() { /* load config */ }
|
||||
|
||||
// BROKEN IN MULTITHREADED CODE — see explanation below
|
||||
public static AppConfigNaiveLazy getInstance() {
|
||||
if (instance == null) { // Thread A: checks, sees null
|
||||
instance = new AppConfigNaiveLazy(); // Thread B: also sees null, also enters here
|
||||
} // Both threads create a new AppConfigNaiveLazy!
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
16
01-creational/singleton/AppConfigSynchronized.java
Normal file
16
01-creational/singleton/AppConfigSynchronized.java
Normal file
@@ -0,0 +1,16 @@
|
||||
package singleton;
|
||||
|
||||
public class AppConfigSynchronized {
|
||||
|
||||
private static AppConfigSynchronized instance;
|
||||
|
||||
private AppConfigSynchronized() { /* load config */ }
|
||||
|
||||
// synchronized: only one thread can execute this method at a time
|
||||
public static synchronized AppConfigSynchronized getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new AppConfigSynchronized(); // safe — no other thread can be here
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
36
01-creational/singleton/Main.java
Normal file
36
01-creational/singleton/Main.java
Normal file
@@ -0,0 +1,36 @@
|
||||
package singleton;
|
||||
|
||||
public class Main {
|
||||
public static void main(String[] args) {
|
||||
|
||||
System.out.println("--- Implementation 1: Eager ---");
|
||||
AppConfigEager eager1 = AppConfigEager.getInstance();
|
||||
AppConfigEager eager2 = AppConfigEager.getInstance();
|
||||
System.out.println("Same instance? " + (eager1 == eager2));
|
||||
System.out.println("db.url = " + eager1.getProperty("db.url"));
|
||||
|
||||
System.out.println("\n--- Implementation 2: Naive Lazy ---");
|
||||
AppConfigNaiveLazy lazy1 = AppConfigNaiveLazy.getInstance();
|
||||
AppConfigNaiveLazy lazy2 = AppConfigNaiveLazy.getInstance();
|
||||
System.out.println("Same instance? " + (lazy1 == lazy2));
|
||||
|
||||
System.out.println("\n--- Implementation 3: Synchronized Method ---");
|
||||
AppConfigSynchronized sync1 = AppConfigSynchronized.getInstance();
|
||||
AppConfigSynchronized sync2 = AppConfigSynchronized.getInstance();
|
||||
System.out.println("Same instance? " + (sync1 == sync2));
|
||||
|
||||
System.out.println("\n--- Implementation 4: Double-Checked Locking ---");
|
||||
AppConfigDCL dcl1 = AppConfigDCL.getInstance();
|
||||
AppConfigDCL dcl2 = AppConfigDCL.getInstance();
|
||||
System.out.println("Same instance? " + (dcl1 == dcl2));
|
||||
|
||||
System.out.println("\n--- Implementation 5: Holder Idiom ---");
|
||||
AppConfigHolder holder1 = AppConfigHolder.getInstance();
|
||||
AppConfigHolder holder2 = AppConfigHolder.getInstance();
|
||||
System.out.println("Same instance? " + (holder1 == holder2));
|
||||
|
||||
System.out.println("\n--- Implementation 6: Enum ---");
|
||||
AppConfigEnum enumConfig = AppConfigEnum.INSTANCE;
|
||||
System.out.println("db.url = " + enumConfig.getProperty("db.url"));
|
||||
}
|
||||
}
|
||||
34
01-creational/singleton/README.md
Normal file
34
01-creational/singleton/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Singleton Design Pattern — Java Example
|
||||
|
||||
**Pattern:** Creational → Singleton
|
||||
**Article:** https://ankurm.com/singleton-design-pattern-java/
|
||||
|
||||
## What this example shows
|
||||
|
||||
Six different ways to implement a singleton, compiled and run side by side so their tradeoffs are directly comparable. `AppConfigEager` initializes at class-load time. `AppConfigNaiveLazy` defers initialization but is not thread-safe. `AppConfigSynchronized` fixes thread-safety at the cost of locking on every call. `AppConfigDCL` uses double-checked locking to avoid that per-call lock. `AppConfigHolder` uses the initialization-on-demand holder idiom for lazy, thread-safe initialization with no locking at all. `AppConfigEnum` uses a single-element enum, which the JVM guarantees is a singleton even against reflection and serialization attacks. `Main` exercises all six in one run.
|
||||
|
||||
## How to run
|
||||
|
||||
```bash
|
||||
javac singleton/*.java -d out/singleton
|
||||
java -cp out/singleton singleton.Main
|
||||
```
|
||||
|
||||
Requires Java 25.
|
||||
|
||||
## Post Section ↔ File Mapping
|
||||
|
||||
| Post Section | File(s) |
|
||||
|---|---|
|
||||
| Implementation 1 — Eager Initialization | `AppConfigEager.java` |
|
||||
| Implementation 2 — Naïve Lazy Initialization | `AppConfigNaiveLazy.java` |
|
||||
| Implementation 3 — Synchronized Method | `AppConfigSynchronized.java` |
|
||||
| Implementation 4 — Double-Checked Locking | `AppConfigDCL.java` |
|
||||
| Implementation 5 — Initialization-on-Demand Holder | `AppConfigHolder.java` |
|
||||
| Implementation 6 — Enum Singleton | `AppConfigEnum.java` |
|
||||
| Running All Six Implementations Together | `Main.java` |
|
||||
|
||||
Each implementation is renamed to its own class (e.g. `AppConfigEager`, `AppConfigHolder`) so all six can be compiled together in one package and compared directly. The "Breaking via Reflection," "Breaking via Serialization," and Spring `@Component` snippets are illustrative only — they are not part of this repository's runnable example.
|
||||
|
||||
Article: https://ankurm.com/singleton-design-pattern-java/
|
||||
All patterns: https://ankurm.com/design-patterns-java/
|
||||
27
02-structural/adapter/Main.java
Normal file
27
02-structural/adapter/Main.java
Normal file
@@ -0,0 +1,27 @@
|
||||
package adapter;
|
||||
|
||||
public class Main {
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println("=== Adapter Design Pattern Demo ===\n");
|
||||
|
||||
// --- Object Adapter: Stripe ---
|
||||
System.out.println("-- Using Stripe via Adapter --");
|
||||
StripeClient stripeClient = new StripeClient("sk_test_4eC39HqLyjWDarjtT1zdp7dc");
|
||||
PaymentGateway stripeGateway = new StripePaymentAdapter(stripeClient);
|
||||
|
||||
OrderService orderService = new OrderService(stripeGateway);
|
||||
orderService.processOrder("ORD-001", "cus_abc123", 99.99);
|
||||
|
||||
System.out.println("\n-- Testing refund --");
|
||||
boolean refunded = stripeGateway.refund("ch_ORD-001", 99.99);
|
||||
System.out.println("Refund issued: " + refunded);
|
||||
|
||||
// --- JDK Example: InputStreamReader as Adapter ---
|
||||
System.out.println("\n-- JDK Adapter: InputStreamReader --");
|
||||
System.out.println("InputStreamReader wraps System.in (InputStream) as a Reader.");
|
||||
System.out.println("Your code reads chars; the adapter handles byte-to-char conversion.");
|
||||
|
||||
System.out.println("\n=== Demo complete ===");
|
||||
}
|
||||
}
|
||||
23
02-structural/adapter/OrderService.java
Normal file
23
02-structural/adapter/OrderService.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package adapter;
|
||||
/**
|
||||
* The Client — uses only the PaymentGateway interface.
|
||||
* It has no idea whether it's talking to Stripe, PayPal, or Braintree.
|
||||
*/
|
||||
public class OrderService {
|
||||
private final PaymentGateway gateway;
|
||||
public OrderService(PaymentGateway gateway) {
|
||||
this.gateway = gateway;
|
||||
}
|
||||
public void processOrder(String orderId, String customerId, double total) {
|
||||
System.out.printf("%nProcessing order %s for customer %s, total: $%.2f%n",
|
||||
orderId, customerId, total);
|
||||
boolean charged = gateway.charge(customerId, total, "USD");
|
||||
if (charged) {
|
||||
System.out.println("Payment accepted. Order confirmed.");
|
||||
String status = gateway.getStatus("ch_" + orderId);
|
||||
System.out.println("Transaction status: " + status);
|
||||
} else {
|
||||
System.out.println("Payment failed. Order rejected.");
|
||||
}
|
||||
}
|
||||
}
|
||||
10
02-structural/adapter/PaymentGateway.java
Normal file
10
02-structural/adapter/PaymentGateway.java
Normal file
@@ -0,0 +1,10 @@
|
||||
package adapter;
|
||||
/**
|
||||
* The Target interface — what YOUR application's code expects.
|
||||
* Every payment gateway in your system must implement this.
|
||||
*/
|
||||
public interface PaymentGateway {
|
||||
boolean charge(String customerId, double amount, String currency);
|
||||
boolean refund(String transactionId, double amount);
|
||||
String getStatus(String transactionId);
|
||||
}
|
||||
36
02-structural/adapter/README.md
Normal file
36
02-structural/adapter/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Adapter Design Pattern — Java Example
|
||||
|
||||
**Pattern:** Structural → Adapter
|
||||
**Article:** https://ankurm.com/adapter-design-pattern-java/
|
||||
|
||||
## What this example shows
|
||||
|
||||
Wraps a third-party `StripeClient` (with its own API) behind a `PaymentGateway` interface that your application code expects. The `OrderService` client never touches `StripeClient` directly — it only sees `PaymentGateway`. Swapping payment providers requires changing one line.
|
||||
|
||||
## How to run
|
||||
|
||||
```bash
|
||||
# From this folder:
|
||||
javac adapter/*.java
|
||||
java adapter.Main
|
||||
```
|
||||
|
||||
Requires Java 25.
|
||||
|
||||
## Files
|
||||
|
||||
| Post Section | File(s) |
|
||||
|---|---|
|
||||
| Step 1 — Define the Target Interface | `PaymentGateway.java` |
|
||||
| Step 2 — The Adaptee (What You're Wrapping) | `StripeClient.java` |
|
||||
| Step 3 — The Object Adapter | `StripePaymentAdapter.java` |
|
||||
| Step 4 — The Client (Knows Nothing About Stripe) | `OrderService.java` |
|
||||
| Putting It Together: The Main Demo | `Main.java` |
|
||||
| Object Adapter vs Class Adapter | `StripePaymentClassAdapter.java` |
|
||||
|
||||
Note: the `InputStreamReader` JDK-adapter snippet is illustrative only — it is not part of this repository's runnable example.
|
||||
|
||||
## See Also
|
||||
|
||||
- Full article: https://ankurm.com/adapter-design-pattern-java/
|
||||
- All design patterns: https://ankurm.com/design-patterns-java/
|
||||
39
02-structural/adapter/StripeClient.java
Normal file
39
02-structural/adapter/StripeClient.java
Normal file
@@ -0,0 +1,39 @@
|
||||
package adapter;
|
||||
/**
|
||||
* The Adaptee — a third-party payment SDK with a completely different interface.
|
||||
* Imagine this is Stripe's actual SDK: you cannot modify this class,
|
||||
* and it doesn't implement PaymentGateway.
|
||||
*/
|
||||
public class StripeClient {
|
||||
private final String apiKey;
|
||||
public StripeClient(String apiKey) {
|
||||
this.apiKey = apiKey;
|
||||
System.out.println("[Stripe] Initialized with key: " + apiKey.substring(0, 8) + "...");
|
||||
}
|
||||
// Stripe uses cents, not decimal amounts
|
||||
public StripeChargeResult createCharge(String customerId, long amountInCents, String currency) {
|
||||
System.out.printf("[Stripe] Charging customer=%s, amount=%d cents, currency=%s%n",
|
||||
customerId, amountInCents, currency);
|
||||
return new StripeChargeResult("ch_" + System.currentTimeMillis(), true, null);
|
||||
}
|
||||
// Stripe's refund method takes a charge ID and uses different naming
|
||||
public boolean issueRefund(String chargeId, long amountInCents) {
|
||||
System.out.printf("[Stripe] Refunding charge=%s, amount=%d cents%n", chargeId, amountInCents);
|
||||
return true;
|
||||
}
|
||||
// Stripe uses 'retrieve' not 'getStatus', and returns an object
|
||||
public StripeChargeResult retrieveCharge(String chargeId) {
|
||||
System.out.printf("[Stripe] Retrieving charge=%s%n", chargeId);
|
||||
return new StripeChargeResult(chargeId, true, "succeeded");
|
||||
}
|
||||
public static class StripeChargeResult {
|
||||
public final String chargeId;
|
||||
public final boolean success;
|
||||
public final String status;
|
||||
public StripeChargeResult(String chargeId, boolean success, String status) {
|
||||
this.chargeId = chargeId;
|
||||
this.success = success;
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
}
|
||||
36
02-structural/adapter/StripePaymentAdapter.java
Normal file
36
02-structural/adapter/StripePaymentAdapter.java
Normal file
@@ -0,0 +1,36 @@
|
||||
package adapter;
|
||||
/**
|
||||
* The Adapter — bridges StripeClient to PaymentGateway.
|
||||
*
|
||||
* Object Adapter variant: holds a StripeClient instance via composition.
|
||||
* This means it can wrap any StripeClient including subclasses.
|
||||
*/
|
||||
public class StripePaymentAdapter implements PaymentGateway {
|
||||
private final StripeClient stripe;
|
||||
public StripePaymentAdapter(StripeClient stripe) {
|
||||
this.stripe = stripe;
|
||||
}
|
||||
@Override
|
||||
public boolean charge(String customerId, double amount, String currency) {
|
||||
// Translation 1: dollars → cents
|
||||
long amountInCents = Math.round(amount * 100);
|
||||
// Translation 2: charge() → createCharge(), lowercase currency
|
||||
StripeClient.StripeChargeResult result =
|
||||
stripe.createCharge(customerId, amountInCents, currency.toLowerCase());
|
||||
// Translation 3: StripeChargeResult → boolean
|
||||
return result.success;
|
||||
}
|
||||
@Override
|
||||
public boolean refund(String transactionId, double amount) {
|
||||
long amountInCents = Math.round(amount * 100);
|
||||
// Translation: refund() → issueRefund(), your "transactionId" is Stripe's "chargeId"
|
||||
return stripe.issueRefund(transactionId, amountInCents);
|
||||
}
|
||||
@Override
|
||||
public String getStatus(String transactionId) {
|
||||
// Translation: getStatus() → retrieveCharge(), StripeChargeResult → String
|
||||
StripeClient.StripeChargeResult result = stripe.retrieveCharge(transactionId);
|
||||
if (!result.success) return "FAILED";
|
||||
return result.status != null ? result.status.toUpperCase() : "UNKNOWN";
|
||||
}
|
||||
}
|
||||
23
02-structural/adapter/StripePaymentClassAdapter.java
Normal file
23
02-structural/adapter/StripePaymentClassAdapter.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package adapter;
|
||||
// Class Adapter: extends Adaptee, implements Target
|
||||
// Can only be used when you CAN extend the Adaptee (it's not final)
|
||||
public class StripePaymentClassAdapter extends StripeClient implements PaymentGateway {
|
||||
public StripePaymentClassAdapter(String apiKey) {
|
||||
super(apiKey);
|
||||
}
|
||||
@Override
|
||||
public boolean charge(String customerId, double amount, String currency) {
|
||||
long cents = Math.round(amount * 100);
|
||||
return createCharge(customerId, cents, currency.toLowerCase()).success;
|
||||
// Note: calls inherited method directly — no 'stripe.' prefix needed
|
||||
}
|
||||
@Override
|
||||
public boolean refund(String transactionId, double amount) {
|
||||
return issueRefund(transactionId, Math.round(amount * 100));
|
||||
}
|
||||
@Override
|
||||
public String getStatus(String transactionId) {
|
||||
StripeChargeResult r = retrieveCharge(transactionId);
|
||||
return r.success ? (r.status != null ? r.status.toUpperCase() : "UNKNOWN") : "FAILED";
|
||||
}
|
||||
}
|
||||
21
02-structural/bridge/AdvancedRemote.java
Normal file
21
02-structural/bridge/AdvancedRemote.java
Normal file
@@ -0,0 +1,21 @@
|
||||
package bridge;
|
||||
/**
|
||||
* Refined Abstraction — extends RemoteControl with additional capabilities.
|
||||
*
|
||||
* The device hierarchy doesn't change at all. TV and Radio
|
||||
* automatically support mute() and jumpToChannel() because they
|
||||
* implement Device. Zero new code on the implementation side.
|
||||
*/
|
||||
public class AdvancedRemote extends RemoteControl {
|
||||
public AdvancedRemote(Device device) {
|
||||
super(device);
|
||||
}
|
||||
public void mute() {
|
||||
System.out.println(" Muting " + device.getName());
|
||||
device.setVolume(0);
|
||||
}
|
||||
public void jumpToChannel(int channel) {
|
||||
System.out.println(" Jumping to channel " + channel);
|
||||
device.setChannel(channel);
|
||||
}
|
||||
}
|
||||
16
02-structural/bridge/Device.java
Normal file
16
02-structural/bridge/Device.java
Normal file
@@ -0,0 +1,16 @@
|
||||
package bridge;
|
||||
/**
|
||||
* Implementor — the "implementation" side of the bridge.
|
||||
* All devices (TV, Radio, SmartSpeaker, etc.) implement this.
|
||||
* The remote controls only know about this interface, never about specific devices.
|
||||
*/
|
||||
public interface Device {
|
||||
boolean isEnabled();
|
||||
void enable();
|
||||
void disable();
|
||||
int getVolume();
|
||||
void setVolume(int percent);
|
||||
int getChannel();
|
||||
void setChannel(int channel);
|
||||
String getName();
|
||||
}
|
||||
29
02-structural/bridge/Main.java
Normal file
29
02-structural/bridge/Main.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package bridge;
|
||||
public class Main {
|
||||
public static void main(String[] args) {
|
||||
System.out.println("=== Bridge Design Pattern Demo ===\n");
|
||||
// Combination 1: Basic Remote + TV
|
||||
System.out.println("-- Basic Remote controlling TV --");
|
||||
RemoteControl remote1 = new RemoteControl(new TV());
|
||||
remote1.togglePower();
|
||||
remote1.volumeUp();
|
||||
remote1.channelUp();
|
||||
System.out.println();
|
||||
// Combination 2: Advanced Remote + Radio
|
||||
System.out.println("-- Advanced Remote controlling Radio --");
|
||||
AdvancedRemote remote2 = new AdvancedRemote(new Radio());
|
||||
remote2.togglePower();
|
||||
remote2.volumeUp();
|
||||
remote2.mute();
|
||||
remote2.jumpToChannel(91);
|
||||
System.out.println();
|
||||
// Combination 3: Advanced Remote + TV (no new classes needed!)
|
||||
System.out.println("-- Advanced Remote controlling TV --");
|
||||
AdvancedRemote remote3 = new AdvancedRemote(new TV());
|
||||
remote3.togglePower();
|
||||
remote3.jumpToChannel(5);
|
||||
remote3.mute();
|
||||
System.out.println("\n=== Demo complete ===");
|
||||
System.out.println("3 different remote+device combinations, 0 new classes needed.");
|
||||
}
|
||||
}
|
||||
32
02-structural/bridge/README.md
Normal file
32
02-structural/bridge/README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Bridge Design Pattern — Java Example
|
||||
|
||||
**Pattern:** Structural → Bridge
|
||||
**Article:** https://ankurm.com/bridge-design-pattern-java/
|
||||
|
||||
## What this example shows
|
||||
|
||||
Decouples remote controls (Abstraction) from devices (Implementation). A basic remote and an advanced remote each work with any device (TV, Radio) without creating N×M subclasses.
|
||||
|
||||
## How to run
|
||||
|
||||
```bash
|
||||
javac bridge/*.java -d out/bridge
|
||||
java -cp out/bridge bridge.Main
|
||||
```
|
||||
|
||||
Requires Java 25.
|
||||
|
||||
## Post Section ↔ File Mapping
|
||||
|
||||
| Post Section | File(s) |
|
||||
|---|---|
|
||||
| Step 1 — The Implementor Interface | `Device.java` |
|
||||
| Step 2 — Concrete Implementors (TV and Radio) | `TV.java`, `Radio.java` |
|
||||
| Step 3 — The Abstraction (RemoteControl) | `RemoteControl.java` |
|
||||
| Step 4 — Refined Abstraction (AdvancedRemote) | `AdvancedRemote.java` |
|
||||
| Wiring It Together: Main Demo | `Main.java` |
|
||||
|
||||
Note: the "Class Explosion Problem — Visualised" pseudocode and the JDBC/SLF4J snippets shown in the article are illustrative only — they are not part of this repository's runnable example.
|
||||
|
||||
Article: https://ankurm.com/bridge-design-pattern-java/
|
||||
All patterns: https://ankurm.com/design-patterns-java/
|
||||
18
02-structural/bridge/Radio.java
Normal file
18
02-structural/bridge/Radio.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package bridge;
|
||||
// Concrete Implementor #2: Radio
|
||||
public class Radio implements Device {
|
||||
private boolean on = false;
|
||||
private int volume = 20;
|
||||
private int channel = 1;
|
||||
@Override public boolean isEnabled() { return on; }
|
||||
@Override public void enable() { on = true; System.out.println(" [Radio] Powered ON"); }
|
||||
@Override public void disable() { on = false; System.out.println(" [Radio] Powered OFF"); }
|
||||
@Override public int getVolume() { return volume; }
|
||||
@Override public void setVolume(int percent) {
|
||||
this.volume = Math.max(0, Math.min(100, percent));
|
||||
System.out.println(" [Radio] Volume set to " + this.volume);
|
||||
}
|
||||
@Override public int getChannel() { return channel; }
|
||||
@Override public void setChannel(int ch) { this.channel = ch; System.out.println(" [Radio] Frequency -> " + ch); }
|
||||
@Override public String getName() { return "JBL Radio"; }
|
||||
}
|
||||
27
02-structural/bridge/RemoteControl.java
Normal file
27
02-structural/bridge/RemoteControl.java
Normal file
@@ -0,0 +1,27 @@
|
||||
package bridge;
|
||||
/**
|
||||
* Abstraction — the remote control.
|
||||
*
|
||||
* Key point: RemoteControl holds a Device (the bridge) and delegates
|
||||
* all actual work to it. It adds higher-level semantics on top:
|
||||
* "togglePower" instead of separate enable()/disable() calls.
|
||||
*/
|
||||
public class RemoteControl {
|
||||
protected Device device; // THE BRIDGE
|
||||
public RemoteControl(Device device) {
|
||||
this.device = device;
|
||||
System.out.println("Remote paired with: " + device.getName());
|
||||
}
|
||||
// User action → device operation translation
|
||||
public void togglePower() {
|
||||
if (device.isEnabled()) {
|
||||
device.disable();
|
||||
} else {
|
||||
device.enable();
|
||||
}
|
||||
}
|
||||
public void volumeUp() { device.setVolume(device.getVolume() + 10); }
|
||||
public void volumeDown() { device.setVolume(device.getVolume() - 10); }
|
||||
public void channelUp() { device.setChannel(device.getChannel() + 1); }
|
||||
public void channelDown() { device.setChannel(device.getChannel() - 1); }
|
||||
}
|
||||
18
02-structural/bridge/TV.java
Normal file
18
02-structural/bridge/TV.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package bridge;
|
||||
// Concrete Implementor #1: Television
|
||||
public class TV implements Device {
|
||||
private boolean on = false;
|
||||
private int volume = 30;
|
||||
private int channel = 1;
|
||||
@Override public boolean isEnabled() { return on; }
|
||||
@Override public void enable() { on = true; System.out.println(" [TV] Powered ON"); }
|
||||
@Override public void disable() { on = false; System.out.println(" [TV] Powered OFF"); }
|
||||
@Override public int getVolume() { return volume; }
|
||||
@Override public void setVolume(int percent) {
|
||||
this.volume = Math.max(0, Math.min(100, percent));
|
||||
System.out.println(" [TV] Volume set to " + this.volume);
|
||||
}
|
||||
@Override public int getChannel() { return channel; }
|
||||
@Override public void setChannel(int ch) { this.channel = ch; System.out.println(" [TV] Channel -> " + ch); }
|
||||
@Override public String getName() { return "Samsung TV"; }
|
||||
}
|
||||
39
02-structural/composite/Directory.java
Normal file
39
02-structural/composite/Directory.java
Normal file
@@ -0,0 +1,39 @@
|
||||
package composite;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
/**
|
||||
* Composite — a directory that holds both Files (leaves) and other Directories.
|
||||
*
|
||||
* getSize() is recursive: asks each child for its size and sums them.
|
||||
* print() recurses with deeper indentation.
|
||||
*
|
||||
* The caller doesn't care whether a child is a File or Directory —
|
||||
* both answer getSize() and print() the same way.
|
||||
*/
|
||||
public class Directory implements FileSystemItem {
|
||||
private final String name;
|
||||
private final List<FileSystemItem> children = new ArrayList<>();
|
||||
public Directory(String name) { this.name = name; }
|
||||
// Fluent add() — allows chaining: dir.add(file1).add(file2).add(subDir)
|
||||
public Directory add(FileSystemItem item) {
|
||||
children.add(item);
|
||||
return this;
|
||||
}
|
||||
public void remove(FileSystemItem item) { children.remove(item); }
|
||||
@Override public String getName() { return name; }
|
||||
@Override
|
||||
public long getSize() {
|
||||
// Each child knows its own size: files return bytes, directories recurse.
|
||||
// This is the Composite's core: uniform delegation down the tree.
|
||||
return children.stream()
|
||||
.mapToLong(FileSystemItem::getSize)
|
||||
.sum();
|
||||
}
|
||||
@Override
|
||||
public void print(String indent) {
|
||||
System.out.printf("%s[DIR] %s/ (%,d bytes total)%n", indent, name, getSize());
|
||||
for (FileSystemItem child : children) {
|
||||
child.print(indent + " "); // each level indents 4 more spaces
|
||||
}
|
||||
}
|
||||
}
|
||||
20
02-structural/composite/File.java
Normal file
20
02-structural/composite/File.java
Normal file
@@ -0,0 +1,20 @@
|
||||
package composite;
|
||||
/**
|
||||
* Leaf — a single file with no children.
|
||||
* getSize() returns its own bytes. print() outputs a single line.
|
||||
* No knowledge of directories or nesting exists in this class.
|
||||
*/
|
||||
public class File implements FileSystemItem {
|
||||
private final String name;
|
||||
private final long size;
|
||||
public File(String name, long sizeBytes) {
|
||||
this.name = name;
|
||||
this.size = sizeBytes;
|
||||
}
|
||||
@Override public String getName() { return name; }
|
||||
@Override public long getSize() { return size; }
|
||||
@Override
|
||||
public void print(String indent) {
|
||||
System.out.printf("%s[FILE] %s (%,d bytes)%n", indent, name, size);
|
||||
}
|
||||
}
|
||||
10
02-structural/composite/FileSystemItem.java
Normal file
10
02-structural/composite/FileSystemItem.java
Normal file
@@ -0,0 +1,10 @@
|
||||
package composite;
|
||||
/**
|
||||
* Component — the common interface for both files (leaves) and directories (composites).
|
||||
* Clients work through this interface and never need to know which type they're dealing with.
|
||||
*/
|
||||
public interface FileSystemItem {
|
||||
String getName();
|
||||
long getSize(); // total size in bytes — recursive for directories
|
||||
void print(String indent); // display the tree at the given indentation level
|
||||
}
|
||||
36
02-structural/composite/Main.java
Normal file
36
02-structural/composite/Main.java
Normal file
@@ -0,0 +1,36 @@
|
||||
package composite;
|
||||
public class Main {
|
||||
public static void main(String[] args) {
|
||||
System.out.println("=== Composite Design Pattern Demo ===\n");
|
||||
// Build the tree — mix files and directories freely
|
||||
Directory root = new Directory("project");
|
||||
Directory src = new Directory("src");
|
||||
Directory main = new Directory("main");
|
||||
main.add(new File("App.java", 4_200))
|
||||
.add(new File("Config.java", 1_800))
|
||||
.add(new File("Application.yml", 3_100));
|
||||
Directory test = new Directory("test");
|
||||
test.add(new File("AppTest.java", 2_600))
|
||||
.add(new File("ConfigTest.java", 1_200));
|
||||
src.add(main).add(test);
|
||||
Directory resources = new Directory("resources");
|
||||
resources.add(new File("application.yml", 1_500))
|
||||
.add(new File("logback.xml", 900))
|
||||
.add(new File("banner.txt", 200));
|
||||
root.add(src)
|
||||
.add(resources)
|
||||
.add(new File("pom.xml", 8_400))
|
||||
.add(new File("README.md", 2_100));
|
||||
// Print the entire tree — recursion is automatic
|
||||
System.out.println("File system tree:");
|
||||
root.print("");
|
||||
System.out.printf("%nTotal project size: %,d bytes%n", root.getSize());
|
||||
// The key demonstration: File and Directory through the same interface
|
||||
System.out.println("\n-- Treating File and Directory uniformly --");
|
||||
FileSystemItem[] items = { new File("standalone.txt", 500), src };
|
||||
for (FileSystemItem item : items) {
|
||||
System.out.printf("%s -> size: %,d bytes%n", item.getName(), item.getSize());
|
||||
}
|
||||
System.out.println("\n=== Demo complete ===");
|
||||
}
|
||||
}
|
||||
30
02-structural/composite/README.md
Normal file
30
02-structural/composite/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Composite Design Pattern — Java Example
|
||||
|
||||
**Pattern:** Structural → Composite
|
||||
**Article:** https://ankurm.com/composite-design-pattern-java/
|
||||
|
||||
## What this example shows
|
||||
|
||||
Builds a file system tree where files (leaves) and directories (composites) share the same `FileSystemItem` interface. Callers compute size or print the tree without ever checking whether a node is a file or a directory.
|
||||
|
||||
## How to run
|
||||
|
||||
```bash
|
||||
javac composite/*.java -d out/composite
|
||||
java -cp out/composite composite.Main
|
||||
```
|
||||
|
||||
Requires Java 25.
|
||||
|
||||
## Post Section ↔ File Mapping
|
||||
|
||||
| Post Section | File(s) |
|
||||
|---|---|
|
||||
| The Problem: Treating Leaves and Containers Uniformly | illustrative only — not part of this repository's runnable example |
|
||||
| Step 1 — The Component Interface | `FileSystemItem.java` |
|
||||
| Step 2 — The Leaf (File) | `File.java` |
|
||||
| Step 3 — The Composite (Directory) | `Directory.java` |
|
||||
| Building and Using the Tree | `Main.java` |
|
||||
|
||||
Article: https://ankurm.com/composite-design-pattern-java/
|
||||
All patterns: https://ankurm.com/design-patterns-java/
|
||||
42
02-structural/decorator/Main.java
Normal file
42
02-structural/decorator/Main.java
Normal file
@@ -0,0 +1,42 @@
|
||||
package decorator;
|
||||
public class Main {
|
||||
public static void main(String[] args) {
|
||||
System.out.println("=== Decorator Design Pattern Demo ===\n");
|
||||
|
||||
String input = " hello world, badword is here ";
|
||||
System.out.println("Input: \"" + input + "\"\n");
|
||||
|
||||
// Stack 1: just trim
|
||||
TextProcessor trimOnly = new TrimDecorator(new PlainTextProcessor());
|
||||
System.out.println("Trim only: \"" + trimOnly.process(input) + "\"");
|
||||
|
||||
// Stack 2: trim first (innermost), then uppercase (outermost)
|
||||
// Execution order: PlainText → Trim → UpperCase
|
||||
TextProcessor trimThenUpper =
|
||||
new UpperCaseDecorator(
|
||||
new TrimDecorator(
|
||||
new PlainTextProcessor()));
|
||||
System.out.println("Trim + UpperCase: \"" + trimThenUpper.process(input) + "\"");
|
||||
|
||||
// Stack 3: trim, filter profanity, then uppercase
|
||||
TextProcessor full =
|
||||
new UpperCaseDecorator(
|
||||
new ProfanityFilterDecorator(
|
||||
new TrimDecorator(
|
||||
new PlainTextProcessor())));
|
||||
System.out.println("Trim + Filter + Upper: \"" + full.process(input) + "\"");
|
||||
|
||||
// Stack 4: filter THEN trim — different result because order changed
|
||||
TextProcessor filterFirst =
|
||||
new TrimDecorator(
|
||||
new ProfanityFilterDecorator(
|
||||
new PlainTextProcessor()));
|
||||
System.out.println("Filter + Trim: \"" + filterFirst.process(input) + "\"");
|
||||
|
||||
System.out.println("\n--- JDK equivalent ---");
|
||||
System.out.println("new BufferedReader(new InputStreamReader(socket.getInputStream()))");
|
||||
System.out.println("Same pattern: each wrapper adds one behaviour, order matters.");
|
||||
|
||||
System.out.println("\n=== Demo complete ===");
|
||||
}
|
||||
}
|
||||
12
02-structural/decorator/PlainTextProcessor.java
Normal file
12
02-structural/decorator/PlainTextProcessor.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package decorator;
|
||||
/**
|
||||
* Concrete Component — the starting point for decoration.
|
||||
* Returns text unchanged. Decorators wrap around this
|
||||
* and transform the result one layer at a time.
|
||||
*/
|
||||
public class PlainTextProcessor implements TextProcessor {
|
||||
@Override
|
||||
public String process(String text) {
|
||||
return text; // no transformation — base case
|
||||
}
|
||||
}
|
||||
14
02-structural/decorator/ProfanityFilterDecorator.java
Normal file
14
02-structural/decorator/ProfanityFilterDecorator.java
Normal file
@@ -0,0 +1,14 @@
|
||||
package decorator;
|
||||
/** Concrete Decorator: replaces profane words with asterisks. */
|
||||
public class ProfanityFilterDecorator extends TextDecorator {
|
||||
private static final String[] BAD_WORDS = {"badword", "spam"};
|
||||
public ProfanityFilterDecorator(TextProcessor wrapped) { super(wrapped); }
|
||||
@Override
|
||||
public String process(String text) {
|
||||
String result = super.process(text);
|
||||
for (String word : BAD_WORDS) {
|
||||
result = result.replaceAll("(?i)" + word, "*".repeat(word.length()));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
32
02-structural/decorator/README.md
Normal file
32
02-structural/decorator/README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Decorator Design Pattern — Java Example
|
||||
|
||||
**Pattern:** Structural → Decorator
|
||||
**Article:** https://ankurm.com/decorator-design-pattern-java/
|
||||
|
||||
## What this example shows
|
||||
|
||||
Builds a text-processing pipeline where behaviours (trim, uppercase, profanity filter) are stacked as wrapper objects around a `PlainTextProcessor`, all sharing the `TextProcessor` interface — the same idea behind `new BufferedReader(new InputStreamReader(...))` in the JDK.
|
||||
|
||||
## How to run
|
||||
|
||||
```bash
|
||||
javac decorator/*.java -d out/decorator
|
||||
java -cp out/decorator decorator.Main
|
||||
```
|
||||
|
||||
Requires Java 25.
|
||||
|
||||
## Post Section ↔ File Mapping
|
||||
|
||||
| Post Section | File(s) |
|
||||
|---|---|
|
||||
| Step 1 — Component Interface | `TextProcessor.java` |
|
||||
| Step 2 — Concrete Component | `PlainTextProcessor.java` |
|
||||
| Step 3 — Base Decorator | `TextDecorator.java` |
|
||||
| Step 4 — Concrete Decorators | `UpperCaseDecorator.java`, `TrimDecorator.java`, `ProfanityFilterDecorator.java` |
|
||||
| Stacking the Decorators — Order Matters | `Main.java` |
|
||||
|
||||
Note: the JDK `InputStream`/`BufferedInputStream`/`GZIPInputStream` snippet under "Decorator in the JDK: I/O Streams" is illustrative only — it is not part of this repository's runnable example.
|
||||
|
||||
Article: https://ankurm.com/decorator-design-pattern-java/
|
||||
All patterns: https://ankurm.com/design-patterns-java/
|
||||
22
02-structural/decorator/TextDecorator.java
Normal file
22
02-structural/decorator/TextDecorator.java
Normal file
@@ -0,0 +1,22 @@
|
||||
package decorator;
|
||||
/**
|
||||
* Base Decorator — holds a reference to the wrapped TextProcessor
|
||||
* and delegates to it. Concrete decorators extend this class.
|
||||
*
|
||||
* Why an abstract class rather than another interface implementation?
|
||||
* To avoid repeating the wrapping boilerplate (the field + constructor +
|
||||
* delegation call) in every single concrete decorator.
|
||||
*/
|
||||
public abstract class TextDecorator implements TextProcessor {
|
||||
protected final TextProcessor wrapped;
|
||||
protected TextDecorator(TextProcessor wrapped) {
|
||||
this.wrapped = wrapped;
|
||||
}
|
||||
@Override
|
||||
public String process(String text) {
|
||||
// Default: pass through to the wrapped processor.
|
||||
// Concrete decorators call super.process(text) to invoke this,
|
||||
// then apply their own transformation to the result.
|
||||
return wrapped.process(text);
|
||||
}
|
||||
}
|
||||
9
02-structural/decorator/TextProcessor.java
Normal file
9
02-structural/decorator/TextProcessor.java
Normal file
@@ -0,0 +1,9 @@
|
||||
package decorator;
|
||||
/**
|
||||
* Component — defines what all text processors do.
|
||||
* Both the base implementation AND every decorator implement this.
|
||||
* Sharing this type is what makes decorators stackable.
|
||||
*/
|
||||
public interface TextProcessor {
|
||||
String process(String text);
|
||||
}
|
||||
9
02-structural/decorator/TrimDecorator.java
Normal file
9
02-structural/decorator/TrimDecorator.java
Normal file
@@ -0,0 +1,9 @@
|
||||
package decorator;
|
||||
/** Concrete Decorator: trims leading and trailing whitespace. */
|
||||
public class TrimDecorator extends TextDecorator {
|
||||
public TrimDecorator(TextProcessor wrapped) { super(wrapped); }
|
||||
@Override
|
||||
public String process(String text) {
|
||||
return super.process(text).trim();
|
||||
}
|
||||
}
|
||||
13
02-structural/decorator/UpperCaseDecorator.java
Normal file
13
02-structural/decorator/UpperCaseDecorator.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package decorator;
|
||||
/** Concrete Decorator: converts result to upper case. */
|
||||
public class UpperCaseDecorator extends TextDecorator {
|
||||
public UpperCaseDecorator(TextProcessor wrapped) {
|
||||
super(wrapped);
|
||||
}
|
||||
@Override
|
||||
public String process(String text) {
|
||||
// 1. Get result from everything below in the stack
|
||||
// 2. Apply OUR transformation: uppercase
|
||||
return super.process(text).toUpperCase();
|
||||
}
|
||||
}
|
||||
9
02-structural/facade/AudioMixer.java
Normal file
9
02-structural/facade/AudioMixer.java
Normal file
@@ -0,0 +1,9 @@
|
||||
package facade;
|
||||
|
||||
/** AudioMixer — normalises audio tracks after conversion. */
|
||||
class AudioMixer {
|
||||
static VideoFile fix(VideoFile result) {
|
||||
System.out.println(" AudioMixer: fixing audio tracks");
|
||||
return new VideoFile(result.getFilename());
|
||||
}
|
||||
}
|
||||
15
02-structural/facade/BitrateReader.java
Normal file
15
02-structural/facade/BitrateReader.java
Normal file
@@ -0,0 +1,15 @@
|
||||
package facade;
|
||||
|
||||
/** BitrateReader — reads the video buffer and converts it between codecs. */
|
||||
class BitrateReader {
|
||||
static VideoFile read(VideoFile file, Codec codec) {
|
||||
System.out.println(" BitrateReader: reading " + file.getFilename()
|
||||
+ " with codec " + codec.getName());
|
||||
return new VideoFile(file.getFilename());
|
||||
}
|
||||
|
||||
static VideoFile convert(VideoFile buffer, Codec codec) {
|
||||
System.out.println(" BitrateReader: converting to " + codec.getName());
|
||||
return new VideoFile(buffer.getFilename());
|
||||
}
|
||||
}
|
||||
6
02-structural/facade/Codec.java
Normal file
6
02-structural/facade/Codec.java
Normal file
@@ -0,0 +1,6 @@
|
||||
package facade;
|
||||
|
||||
/** Codec — common interface implemented by each concrete compression codec. */
|
||||
interface Codec {
|
||||
String getName();
|
||||
}
|
||||
10
02-structural/facade/CodecFactory.java
Normal file
10
02-structural/facade/CodecFactory.java
Normal file
@@ -0,0 +1,10 @@
|
||||
package facade;
|
||||
|
||||
/** CodecFactory — inspects a VideoFile and returns the matching Codec. */
|
||||
class CodecFactory {
|
||||
static Codec extract(VideoFile file) {
|
||||
System.out.println(" CodecFactory: extracting codec from " + file.getFilename());
|
||||
if ("mpeg4".equals(file.getCodecType())) return new MPEG4CompressionCodec();
|
||||
return new OggCompressionCodec();
|
||||
}
|
||||
}
|
||||
6
02-structural/facade/MPEG4CompressionCodec.java
Normal file
6
02-structural/facade/MPEG4CompressionCodec.java
Normal file
@@ -0,0 +1,6 @@
|
||||
package facade;
|
||||
|
||||
/** Concrete Codec: MPEG-4 compression. */
|
||||
class MPEG4CompressionCodec implements Codec {
|
||||
@Override public String getName() { return "mpeg4"; }
|
||||
}
|
||||
30
02-structural/facade/Main.java
Normal file
30
02-structural/facade/Main.java
Normal file
@@ -0,0 +1,30 @@
|
||||
package facade;
|
||||
|
||||
/**
|
||||
* Facade Design Pattern — Runnable Demo
|
||||
*
|
||||
* Demonstrates reducing a complex video conversion subsystem
|
||||
* to a single method call via a Facade.
|
||||
*
|
||||
* Run: javac facade/*.java && java facade.Main
|
||||
* Article: https://ankurm.com/facade-design-pattern-java/
|
||||
*/
|
||||
public class Main {
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println("=== Facade Design Pattern Demo ===\n");
|
||||
|
||||
VideoConversionFacade converter = new VideoConversionFacade();
|
||||
|
||||
System.out.println("-- Client: just one method call --");
|
||||
String result1 = converter.convertVideo("holiday.ogg", "mp4");
|
||||
System.out.println("Output: " + result1);
|
||||
|
||||
System.out.println();
|
||||
String result2 = converter.convertVideo("presentation.mp4", "ogg");
|
||||
System.out.println("Output: " + result2);
|
||||
|
||||
System.out.println("\nClient code: 1 line. Subsystem: 6 classes. Facade hides the complexity.");
|
||||
System.out.println("\n=== Demo complete ===");
|
||||
}
|
||||
}
|
||||
6
02-structural/facade/OggCompressionCodec.java
Normal file
6
02-structural/facade/OggCompressionCodec.java
Normal file
@@ -0,0 +1,6 @@
|
||||
package facade;
|
||||
|
||||
/** Concrete Codec: Ogg compression. */
|
||||
class OggCompressionCodec implements Codec {
|
||||
@Override public String getName() { return "ogg"; }
|
||||
}
|
||||
35
02-structural/facade/README.md
Normal file
35
02-structural/facade/README.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Facade Design Pattern — Java Example
|
||||
|
||||
**Pattern:** Structural → Facade
|
||||
**Article:** https://ankurm.com/facade-design-pattern-java/
|
||||
|
||||
## What this example shows
|
||||
|
||||
A video-conversion subsystem (`VideoFile`, `Codec`, `MPEG4CompressionCodec`, `OggCompressionCodec`, `CodecFactory`, `BitrateReader`, `AudioMixer`) is wrapped behind one class, `VideoConversionFacade`, that exposes a single `convertVideo()` method. Callers never touch the six subsystem classes directly, but they remain public and usable on their own for advanced use cases.
|
||||
|
||||
## How to run
|
||||
|
||||
```bash
|
||||
javac facade/*.java -d out/facade
|
||||
java -cp out/facade facade.Main
|
||||
```
|
||||
|
||||
Requires Java 25.
|
||||
|
||||
## Post Section ↔ File Mapping
|
||||
|
||||
| Post Section | File(s) |
|
||||
|---|---|
|
||||
| The Subsystem (Complex, But Unchanged) — VideoFile | `VideoFile.java` |
|
||||
| The Subsystem (Complex, But Unchanged) — Codec interface | `Codec.java` |
|
||||
| The Subsystem (Complex, But Unchanged) — Concrete Codecs | `MPEG4CompressionCodec.java`, `OggCompressionCodec.java` |
|
||||
| The Subsystem (Complex, But Unchanged) — CodecFactory | `CodecFactory.java` |
|
||||
| The Subsystem (Complex, But Unchanged) — BitrateReader | `BitrateReader.java` |
|
||||
| The Subsystem (Complex, But Unchanged) — AudioMixer | `AudioMixer.java` |
|
||||
| The Facade | `VideoConversionFacade.java` |
|
||||
| Client Code: One Line | `Main.java` |
|
||||
|
||||
Note: the SLF4J `LoggerFactory`, JDBC `DriverManager`, and Spring `JdbcTemplate` snippets under "Facade in the JDK and Real Frameworks" are illustrative only — they are not part of this repository's runnable example.
|
||||
|
||||
Article: https://ankurm.com/facade-design-pattern-java/
|
||||
All patterns: https://ankurm.com/design-patterns-java/
|
||||
39
02-structural/facade/VideoConversionFacade.java
Normal file
39
02-structural/facade/VideoConversionFacade.java
Normal file
@@ -0,0 +1,39 @@
|
||||
package facade;
|
||||
|
||||
/**
|
||||
* Facade — the single, simple entry point to a complex video subsystem.
|
||||
*
|
||||
* Without this class, clients need to know about CodecFactory,
|
||||
* BitrateReader, AudioMixer, and VideoFile — 4 classes, dozens of methods.
|
||||
* The facade reduces that to ONE method call.
|
||||
*
|
||||
* The subsystem classes still exist and can be used directly
|
||||
* by advanced users who need fine-grained control.
|
||||
*/
|
||||
public class VideoConversionFacade {
|
||||
|
||||
public String convertVideo(String inputFile, String targetFormat) {
|
||||
System.out.println("VideoConversionFacade: starting conversion of " + inputFile);
|
||||
|
||||
// Step 1: open the file and detect codec
|
||||
VideoFile file = new VideoFile(inputFile);
|
||||
Codec sourceCodec = CodecFactory.extract(file);
|
||||
|
||||
// Step 2: prepare destination codec
|
||||
Codec destCodec;
|
||||
if ("mp4".equals(targetFormat)) {
|
||||
destCodec = new MPEG4CompressionCodec();
|
||||
} else {
|
||||
destCodec = new OggCompressionCodec();
|
||||
}
|
||||
|
||||
// Step 3: read, mix audio, encode
|
||||
VideoFile buffer = BitrateReader.read(file, sourceCodec);
|
||||
VideoFile intermediateResult = BitrateReader.convert(buffer, destCodec);
|
||||
VideoFile result = AudioMixer.fix(intermediateResult);
|
||||
|
||||
String outputFilename = inputFile.replaceAll("\\.[^.]+$", "." + targetFormat);
|
||||
System.out.println("VideoConversionFacade: conversion complete -> " + outputFilename);
|
||||
return outputFilename;
|
||||
}
|
||||
}
|
||||
23
02-structural/facade/VideoFile.java
Normal file
23
02-structural/facade/VideoFile.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package facade;
|
||||
|
||||
/**
|
||||
* Complex subsystem classes — these are what the Facade hides.
|
||||
* Each class has its own complex API; clients shouldn't need to know all of them.
|
||||
*/
|
||||
class VideoFile {
|
||||
private final String filename;
|
||||
private final String codecType;
|
||||
|
||||
VideoFile(String filename) {
|
||||
this(filename, filename.endsWith(".mp4") ? "mpeg4" : "ogg");
|
||||
}
|
||||
|
||||
VideoFile(String filename, String codec) {
|
||||
this.filename = filename;
|
||||
this.codecType = codec;
|
||||
System.out.println(" VideoFile: " + filename + " [codec: " + codecType + "]");
|
||||
}
|
||||
|
||||
public String getFilename() { return filename; }
|
||||
public String getCodecType() { return codecType; }
|
||||
}
|
||||
50
02-structural/flyweight/Main.java
Normal file
50
02-structural/flyweight/Main.java
Normal file
@@ -0,0 +1,50 @@
|
||||
package flyweight;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* Flyweight Design Pattern — Runnable Demo
|
||||
*
|
||||
* Creates 1000 trees of only 3 species. Without Flyweight: 1000 TreeType
|
||||
* objects. With Flyweight: 3 TreeType objects (one per species), shared.
|
||||
*
|
||||
* Run: javac flyweight/*.java && java flyweight.Main
|
||||
* Article: https://ankurm.com/flyweight-design-pattern-java/
|
||||
*/
|
||||
public class Main {
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println("=== Flyweight Design Pattern Demo ===\n");
|
||||
|
||||
List<Tree> forest = new ArrayList<>();
|
||||
Random rnd = new Random(42);
|
||||
|
||||
String[][] treeSpecs = {
|
||||
{"Oak", "dark-green", "rough-bark"},
|
||||
{"Pine", "blue-green", "needle-texture"},
|
||||
{"Birch","light-green","smooth-white-bark"}
|
||||
};
|
||||
|
||||
System.out.println("Creating 1,000 trees (only 3 TreeType objects should be created):");
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
String[] spec = treeSpecs[i % 3];
|
||||
TreeType type = TreeFactory.getTreeType(spec[0], spec[1], spec[2]);
|
||||
forest.add(new Tree(rnd.nextInt(800), rnd.nextInt(600), type));
|
||||
}
|
||||
|
||||
System.out.println("\nForest created. Drawing first 5 trees:");
|
||||
for (int i = 0; i < 5; i++) {
|
||||
forest.get(i).draw();
|
||||
}
|
||||
|
||||
System.out.println("\n--- Memory summary ---");
|
||||
System.out.println("Trees in forest : " + forest.size());
|
||||
System.out.println("Unique TreeType objects in pool: " + TreeFactory.getPoolSize());
|
||||
System.out.println("Without Flyweight : 1,000 TreeType objects");
|
||||
System.out.println("With Flyweight : " + TreeFactory.getPoolSize() + " TreeType objects shared");
|
||||
|
||||
System.out.println("\n=== Demo complete ===");
|
||||
}
|
||||
}
|
||||
31
02-structural/flyweight/README.md
Normal file
31
02-structural/flyweight/README.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Flyweight Design Pattern — Java Example
|
||||
|
||||
**Pattern:** Structural → Flyweight
|
||||
**Article:** https://ankurm.com/flyweight-design-pattern-java/
|
||||
|
||||
## What this example shows
|
||||
|
||||
A forest of 1,000 trees shares just 3 `TreeType` flyweight objects (one per species) instead of allocating a new heavy object per tree. `TreeType` holds the intrinsic (shared) state — name, color, texture. `Tree` holds only the extrinsic (unique) state — x/y position — plus a reference to its shared `TreeType`. `TreeFactory` is the pool manager: `Map.computeIfAbsent()` returns an existing flyweight or creates and caches one.
|
||||
|
||||
## How to run
|
||||
|
||||
```bash
|
||||
javac flyweight/*.java -d out/flyweight
|
||||
java -cp out/flyweight flyweight.Main
|
||||
```
|
||||
|
||||
Requires Java 25.
|
||||
|
||||
## Post Section ↔ File Mapping
|
||||
|
||||
| Post Section | File(s) |
|
||||
|---|---|
|
||||
| Step 1 — The Flyweight (TreeType) | `TreeType.java` |
|
||||
| Step 2 — The Factory (TreeFactory) | `TreeFactory.java` |
|
||||
| Step 3 — The Context (Tree) | `Tree.java` |
|
||||
| Putting the Forest Together | `Main.java` |
|
||||
|
||||
Note: the "Flyweight in the JDK: String Pool and Integer Cache" and "Measuring the Benefit" snippets are illustrative only — they are not part of this repository's runnable example.
|
||||
|
||||
Article: https://ankurm.com/flyweight-design-pattern-java/
|
||||
All patterns: https://ankurm.com/design-patterns-java/
|
||||
31
02-structural/flyweight/Tree.java
Normal file
31
02-structural/flyweight/Tree.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package flyweight;
|
||||
|
||||
/**
|
||||
* Context — stores the UNIQUE (extrinsic) state for each tree instance.
|
||||
* This is NOT the flyweight itself; it's the lightweight object that
|
||||
* holds position data and delegates rendering to a shared TreeType.
|
||||
*
|
||||
* 10,000 Tree objects × (x:4 bytes + y:4 bytes + reference:8 bytes) = ~160 KB
|
||||
* vs.
|
||||
* 10,000 Tree objects × (name + color + texture + x + y) = potentially MBs
|
||||
*/
|
||||
public class Tree {
|
||||
|
||||
// Extrinsic (unique) state — different per tree
|
||||
private final int x;
|
||||
private final int y;
|
||||
|
||||
// Reference to the SHARED flyweight
|
||||
private final TreeType type;
|
||||
|
||||
public Tree(int x, int y, TreeType type) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public void draw() {
|
||||
// Passes extrinsic state (position) into the shared flyweight
|
||||
type.draw(x, y);
|
||||
}
|
||||
}
|
||||
29
02-structural/flyweight/TreeFactory.java
Normal file
29
02-structural/flyweight/TreeFactory.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package flyweight;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Flyweight Factory — the cache that ensures each unique TreeType
|
||||
* is only created once, no matter how many trees use it.
|
||||
*
|
||||
* This is the piece that makes Flyweight work:
|
||||
* it intercepts creation requests and returns an existing
|
||||
* shared instance if one already exists.
|
||||
*/
|
||||
public class TreeFactory {
|
||||
|
||||
// The pool of shared flyweights
|
||||
private static final Map<String, TreeType> treeTypes = new HashMap<>();
|
||||
|
||||
public static TreeType getTreeType(String name, String color, String texture) {
|
||||
String key = name + "_" + color + "_" + texture;
|
||||
|
||||
// Only create a new TreeType if we haven't seen this combination before
|
||||
return treeTypes.computeIfAbsent(key, k -> new TreeType(name, color, texture));
|
||||
}
|
||||
|
||||
public static int getPoolSize() {
|
||||
return treeTypes.size();
|
||||
}
|
||||
}
|
||||
29
02-structural/flyweight/TreeType.java
Normal file
29
02-structural/flyweight/TreeType.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package flyweight;
|
||||
|
||||
/**
|
||||
* Flyweight — holds the SHARED (intrinsic) state.
|
||||
* TreeType is shared between all trees of the same species.
|
||||
*
|
||||
* If you have 10,000 oak trees, there's ONE OakType object in memory.
|
||||
* Each individual tree only stores its unique position (extrinsic state).
|
||||
*/
|
||||
public class TreeType {
|
||||
|
||||
// Intrinsic (shared) state — same for all trees of this species
|
||||
private final String name;
|
||||
private final String color;
|
||||
private final String texture; // imagine a large texture bitmap here
|
||||
|
||||
public TreeType(String name, String color, String texture) {
|
||||
this.name = name;
|
||||
this.color = color;
|
||||
this.texture = texture;
|
||||
System.out.println(" [TreeType created: " + name + "]"); // see how few are created
|
||||
}
|
||||
|
||||
// Extrinsic state (x, y) is passed IN at render time — NOT stored here
|
||||
public void draw(int x, int y) {
|
||||
System.out.printf(" Drawing %s tree [%s/%s] at (%d, %d)%n",
|
||||
name, color, texture, x, y);
|
||||
}
|
||||
}
|
||||
11
02-structural/proxy/DatabaseConnection.java
Normal file
11
02-structural/proxy/DatabaseConnection.java
Normal file
@@ -0,0 +1,11 @@
|
||||
package proxy;
|
||||
|
||||
/**
|
||||
* Subject interface — defines what both the real object and proxy expose.
|
||||
* Clients depend on this, not on the concrete class.
|
||||
*/
|
||||
public interface DatabaseConnection {
|
||||
void connect();
|
||||
String executeQuery(String sql);
|
||||
void disconnect();
|
||||
}
|
||||
52
02-structural/proxy/LazyConnectionProxy.java
Normal file
52
02-structural/proxy/LazyConnectionProxy.java
Normal file
@@ -0,0 +1,52 @@
|
||||
package proxy;
|
||||
|
||||
/**
|
||||
* Virtual Proxy — delays creating the RealDatabaseConnection until
|
||||
* the first actual query is made. If no query is ever made (e.g.,
|
||||
* the service is initialized but never used in this request),
|
||||
* the expensive connection is never opened.
|
||||
*
|
||||
* This is exactly how Hibernate proxies work: entities are not
|
||||
* loaded from the database until you access a field on them.
|
||||
*/
|
||||
public class LazyConnectionProxy implements DatabaseConnection {
|
||||
|
||||
private final String url;
|
||||
private RealDatabaseConnection real; // null until first use
|
||||
|
||||
public LazyConnectionProxy(String url) {
|
||||
this.url = url;
|
||||
System.out.println("[Proxy] Created for " + url + " (real connection NOT opened yet)");
|
||||
}
|
||||
|
||||
// Lazy initialization — create and connect only on first real need
|
||||
private void initIfNeeded() {
|
||||
if (real == null) {
|
||||
System.out.println("[Proxy] First access — initializing real connection...");
|
||||
real = new RealDatabaseConnection(url);
|
||||
real.connect();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect() {
|
||||
// Proxy absorbs the connect() call — real connection opened lazily
|
||||
System.out.println("[Proxy] connect() called — deferring to first query");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String executeQuery(String sql) {
|
||||
initIfNeeded(); // NOW we actually need the connection
|
||||
return real.executeQuery(sql);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnect() {
|
||||
if (real != null) {
|
||||
real.disconnect();
|
||||
real = null;
|
||||
} else {
|
||||
System.out.println("[Proxy] disconnect() called but connection was never opened");
|
||||
}
|
||||
}
|
||||
}
|
||||
41
02-structural/proxy/LoggingProxy.java
Normal file
41
02-structural/proxy/LoggingProxy.java
Normal file
@@ -0,0 +1,41 @@
|
||||
package proxy;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
/**
|
||||
* Logging Proxy — adds timing and audit logging around every query
|
||||
* without touching RealDatabaseConnection or any of its callers.
|
||||
*
|
||||
* This is the "cross-cutting concern" use case of Proxy,
|
||||
* the same mechanism behind Spring AOP's @Around advice.
|
||||
*/
|
||||
public class LoggingProxy implements DatabaseConnection {
|
||||
|
||||
private final DatabaseConnection target;
|
||||
|
||||
public LoggingProxy(DatabaseConnection target) {
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect() {
|
||||
System.out.println("[Log] connect() at " + Instant.now());
|
||||
target.connect();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String executeQuery(String sql) {
|
||||
long start = System.currentTimeMillis();
|
||||
System.out.println("[Log] QUERY START: " + sql);
|
||||
String result = target.executeQuery(sql);
|
||||
long elapsed = System.currentTimeMillis() - start;
|
||||
System.out.println("[Log] QUERY END: " + elapsed + "ms | result: " + result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnect() {
|
||||
System.out.println("[Log] disconnect() at " + Instant.now());
|
||||
target.disconnect();
|
||||
}
|
||||
}
|
||||
51
02-structural/proxy/Main.java
Normal file
51
02-structural/proxy/Main.java
Normal file
@@ -0,0 +1,51 @@
|
||||
package proxy;
|
||||
|
||||
/**
|
||||
* Proxy Design Pattern — Runnable Demo
|
||||
*
|
||||
* Shows two proxy types:
|
||||
* 1. Virtual Proxy (lazy connection)
|
||||
* 2. Logging Proxy (cross-cutting concern)
|
||||
* 3. Proxy chaining (both together)
|
||||
*
|
||||
* Run: javac proxy/*.java && java proxy.Main
|
||||
* Article: https://ankurm.com/proxy-design-pattern-java/
|
||||
*/
|
||||
public class Main {
|
||||
|
||||
public static void main(String[] args) throws InterruptedException {
|
||||
System.out.println("=== Proxy Design Pattern Demo ===\n");
|
||||
|
||||
// --- Virtual Proxy: lazy connection ---
|
||||
System.out.println("-- Virtual Proxy (lazy loading) --");
|
||||
DatabaseConnection lazy = new LazyConnectionProxy("jdbc:postgresql://localhost/mydb");
|
||||
lazy.connect(); // absorbed by proxy, no real connection yet
|
||||
System.out.println("(no real connection yet — saved startup time)");
|
||||
System.out.println("Result: " + lazy.executeQuery("SELECT * FROM users WHERE id=1"));
|
||||
System.out.println("Result: " + lazy.executeQuery("SELECT COUNT(*) FROM orders"));
|
||||
lazy.disconnect();
|
||||
|
||||
System.out.println();
|
||||
|
||||
// --- Logging Proxy: wraps the real connection ---
|
||||
System.out.println("-- Logging Proxy --");
|
||||
DatabaseConnection real = new RealDatabaseConnection("jdbc:mysql://localhost/shopdb");
|
||||
real.connect();
|
||||
DatabaseConnection logged = new LoggingProxy(real);
|
||||
logged.executeQuery("SELECT * FROM products LIMIT 10");
|
||||
logged.disconnect();
|
||||
|
||||
System.out.println();
|
||||
|
||||
// --- Proxy chaining: lazy + logging ---
|
||||
System.out.println("-- Chained Proxies: Lazy + Logging --");
|
||||
DatabaseConnection chain =
|
||||
new LoggingProxy(
|
||||
new LazyConnectionProxy("jdbc:oracle://localhost/warehouse"));
|
||||
chain.connect();
|
||||
chain.executeQuery("SELECT SUM(quantity) FROM inventory");
|
||||
chain.disconnect();
|
||||
|
||||
System.out.println("\n=== Demo complete ===");
|
||||
}
|
||||
}
|
||||
32
02-structural/proxy/README.md
Normal file
32
02-structural/proxy/README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Proxy Design Pattern — Java Example
|
||||
|
||||
**Pattern:** Structural → Proxy
|
||||
**Article:** https://ankurm.com/proxy-design-pattern-java/
|
||||
|
||||
## What this example shows
|
||||
|
||||
A `DatabaseConnection` interface is implemented by a real, expensive connection (`RealDatabaseConnection`) and by two proxies that sit in front of it: `LazyConnectionProxy` defers opening the real connection until the first query actually runs (virtual proxy), and `LoggingProxy` wraps any `DatabaseConnection` — real or proxied — and adds timing/audit logging around every call (cross-cutting concern proxy). Because both proxies implement the same interface as the real subject, they compose: `Main` chains `LoggingProxy` around a `LazyConnectionProxy` to get lazy loading and logging together.
|
||||
|
||||
## How to run
|
||||
|
||||
```bash
|
||||
javac proxy/*.java -d out/proxy
|
||||
java -cp out/proxy proxy.Main
|
||||
```
|
||||
|
||||
Requires Java 25.
|
||||
|
||||
## Post Section ↔ File Mapping
|
||||
|
||||
| Post Section | File(s) |
|
||||
|---|---|
|
||||
| The Subject Interface | `DatabaseConnection.java` |
|
||||
| The Real Subject | `RealDatabaseConnection.java` |
|
||||
| Virtual Proxy: Lazy Initialisation | `LazyConnectionProxy.java` |
|
||||
| Logging Proxy: Cross-Cutting Concerns | `LoggingProxy.java` |
|
||||
| Wiring It Together: Proxy Chaining | `Main.java` |
|
||||
|
||||
Note: the "Dynamic Proxy with java.lang.reflect.Proxy" snippet is illustrative only — it is not part of this repository's runnable example.
|
||||
|
||||
Article: https://ankurm.com/proxy-design-pattern-java/
|
||||
All patterns: https://ankurm.com/design-patterns-java/
|
||||
33
02-structural/proxy/RealDatabaseConnection.java
Normal file
33
02-structural/proxy/RealDatabaseConnection.java
Normal file
@@ -0,0 +1,33 @@
|
||||
package proxy;
|
||||
|
||||
/**
|
||||
* Real Subject — the actual, expensive database connection.
|
||||
* Opening it takes time. We want to delay this until truly needed.
|
||||
*/
|
||||
public class RealDatabaseConnection implements DatabaseConnection {
|
||||
|
||||
private final String url;
|
||||
|
||||
public RealDatabaseConnection(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect() {
|
||||
System.out.println("[Real DB] Connecting to " + url + " (expensive operation)...");
|
||||
// Simulate connection setup time
|
||||
try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
|
||||
System.out.println("[Real DB] Connected.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String executeQuery(String sql) {
|
||||
System.out.println("[Real DB] Executing: " + sql);
|
||||
return "ResultSet{rows=42}"; // simulated result
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnect() {
|
||||
System.out.println("[Real DB] Disconnecting from " + url);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package chain;
|
||||
|
||||
/** Handles CRITICAL tickets — all-hands incident response */
|
||||
public class CriticalIncidentTeam extends SupportHandler {
|
||||
|
||||
@Override
|
||||
protected boolean canHandle(SupportTicket ticket) {
|
||||
return ticket.getPriority() == SupportTicket.Priority.CRITICAL;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handle(SupportTicket ticket) {
|
||||
System.out.println(" [CRITICAL TEAM] All-hands war room opened: " + ticket);
|
||||
}
|
||||
}
|
||||
15
03-behavioral/chain-of-responsibility/Level1Support.java
Normal file
15
03-behavioral/chain-of-responsibility/Level1Support.java
Normal file
@@ -0,0 +1,15 @@
|
||||
package chain;
|
||||
|
||||
/** Handles LOW priority tickets — basic FAQ and documentation responses */
|
||||
public class Level1Support extends SupportHandler {
|
||||
|
||||
@Override
|
||||
protected boolean canHandle(SupportTicket ticket) {
|
||||
return ticket.getPriority() == SupportTicket.Priority.LOW;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handle(SupportTicket ticket) {
|
||||
System.out.println(" [Level-1] Resolved with FAQ: " + ticket);
|
||||
}
|
||||
}
|
||||
15
03-behavioral/chain-of-responsibility/Level2Support.java
Normal file
15
03-behavioral/chain-of-responsibility/Level2Support.java
Normal file
@@ -0,0 +1,15 @@
|
||||
package chain;
|
||||
|
||||
/** Handles MEDIUM priority tickets — technical troubleshooting */
|
||||
public class Level2Support extends SupportHandler {
|
||||
|
||||
@Override
|
||||
protected boolean canHandle(SupportTicket ticket) {
|
||||
return ticket.getPriority() == SupportTicket.Priority.MEDIUM;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handle(SupportTicket ticket) {
|
||||
System.out.println(" [Level-2] Diagnosed and fixed: " + ticket);
|
||||
}
|
||||
}
|
||||
15
03-behavioral/chain-of-responsibility/Level3Support.java
Normal file
15
03-behavioral/chain-of-responsibility/Level3Support.java
Normal file
@@ -0,0 +1,15 @@
|
||||
package chain;
|
||||
|
||||
/** Handles HIGH priority tickets — senior engineers */
|
||||
public class Level3Support extends SupportHandler {
|
||||
|
||||
@Override
|
||||
protected boolean canHandle(SupportTicket ticket) {
|
||||
return ticket.getPriority() == SupportTicket.Priority.HIGH;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handle(SupportTicket ticket) {
|
||||
System.out.println(" [Level-3] Engineering deep-dive completed: " + ticket);
|
||||
}
|
||||
}
|
||||
38
03-behavioral/chain-of-responsibility/Main.java
Normal file
38
03-behavioral/chain-of-responsibility/Main.java
Normal file
@@ -0,0 +1,38 @@
|
||||
package chain;
|
||||
|
||||
/**
|
||||
* Chain of Responsibility Design Pattern — Runnable Demo
|
||||
*
|
||||
* A support ticket routing system where each handler
|
||||
* either resolves a ticket or passes it up the chain.
|
||||
*
|
||||
* Run: javac chain/*.java -d out/chain && java -cp out/chain chain.Main
|
||||
* Article: https://ankurm.com/chain-of-responsibility-design-pattern-java/
|
||||
*/
|
||||
public class Main {
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println("=== Chain of Responsibility Demo ===\n");
|
||||
|
||||
// Build the chain: L1 -> L2 -> L3 -> Critical
|
||||
SupportHandler l1 = new Level1Support();
|
||||
l1.setNext(new Level2Support())
|
||||
.setNext(new Level3Support())
|
||||
.setNext(new CriticalIncidentTeam());
|
||||
|
||||
SupportTicket[] tickets = {
|
||||
new SupportTicket("Can't find the login button", SupportTicket.Priority.LOW),
|
||||
new SupportTicket("API returns 500 on /checkout", SupportTicket.Priority.MEDIUM),
|
||||
new SupportTicket("Database replication lag > 30s", SupportTicket.Priority.HIGH),
|
||||
new SupportTicket("Complete payment system outage", SupportTicket.Priority.CRITICAL),
|
||||
};
|
||||
|
||||
for (SupportTicket t : tickets) {
|
||||
System.out.println("Ticket: " + t);
|
||||
l1.handleRequest(t);
|
||||
System.out.println();
|
||||
}
|
||||
|
||||
System.out.println("=== Demo complete ===");
|
||||
}
|
||||
}
|
||||
31
03-behavioral/chain-of-responsibility/README.md
Normal file
31
03-behavioral/chain-of-responsibility/README.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Chain of Responsibility Design Pattern — Java Example
|
||||
|
||||
**Pattern:** Behavioral → Chain of Responsibility
|
||||
**Article:** https://ankurm.com/chain-of-responsibility-design-pattern-java/
|
||||
|
||||
## What this example shows
|
||||
|
||||
A support-ticket routing system: four handlers (`Level1Support`, `Level2Support`, `Level3Support`, `CriticalIncidentTeam`) are chained together via `SupportHandler.setNext()`. Each handler checks `canHandle()` for the ticket's priority — if it can't handle the ticket, it logs that it's passing the ticket up and delegates to the next handler in the chain. The sender (`Main`) only ever talks to the first handler; it has no idea which handler will ultimately resolve each ticket.
|
||||
|
||||
## How to run
|
||||
|
||||
```bash
|
||||
javac chain-of-responsibility/*.java -d out/chain
|
||||
java -cp out/chain chain.Main
|
||||
```
|
||||
|
||||
Requires Java 25.
|
||||
|
||||
## Post Section ↔ File Mapping
|
||||
|
||||
| Post Section | File(s) |
|
||||
|---|---|
|
||||
| The Problem: Rigid Routing Logic | illustrative only — not part of this repository's runnable example |
|
||||
| Implementation: Support Ticket Escalation — base handler | `SupportHandler.java` |
|
||||
| Implementation: Support Ticket Escalation — the ticket | `SupportTicket.java` |
|
||||
| Each concrete handler checks only its own responsibility — Level 1 & 2 | `Level1Support.java`, `Level2Support.java` |
|
||||
| Level3Support and CriticalIncidentTeam follow exactly the same shape | `Level3Support.java`, `CriticalIncidentTeam.java` |
|
||||
| The chain is assembled in one place | `Main.java` |
|
||||
|
||||
Article: https://ankurm.com/chain-of-responsibility-design-pattern-java/
|
||||
All patterns: https://ankurm.com/design-patterns-java/
|
||||
31
03-behavioral/chain-of-responsibility/SupportHandler.java
Normal file
31
03-behavioral/chain-of-responsibility/SupportHandler.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package chain;
|
||||
|
||||
/**
|
||||
* Handler interface — defines the chain contract.
|
||||
* Each handler knows its next handler and can either
|
||||
* handle the request itself or pass it along.
|
||||
*/
|
||||
public abstract class SupportHandler {
|
||||
|
||||
private SupportHandler next;
|
||||
|
||||
public SupportHandler setNext(SupportHandler next) {
|
||||
this.next = next;
|
||||
return next; // fluent API: h1.setNext(h2).setNext(h3)
|
||||
}
|
||||
|
||||
// Template method: subclasses implement handle(); base manages chaining
|
||||
public final void handleRequest(SupportTicket ticket) {
|
||||
if (canHandle(ticket)) {
|
||||
handle(ticket);
|
||||
} else if (next != null) {
|
||||
System.out.println(" [" + getClass().getSimpleName() + "] passing up...");
|
||||
next.handleRequest(ticket);
|
||||
} else {
|
||||
System.out.println(" [UNHANDLED] No handler for: " + ticket);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract boolean canHandle(SupportTicket ticket);
|
||||
protected abstract void handle(SupportTicket ticket);
|
||||
}
|
||||
21
03-behavioral/chain-of-responsibility/SupportTicket.java
Normal file
21
03-behavioral/chain-of-responsibility/SupportTicket.java
Normal file
@@ -0,0 +1,21 @@
|
||||
package chain;
|
||||
|
||||
public class SupportTicket {
|
||||
|
||||
public enum Priority { LOW, MEDIUM, HIGH, CRITICAL }
|
||||
|
||||
private final String description;
|
||||
private final Priority priority;
|
||||
|
||||
public SupportTicket(String description, Priority priority) {
|
||||
this.description = description;
|
||||
this.priority = priority;
|
||||
}
|
||||
|
||||
public Priority getPriority() { return priority; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "[" + priority + "] " + description;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user