Sync Factory Method, Abstract Factory, Builder, Prototype to Java 25; fix post/code mismatches
This commit is contained in:
@@ -1,21 +1,30 @@
|
||||
package abstractfactory;
|
||||
|
||||
/** Client - uses the factory without knowing concrete classes */
|
||||
public class Application {
|
||||
private final Button button;
|
||||
private final Checkbox checkbox;
|
||||
|
||||
public Application(GUIFactory factory) {
|
||||
button = factory.createButton();
|
||||
checkbox = factory.createCheckbox();
|
||||
// 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() {
|
||||
button.render();
|
||||
checkbox.render();
|
||||
System.out.println("Rendering login screen:");
|
||||
usernameField.render();
|
||||
submitButton.render();
|
||||
}
|
||||
|
||||
public void simulateClick() {
|
||||
button.onClick();
|
||||
public void simulateSubmit() {
|
||||
submitButton.onClick();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package abstractfactory;
|
||||
|
||||
// Every button, regardless of theme, must be able to render and handle clicks
|
||||
public interface Button {
|
||||
void render();
|
||||
void onClick();
|
||||
void render(); // draw on screen
|
||||
void onClick(); // respond to a click event
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
package abstractfactory;
|
||||
|
||||
public interface Checkbox {
|
||||
void render();
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package abstractfactory;
|
||||
|
||||
/** Abstract Factory - creates families of related UI components */
|
||||
public interface GUIFactory {
|
||||
Button createButton();
|
||||
Checkbox createCheckbox();
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package abstractfactory;
|
||||
|
||||
public class MacButton implements Button {
|
||||
@Override public void render() { System.out.println("[Mac] Rendering button with rounded corners"); }
|
||||
@Override public void onClick() { System.out.println("[Mac] Button clicked - glow animation"); }
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package abstractfactory;
|
||||
|
||||
public class MacCheckbox implements Checkbox {
|
||||
@Override public void render() { System.out.println("[Mac] Rendering checkbox with rounded tick box"); }
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package abstractfactory;
|
||||
|
||||
public class MacFactory implements GUIFactory {
|
||||
@Override public Button createButton() { return new MacButton(); }
|
||||
@Override public Checkbox createCheckbox() { return new MacCheckbox(); }
|
||||
}
|
||||
@@ -1,22 +1,16 @@
|
||||
package abstractfactory;
|
||||
|
||||
// Entry point: the ONLY place that knows the theme
|
||||
public class Main {
|
||||
public static void main(String[] args) {
|
||||
System.out.println("=== Abstract Factory Pattern Demo ===\n");
|
||||
|
||||
String os = System.getProperty("os.name", "Windows").toLowerCase();
|
||||
GUIFactory factory = os.contains("mac") ? new MacFactory() : new WindowsFactory();
|
||||
|
||||
System.out.println("Detected OS family: " + (os.contains("mac") ? "Mac" : "Windows"));
|
||||
System.out.println();
|
||||
// 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.simulateClick();
|
||||
|
||||
System.out.println("\n--- Forcing Mac UI ---");
|
||||
Application macApp = new Application(new MacFactory());
|
||||
macApp.render();
|
||||
macApp.simulateClick();
|
||||
app.simulateSubmit();
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package abstractfactory;
|
||||
|
||||
public class WindowsButton implements Button {
|
||||
@Override public void render() { System.out.println("[Windows] Rendering button with square corners"); }
|
||||
@Override public void onClick() { System.out.println("[Windows] Button clicked - raised border effect"); }
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package abstractfactory;
|
||||
|
||||
public class WindowsCheckbox implements Checkbox {
|
||||
@Override public void render() { System.out.println("[Windows] Rendering checkbox with square tick box"); }
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package abstractfactory;
|
||||
|
||||
public class WindowsFactory implements GUIFactory {
|
||||
@Override public Button createButton() { return new WindowsButton(); }
|
||||
@Override public Checkbox createCheckbox() { return new WindowsCheckbox(); }
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package builder;
|
||||
|
||||
/** Director knows how to build common configurations */
|
||||
public class Director {
|
||||
public House buildStarter(House.Builder builder) {
|
||||
return builder.rooms(2).floors(1).garage(false).roofType("gabled").build();
|
||||
}
|
||||
|
||||
public House buildLuxury(House.Builder builder) {
|
||||
return builder.rooms(6).floors(3).garage(true).garden(true).pool(true).roofType("hip").build();
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package builder;
|
||||
|
||||
public class House {
|
||||
private final int rooms;
|
||||
private final int floors;
|
||||
private final boolean hasGarage;
|
||||
private final boolean hasGarden;
|
||||
private final boolean hasPool;
|
||||
private final String roofType;
|
||||
|
||||
private House(Builder builder) {
|
||||
this.rooms = builder.rooms;
|
||||
this.floors = builder.floors;
|
||||
this.hasGarage = builder.hasGarage;
|
||||
this.hasGarden = builder.hasGarden;
|
||||
this.hasPool = builder.hasPool;
|
||||
this.roofType = builder.roofType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "House{rooms=" + rooms + ", floors=" + floors
|
||||
+ ", garage=" + hasGarage + ", garden=" + hasGarden
|
||||
+ ", pool=" + hasPool + ", roof='" + roofType + "'}";
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private int rooms = 1;
|
||||
private int floors = 1;
|
||||
private boolean hasGarage = false;
|
||||
private boolean hasGarden = false;
|
||||
private boolean hasPool = false;
|
||||
private String roofType = "flat";
|
||||
|
||||
public Builder rooms(int rooms) { this.rooms = rooms; return this; }
|
||||
public Builder floors(int floors) { this.floors = floors; return this; }
|
||||
public Builder garage(boolean v) { this.hasGarage = v; return this; }
|
||||
public Builder garden(boolean v) { this.hasGarden = v; return this; }
|
||||
public Builder pool(boolean v) { this.hasPool = v; return this; }
|
||||
public Builder roofType(String roofType) { this.roofType = roofType; return this; }
|
||||
public House build() { return new House(this); }
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,42 @@
|
||||
package builder;
|
||||
|
||||
public class Main {
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println("=== Builder Pattern Demo ===\n");
|
||||
|
||||
Director director = new Director();
|
||||
// Simple GET — only the required field
|
||||
HttpRequest ping = new HttpRequest.Builder()
|
||||
.url("https://api.example.com/health")
|
||||
.build();
|
||||
|
||||
House starter = director.buildStarter(new House.Builder());
|
||||
System.out.println("Starter home : " + starter);
|
||||
System.out.println("Simple GET: " + ping);
|
||||
|
||||
House luxury = director.buildLuxury(new House.Builder());
|
||||
System.out.println("Luxury home : " + luxury);
|
||||
// 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();
|
||||
|
||||
// Client builds a custom house directly without the Director
|
||||
House custom = new House.Builder()
|
||||
.rooms(4).floors(2).garden(true).roofType("mansard").build();
|
||||
System.out.println("Custom home : " + custom);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package factorymethod;
|
||||
|
||||
public class EmailNotification implements Notification {
|
||||
private final String email;
|
||||
public EmailNotification(String email) { this.email = email; }
|
||||
|
||||
@Override
|
||||
public void send(String message) {
|
||||
System.out.println("Email -> " + email + ": " + message);
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package factorymethod;
|
||||
|
||||
public class EmailService extends NotificationService {
|
||||
private final String email;
|
||||
public EmailService(String email) { this.email = email; }
|
||||
|
||||
@Override
|
||||
protected Notification createNotification() {
|
||||
return new EmailNotification(email);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,23 @@
|
||||
package factorymethod;
|
||||
|
||||
public class Main {
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println("=== Factory Method Pattern Demo ===\n");
|
||||
|
||||
NotificationService[] services = {
|
||||
new EmailService("alice@example.com"),
|
||||
new SmsService("+1-555-0123"),
|
||||
new PushService("device-token-abc123")
|
||||
};
|
||||
// The client only knows about NotificationSender (the Creator)
|
||||
NotificationSender sender;
|
||||
|
||||
for (NotificationService service : services) {
|
||||
service.notifyUser("Your order #1042 has been shipped!");
|
||||
}
|
||||
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%");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
package factorymethod;
|
||||
|
||||
public interface Notification {
|
||||
void send(String message);
|
||||
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,24 @@
|
||||
package factorymethod;
|
||||
|
||||
/** Creator - declares the factory method */
|
||||
public abstract class NotificationService {
|
||||
protected abstract Notification createNotification();
|
||||
public class NotificationService {
|
||||
|
||||
public void notifyUser(String message) {
|
||||
Notification n = createNotification();
|
||||
n.send(message);
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package factorymethod;
|
||||
|
||||
public class PushNotification implements Notification {
|
||||
private final String deviceToken;
|
||||
public PushNotification(String deviceToken) { this.deviceToken = deviceToken; }
|
||||
|
||||
@Override
|
||||
public void send(String message) {
|
||||
System.out.println("Push -> " + deviceToken + ": " + message);
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package factorymethod;
|
||||
|
||||
public class PushService extends NotificationService {
|
||||
private final String token;
|
||||
public PushService(String token) { this.token = token; }
|
||||
|
||||
@Override
|
||||
protected Notification createNotification() {
|
||||
return new PushNotification(token);
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
package factorymethod;
|
||||
|
||||
public class SmsNotification implements Notification {
|
||||
private final String phone;
|
||||
public SmsNotification(String phone) { this.phone = phone; }
|
||||
|
||||
@Override
|
||||
public void send(String message) {
|
||||
System.out.println("SMS -> " + phone + ": " + message);
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package factorymethod;
|
||||
|
||||
public class SmsService extends NotificationService {
|
||||
private final String phone;
|
||||
public SmsService(String phone) { this.phone = phone; }
|
||||
|
||||
@Override
|
||||
protected Notification createNotification() {
|
||||
return new SmsNotification(phone);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package prototype;
|
||||
|
||||
public class Circle extends Shape {
|
||||
private int radius;
|
||||
|
||||
public Circle(int radius, String color) {
|
||||
this.radius = radius;
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
/** Copy constructor used by clone() */
|
||||
private Circle(Circle source) {
|
||||
super(source);
|
||||
this.radius = source.radius;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Circle clone() { return new Circle(this); }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Circle{color=" + color + ", radius=" + radius + ", pos=(" + x + "," + y + ")}";
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1,47 @@
|
||||
package prototype;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
public class Main {
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println("=== Prototype Pattern Demo ===\n");
|
||||
|
||||
List<Shape> originals = new ArrayList<>();
|
||||
originals.add(new Circle(10, "red"));
|
||||
originals.add(new Rectangle(20, 30, "blue"));
|
||||
// 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
|
||||
);
|
||||
|
||||
// Clone each shape - no need to know the concrete type
|
||||
List<Shape> copies = new ArrayList<>();
|
||||
for (Shape shape : originals) {
|
||||
copies.add(shape.clone());
|
||||
}
|
||||
System.out.println("Original: " + original);
|
||||
|
||||
// Mutate copies - originals are unaffected
|
||||
copies.get(0).setColor("green");
|
||||
copies.get(0).move(5, 5);
|
||||
copies.get(1).setColor("yellow");
|
||||
// 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("--- Originals ---");
|
||||
originals.forEach(System.out::println);
|
||||
System.out.println("Draft: " + draft);
|
||||
System.out.println("Original: " + original); // unchanged — deep copy worked
|
||||
|
||||
System.out.println("\n--- Clones (mutated) ---");
|
||||
copies.forEach(System.out::println);
|
||||
// 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
|
||||
|
||||
System.out.println("\nSame instance? " + (originals.get(0) == copies.get(0)));
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
package prototype;
|
||||
|
||||
public class Rectangle extends Shape {
|
||||
private int width;
|
||||
private int height;
|
||||
|
||||
public Rectangle(int width, int height, String color) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
private Rectangle(Rectangle source) {
|
||||
super(source);
|
||||
this.width = source.width;
|
||||
this.height = source.height;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Rectangle clone() { return new Rectangle(this); }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Rectangle{color=" + color + ", size=" + width + "x" + height + ", pos=(" + x + "," + y + ")}";
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package prototype;
|
||||
|
||||
/** Prototype - every shape can clone itself */
|
||||
public abstract class Shape {
|
||||
protected String color;
|
||||
protected int x;
|
||||
protected int y;
|
||||
|
||||
protected Shape(Shape source) {
|
||||
this.color = source.color;
|
||||
this.x = source.x;
|
||||
this.y = source.y;
|
||||
}
|
||||
|
||||
protected Shape() {}
|
||||
|
||||
public abstract Shape clone();
|
||||
|
||||
public void setColor(String color) { this.color = color; }
|
||||
public String getColor() { return color; }
|
||||
public void move(int x, int y) { this.x = x; this.y = y; }
|
||||
}
|
||||
19
01-creational/singleton/AppConfig.java
Normal file
19
01-creational/singleton/AppConfig.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package singleton;
|
||||
public class AppConfig {
|
||||
|
||||
private AppConfig() {
|
||||
System.out.println("Reading configuration from file...");
|
||||
}
|
||||
|
||||
private static class Holder {
|
||||
static final AppConfig INSTANCE = new AppConfig();
|
||||
}
|
||||
|
||||
public static AppConfig getInstance() {
|
||||
return Holder.INSTANCE;
|
||||
}
|
||||
|
||||
public String getProperty(String key) {
|
||||
return System.getProperty(key, "(not set)");
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package singleton;
|
||||
|
||||
/**
|
||||
* Thread-safe Singleton using double-checked locking.
|
||||
*/
|
||||
public class DatabaseConnection {
|
||||
private static volatile DatabaseConnection instance;
|
||||
private final String url;
|
||||
private int queryCount = 0;
|
||||
|
||||
private DatabaseConnection() {
|
||||
this.url = "jdbc:mysql://localhost:3306/myapp";
|
||||
System.out.println("Establishing connection to " + url);
|
||||
}
|
||||
|
||||
public static DatabaseConnection getInstance() {
|
||||
if (instance == null) {
|
||||
synchronized (DatabaseConnection.class) {
|
||||
if (instance == null) {
|
||||
instance = new DatabaseConnection();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public String executeQuery(String sql) {
|
||||
queryCount++;
|
||||
return "Result #" + queryCount + " for: " + sql;
|
||||
}
|
||||
|
||||
public int getQueryCount() { return queryCount; }
|
||||
public String getUrl() { return url; }
|
||||
}
|
||||
@@ -1,19 +1,9 @@
|
||||
package singleton;
|
||||
|
||||
public class Main {
|
||||
public static void main(String[] args) {
|
||||
System.out.println("=== Singleton Pattern Demo ===\n");
|
||||
|
||||
DatabaseConnection c1 = DatabaseConnection.getInstance();
|
||||
DatabaseConnection c2 = DatabaseConnection.getInstance();
|
||||
DatabaseConnection c3 = DatabaseConnection.getInstance();
|
||||
|
||||
System.out.println("\nAll three references same instance? " + (c1 == c2 && c2 == c3));
|
||||
|
||||
System.out.println(c1.executeQuery("SELECT * FROM users"));
|
||||
System.out.println(c2.executeQuery("SELECT * FROM orders"));
|
||||
System.out.println(c3.executeQuery("SELECT COUNT(*) FROM products"));
|
||||
|
||||
System.out.println("\nTotal queries: " + c1.getQueryCount());
|
||||
AppConfig config1 = AppConfig.getInstance();
|
||||
AppConfig config2 = AppConfig.getInstance();
|
||||
System.out.println("Same instance? " + (config1 == config2)); // true
|
||||
System.out.println("db.url = " + config1.getProperty("db.url"));
|
||||
}
|
||||
}
|
||||
|
||||
79
README.md
79
README.md
@@ -1,6 +1,6 @@
|
||||
# GoF Design Patterns in Java
|
||||
|
||||
Complete runnable Java 17 implementations of all 23 Gang of Four design patterns.
|
||||
Complete runnable Java 25 implementations of all 23 Gang of Four design patterns.
|
||||
Each pattern has a dedicated article at [ankurm.com](https://ankurm.com/gof-design-patterns-java/)
|
||||
with a UML diagram, step-by-step explanation, and console output.
|
||||
|
||||
@@ -38,7 +38,7 @@ design-patterns/
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Java 17+ (Eclipse Temurin recommended)
|
||||
- Java 25+ (Eclipse Temurin recommended)
|
||||
- No build tool required — plain `javac` / `java`
|
||||
|
||||
## Run any pattern
|
||||
@@ -83,6 +83,81 @@ java -cp out\strategy strategy.Main
|
||||
| Template Method | Behavioral | https://ankurm.com/template-method-design-pattern-java/ |
|
||||
| Visitor | Behavioral | https://ankurm.com/visitor-design-pattern-java/ |
|
||||
|
||||
## Post ↔ Code Mapping
|
||||
|
||||
Each article is split into multiple runnable parts. This section maps every post section to the exact file(s) in this repository, so you can jump straight from "what I just read" to "the file that implements it." Updated pattern-by-pattern as each article is verified against Java 25.
|
||||
|
||||
### Factory Method (`01-creational/factory-method/`)
|
||||
|
||||
| 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` |
|
||||
|
||||
Run it:
|
||||
```bash
|
||||
javac 01-creational/factory-method/*.java -d out/factory-method
|
||||
java -cp out/factory-method factorymethod.Main
|
||||
```
|
||||
|
||||
### Abstract Factory (`01-creational/abstract-factory/`)
|
||||
|
||||
| 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` |
|
||||
|
||||
Run it:
|
||||
```bash
|
||||
javac 01-creational/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
|
||||
```
|
||||
|
||||
### Builder (`01-creational/builder/`)
|
||||
|
||||
| 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.
|
||||
|
||||
Run it:
|
||||
```bash
|
||||
javac 01-creational/builder/*.java -d out/builder
|
||||
java -cp out/builder builder.Main
|
||||
```
|
||||
|
||||
### Prototype (`01-creational/prototype/`)
|
||||
|
||||
| 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.
|
||||
|
||||
Run it:
|
||||
```bash
|
||||
javac 01-creational/prototype/*.java -d out/prototype
|
||||
java -cp out/prototype prototype.Main
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
- *Design Patterns: Elements of Reusable Object-Oriented Software* — Gamma, Helm, Johnson, Vlissides
|
||||
|
||||
Reference in New Issue
Block a user