diff --git a/01-creational/abstract-factory/Application.java b/01-creational/abstract-factory/Application.java index 9fb136f..a1c76a9 100644 --- a/01-creational/abstract-factory/Application.java +++ b/01-creational/abstract-factory/Application.java @@ -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(); } } diff --git a/01-creational/abstract-factory/Button.java b/01-creational/abstract-factory/Button.java index 4b7220e..a88f44c 100644 --- a/01-creational/abstract-factory/Button.java +++ b/01-creational/abstract-factory/Button.java @@ -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 } diff --git a/01-creational/abstract-factory/Checkbox.java b/01-creational/abstract-factory/Checkbox.java deleted file mode 100644 index 4dc26fc..0000000 --- a/01-creational/abstract-factory/Checkbox.java +++ /dev/null @@ -1,5 +0,0 @@ -package abstractfactory; - -public interface Checkbox { - void render(); -} diff --git a/01-creational/abstract-factory/DarkButton.java b/01-creational/abstract-factory/DarkButton.java new file mode 100644 index 0000000..3ae4118 --- /dev/null +++ b/01-creational/abstract-factory/DarkButton.java @@ -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"); + } +} diff --git a/01-creational/abstract-factory/DarkTextField.java b/01-creational/abstract-factory/DarkTextField.java new file mode 100644 index 0000000..9ced93d --- /dev/null +++ b/01-creational/abstract-factory/DarkTextField.java @@ -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; } +} diff --git a/01-creational/abstract-factory/DarkThemeFactory.java b/01-creational/abstract-factory/DarkThemeFactory.java new file mode 100644 index 0000000..5fcb488 --- /dev/null +++ b/01-creational/abstract-factory/DarkThemeFactory.java @@ -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(); + } +} diff --git a/01-creational/abstract-factory/GUIFactory.java b/01-creational/abstract-factory/GUIFactory.java deleted file mode 100644 index 96b8f58..0000000 --- a/01-creational/abstract-factory/GUIFactory.java +++ /dev/null @@ -1,7 +0,0 @@ -package abstractfactory; - -/** Abstract Factory - creates families of related UI components */ -public interface GUIFactory { - Button createButton(); - Checkbox createCheckbox(); -} diff --git a/01-creational/abstract-factory/LightButton.java b/01-creational/abstract-factory/LightButton.java new file mode 100644 index 0000000..d1e1e4f --- /dev/null +++ b/01-creational/abstract-factory/LightButton.java @@ -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"); + } +} diff --git a/01-creational/abstract-factory/LightTextField.java b/01-creational/abstract-factory/LightTextField.java new file mode 100644 index 0000000..acf6440 --- /dev/null +++ b/01-creational/abstract-factory/LightTextField.java @@ -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; } +} diff --git a/01-creational/abstract-factory/LightThemeFactory.java b/01-creational/abstract-factory/LightThemeFactory.java new file mode 100644 index 0000000..381f7f9 --- /dev/null +++ b/01-creational/abstract-factory/LightThemeFactory.java @@ -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(); + } +} diff --git a/01-creational/abstract-factory/MacButton.java b/01-creational/abstract-factory/MacButton.java deleted file mode 100644 index e814831..0000000 --- a/01-creational/abstract-factory/MacButton.java +++ /dev/null @@ -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"); } -} diff --git a/01-creational/abstract-factory/MacCheckbox.java b/01-creational/abstract-factory/MacCheckbox.java deleted file mode 100644 index 1f131a4..0000000 --- a/01-creational/abstract-factory/MacCheckbox.java +++ /dev/null @@ -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"); } -} diff --git a/01-creational/abstract-factory/MacFactory.java b/01-creational/abstract-factory/MacFactory.java deleted file mode 100644 index 0a027c1..0000000 --- a/01-creational/abstract-factory/MacFactory.java +++ /dev/null @@ -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(); } -} diff --git a/01-creational/abstract-factory/Main.java b/01-creational/abstract-factory/Main.java index 60981b9..24e3f9d 100644 --- a/01-creational/abstract-factory/Main.java +++ b/01-creational/abstract-factory/Main.java @@ -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(); } } diff --git a/01-creational/abstract-factory/TextField.java b/01-creational/abstract-factory/TextField.java new file mode 100644 index 0000000..fdda80e --- /dev/null +++ b/01-creational/abstract-factory/TextField.java @@ -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(); +} diff --git a/01-creational/abstract-factory/UIFactory.java b/01-creational/abstract-factory/UIFactory.java new file mode 100644 index 0000000..7778b64 --- /dev/null +++ b/01-creational/abstract-factory/UIFactory.java @@ -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. +} diff --git a/01-creational/abstract-factory/WindowsButton.java b/01-creational/abstract-factory/WindowsButton.java deleted file mode 100644 index c93ad4b..0000000 --- a/01-creational/abstract-factory/WindowsButton.java +++ /dev/null @@ -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"); } -} diff --git a/01-creational/abstract-factory/WindowsCheckbox.java b/01-creational/abstract-factory/WindowsCheckbox.java deleted file mode 100644 index 91fb697..0000000 --- a/01-creational/abstract-factory/WindowsCheckbox.java +++ /dev/null @@ -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"); } -} diff --git a/01-creational/abstract-factory/WindowsFactory.java b/01-creational/abstract-factory/WindowsFactory.java deleted file mode 100644 index 3f3c63b..0000000 --- a/01-creational/abstract-factory/WindowsFactory.java +++ /dev/null @@ -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(); } -} diff --git a/01-creational/builder/Director.java b/01-creational/builder/Director.java deleted file mode 100644 index 642f7e4..0000000 --- a/01-creational/builder/Director.java +++ /dev/null @@ -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(); - } -} diff --git a/01-creational/builder/House.java b/01-creational/builder/House.java deleted file mode 100644 index 9d615b5..0000000 --- a/01-creational/builder/House.java +++ /dev/null @@ -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); } - } -} diff --git a/01-creational/builder/HttpClientConfig.java b/01-creational/builder/HttpClientConfig.java new file mode 100644 index 0000000..7508513 --- /dev/null +++ b/01-creational/builder/HttpClientConfig.java @@ -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(); + } +} diff --git a/01-creational/builder/HttpRequest.java b/01-creational/builder/HttpRequest.java new file mode 100644 index 0000000..bae610b --- /dev/null +++ b/01-creational/builder/HttpRequest.java @@ -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 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 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 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 + } + } +} diff --git a/01-creational/builder/Main.java b/01-creational/builder/Main.java index 02a641b..f8dc88f 100644 --- a/01-creational/builder/Main.java +++ b/01-creational/builder/Main.java @@ -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); } } diff --git a/01-creational/factory-method/EmailNotification.java b/01-creational/factory-method/EmailNotification.java index 68bc54c..47d5736 100644 --- a/01-creational/factory-method/EmailNotification.java +++ b/01-creational/factory-method/EmailNotification.java @@ -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"; } } diff --git a/01-creational/factory-method/EmailSender.java b/01-creational/factory-method/EmailSender.java new file mode 100644 index 0000000..3979dbc --- /dev/null +++ b/01-creational/factory-method/EmailSender.java @@ -0,0 +1,8 @@ +package factorymethod; + +public class EmailSender extends NotificationSender { + @Override + protected Notification createNotification() { + return new EmailNotification(); + } +} diff --git a/01-creational/factory-method/EmailService.java b/01-creational/factory-method/EmailService.java deleted file mode 100644 index b193545..0000000 --- a/01-creational/factory-method/EmailService.java +++ /dev/null @@ -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); - } -} diff --git a/01-creational/factory-method/Main.java b/01-creational/factory-method/Main.java index 11f9084..8f3856a 100644 --- a/01-creational/factory-method/Main.java +++ b/01-creational/factory-method/Main.java @@ -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%"); } } diff --git a/01-creational/factory-method/Notification.java b/01-creational/factory-method/Notification.java index b09e01f..bcec43c 100644 --- a/01-creational/factory-method/Notification.java +++ b/01-creational/factory-method/Notification.java @@ -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(); } diff --git a/01-creational/factory-method/NotificationSender.java b/01-creational/factory-method/NotificationSender.java new file mode 100644 index 0000000..a55ad5e --- /dev/null +++ b/01-creational/factory-method/NotificationSender.java @@ -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"); + } +} diff --git a/01-creational/factory-method/NotificationService.java b/01-creational/factory-method/NotificationService.java index dc51828..3e1c26b 100644 --- a/01-creational/factory-method/NotificationService.java +++ b/01-creational/factory-method/NotificationService.java @@ -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."); } } diff --git a/01-creational/factory-method/PushNotification.java b/01-creational/factory-method/PushNotification.java index 8b4c3b8..f6307a1 100644 --- a/01-creational/factory-method/PushNotification.java +++ b/01-creational/factory-method/PushNotification.java @@ -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"; } } diff --git a/01-creational/factory-method/PushSender.java b/01-creational/factory-method/PushSender.java new file mode 100644 index 0000000..4abd627 --- /dev/null +++ b/01-creational/factory-method/PushSender.java @@ -0,0 +1,8 @@ +package factorymethod; + +public class PushSender extends NotificationSender { + @Override + protected Notification createNotification() { + return new PushNotification(); + } +} diff --git a/01-creational/factory-method/PushService.java b/01-creational/factory-method/PushService.java deleted file mode 100644 index 1809b9c..0000000 --- a/01-creational/factory-method/PushService.java +++ /dev/null @@ -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); - } -} diff --git a/01-creational/factory-method/SlackNotification.java b/01-creational/factory-method/SlackNotification.java new file mode 100644 index 0000000..e32c73b --- /dev/null +++ b/01-creational/factory-method/SlackNotification.java @@ -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"; } +} diff --git a/01-creational/factory-method/SlackSender.java b/01-creational/factory-method/SlackSender.java new file mode 100644 index 0000000..3104b94 --- /dev/null +++ b/01-creational/factory-method/SlackSender.java @@ -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(); + } +} diff --git a/01-creational/factory-method/SmsNotification.java b/01-creational/factory-method/SmsNotification.java index f5d1dd6..ea8e25a 100644 --- a/01-creational/factory-method/SmsNotification.java +++ b/01-creational/factory-method/SmsNotification.java @@ -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"; } } diff --git a/01-creational/factory-method/SmsSender.java b/01-creational/factory-method/SmsSender.java new file mode 100644 index 0000000..f0657c4 --- /dev/null +++ b/01-creational/factory-method/SmsSender.java @@ -0,0 +1,8 @@ +package factorymethod; + +public class SmsSender extends NotificationSender { + @Override + protected Notification createNotification() { + return new SmsNotification(); + } +} diff --git a/01-creational/factory-method/SmsService.java b/01-creational/factory-method/SmsService.java deleted file mode 100644 index aab7156..0000000 --- a/01-creational/factory-method/SmsService.java +++ /dev/null @@ -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); - } -} diff --git a/01-creational/prototype/Circle.java b/01-creational/prototype/Circle.java deleted file mode 100644 index 16d3c90..0000000 --- a/01-creational/prototype/Circle.java +++ /dev/null @@ -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 + ")}"; - } -} diff --git a/01-creational/prototype/Copyable.java b/01-creational/prototype/Copyable.java new file mode 100644 index 0000000..f541059 --- /dev/null +++ b/01-creational/prototype/Copyable.java @@ -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 copy(); +} diff --git a/01-creational/prototype/Document.java b/01-creational/prototype/Document.java new file mode 100644 index 0000000..691979b --- /dev/null +++ b/01-creational/prototype/Document.java @@ -0,0 +1,46 @@ +package prototype; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class Document implements Copyable { + + private String title; // String: immutable, safe to share on copy + private String content; // String: immutable, safe to share on copy + private List tags; // List: mutable, MUST be deep-copied + private DocumentMetadata metadata; // Mutable object, MUST be deep-copied + + public Document(String title, String content, List 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 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); + } +} diff --git a/01-creational/prototype/DocumentMetadata.java b/01-creational/prototype/DocumentMetadata.java new file mode 100644 index 0000000..366a8ea --- /dev/null +++ b/01-creational/prototype/DocumentMetadata.java @@ -0,0 +1,39 @@ +package prototype; + +import java.time.LocalDate; + +public class DocumentMetadata implements Copyable { + + 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); + } +} diff --git a/01-creational/prototype/DocumentRegistry.java b/01-creational/prototype/DocumentRegistry.java new file mode 100644 index 0000000..b9f4b7d --- /dev/null +++ b/01-creational/prototype/DocumentRegistry.java @@ -0,0 +1,21 @@ +package prototype; + +import java.util.HashMap; +import java.util.Map; + +public class DocumentRegistry { + + private final Map 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 + } +} diff --git a/01-creational/prototype/Main.java b/01-creational/prototype/Main.java index 8743e33..bbcfebd 100644 --- a/01-creational/prototype/Main.java +++ b/01-creational/prototype/Main.java @@ -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 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 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); } } diff --git a/01-creational/prototype/Rectangle.java b/01-creational/prototype/Rectangle.java deleted file mode 100644 index 3951100..0000000 --- a/01-creational/prototype/Rectangle.java +++ /dev/null @@ -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 + ")}"; - } -} diff --git a/01-creational/prototype/Shape.java b/01-creational/prototype/Shape.java deleted file mode 100644 index 0ee3b07..0000000 --- a/01-creational/prototype/Shape.java +++ /dev/null @@ -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; } -} diff --git a/01-creational/singleton/AppConfig.java b/01-creational/singleton/AppConfig.java new file mode 100644 index 0000000..db2f02d --- /dev/null +++ b/01-creational/singleton/AppConfig.java @@ -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)"); + } +} diff --git a/01-creational/singleton/DatabaseConnection.java b/01-creational/singleton/DatabaseConnection.java deleted file mode 100644 index 5da70fc..0000000 --- a/01-creational/singleton/DatabaseConnection.java +++ /dev/null @@ -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; } -} diff --git a/01-creational/singleton/Main.java b/01-creational/singleton/Main.java index a45d237..58a676f 100644 --- a/01-creational/singleton/Main.java +++ b/01-creational/singleton/Main.java @@ -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")); } } diff --git a/README.md b/README.md index 9c6eed8..04de40a 100644 --- a/README.md +++ b/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