Add all 23 GoF design pattern implementations
This commit is contained in:
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/
|
||||
Reference in New Issue
Block a user