From f5688a6b32abfb7ad044b2841a1c4547ac4d95b2 Mon Sep 17 00:00:00 2001 From: Ankur Date: Sat, 25 Jul 2026 10:50:29 +0530 Subject: [PATCH] Add all 23 GoF design pattern implementations --- .gitignore | 4 + .../abstract-factory/Application.java | 30 ++ 01-creational/abstract-factory/Button.java | 7 + .../abstract-factory/DarkButton.java | 11 + .../abstract-factory/DarkTextField.java | 10 + .../abstract-factory/DarkThemeFactory.java | 14 + .../abstract-factory/LightButton.java | 11 + .../abstract-factory/LightTextField.java | 10 + .../abstract-factory/LightThemeFactory.java | 14 + 01-creational/abstract-factory/Main.java | 16 + 01-creational/abstract-factory/README.md | 33 ++ 01-creational/abstract-factory/TextField.java | 7 + 01-creational/abstract-factory/UIFactory.java | 13 + 01-creational/builder/HttpClientConfig.java | 27 + 01-creational/builder/HttpRequest.java | 76 +++ 01-creational/builder/Main.java | 42 ++ 01-creational/builder/README.md | 30 ++ .../factory-method/EmailNotification.java | 13 + 01-creational/factory-method/EmailSender.java | 8 + 01-creational/factory-method/Main.java | 23 + .../factory-method/Notification.java | 11 + .../factory-method/NotificationSender.java | 31 ++ .../factory-method/NotificationService.java | 24 + .../factory-method/PushNotification.java | 13 + 01-creational/factory-method/PushSender.java | 8 + 01-creational/factory-method/README.md | 32 ++ .../factory-method/SlackNotification.java | 11 + 01-creational/factory-method/SlackSender.java | 9 + .../factory-method/SmsNotification.java | 13 + 01-creational/factory-method/SmsSender.java | 8 + 01-creational/prototype/Copyable.java | 8 + 01-creational/prototype/Document.java | 46 ++ 01-creational/prototype/DocumentMetadata.java | 39 ++ 01-creational/prototype/DocumentRegistry.java | 21 + 01-creational/prototype/Main.java | 47 ++ 01-creational/prototype/README.md | 32 ++ 01-creational/singleton/AppConfigDCL.java | 31 ++ 01-creational/singleton/AppConfigEager.java | 24 + 01-creational/singleton/AppConfigEnum.java | 19 + 01-creational/singleton/AppConfigHolder.java | 19 + .../singleton/AppConfigNaiveLazy.java | 16 + .../singleton/AppConfigSynchronized.java | 16 + 01-creational/singleton/Main.java | 36 ++ 01-creational/singleton/README.md | 34 ++ 02-structural/adapter/Main.java | 27 + 02-structural/adapter/OrderService.java | 23 + 02-structural/adapter/PaymentGateway.java | 10 + 02-structural/adapter/README.md | 36 ++ 02-structural/adapter/StripeClient.java | 39 ++ .../adapter/StripePaymentAdapter.java | 36 ++ .../adapter/StripePaymentClassAdapter.java | 23 + 02-structural/bridge/AdvancedRemote.java | 21 + 02-structural/bridge/Device.java | 16 + 02-structural/bridge/Main.java | 29 + 02-structural/bridge/README.md | 32 ++ 02-structural/bridge/Radio.java | 18 + 02-structural/bridge/RemoteControl.java | 27 + 02-structural/bridge/TV.java | 18 + 02-structural/composite/Directory.java | 39 ++ 02-structural/composite/File.java | 20 + 02-structural/composite/FileSystemItem.java | 10 + 02-structural/composite/Main.java | 36 ++ 02-structural/composite/README.md | 30 ++ 02-structural/decorator/Main.java | 42 ++ .../decorator/PlainTextProcessor.java | 12 + .../decorator/ProfanityFilterDecorator.java | 14 + 02-structural/decorator/README.md | 32 ++ 02-structural/decorator/TextDecorator.java | 22 + 02-structural/decorator/TextProcessor.java | 9 + 02-structural/decorator/TrimDecorator.java | 9 + .../decorator/UpperCaseDecorator.java | 13 + 02-structural/facade/AudioMixer.java | 9 + 02-structural/facade/BitrateReader.java | 15 + 02-structural/facade/Codec.java | 6 + 02-structural/facade/CodecFactory.java | 10 + .../facade/MPEG4CompressionCodec.java | 6 + 02-structural/facade/Main.java | 30 ++ 02-structural/facade/OggCompressionCodec.java | 6 + 02-structural/facade/README.md | 35 ++ .../facade/VideoConversionFacade.java | 39 ++ 02-structural/facade/VideoFile.java | 23 + 02-structural/flyweight/Main.java | 50 ++ 02-structural/flyweight/README.md | 31 ++ 02-structural/flyweight/Tree.java | 31 ++ 02-structural/flyweight/TreeFactory.java | 29 + 02-structural/flyweight/TreeType.java | 29 + 02-structural/proxy/DatabaseConnection.java | 11 + 02-structural/proxy/LazyConnectionProxy.java | 52 ++ 02-structural/proxy/LoggingProxy.java | 41 ++ 02-structural/proxy/Main.java | 51 ++ 02-structural/proxy/README.md | 32 ++ .../proxy/RealDatabaseConnection.java | 33 ++ .../CriticalIncidentTeam.java | 15 + .../Level1Support.java | 15 + .../Level2Support.java | 15 + .../Level3Support.java | 15 + .../chain-of-responsibility/Main.java | 38 ++ .../chain-of-responsibility/README.md | 31 ++ .../SupportHandler.java | 31 ++ .../SupportTicket.java | 21 + 03-behavioral/command/Command.java | 8 + 03-behavioral/command/CommandHistory.java | 28 + 03-behavioral/command/DeleteCommand.java | 23 + 03-behavioral/command/InsertCommand.java | 17 + 03-behavioral/command/Main.java | 40 ++ 03-behavioral/command/README.md | 32 ++ 03-behavioral/command/TextEditor.java | 21 + 03-behavioral/interpreter/AddExpression.java | 19 + 03-behavioral/interpreter/Expression.java | 9 + 03-behavioral/interpreter/Main.java | 38 ++ .../interpreter/MultiplyExpression.java | 19 + .../interpreter/NumberExpression.java | 18 + 03-behavioral/interpreter/README.md | 31 ++ .../interpreter/SubtractExpression.java | 19 + 03-behavioral/iterator/Book.java | 14 + 03-behavioral/iterator/BookIterator.java | 7 + 03-behavioral/iterator/BookShelf.java | 57 ++ 03-behavioral/iterator/Main.java | 37 ++ 03-behavioral/iterator/README.md | 31 ++ 03-behavioral/mediator/ChatMediator.java | 7 + 03-behavioral/mediator/ChatRoom.java | 27 + 03-behavioral/mediator/Main.java | 35 ++ 03-behavioral/mediator/README.md | 31 ++ 03-behavioral/mediator/User.java | 26 + 03-behavioral/memento/Editor.java | 43 ++ 03-behavioral/memento/EditorMemento.java | 28 + 03-behavioral/memento/History.java | 24 + 03-behavioral/memento/Main.java | 38 ++ 03-behavioral/memento/README.md | 31 ++ 03-behavioral/observer/AlertObserver.java | 22 + 03-behavioral/observer/Main.java | 36 ++ 03-behavioral/observer/PortfolioObserver.java | 19 + 03-behavioral/observer/README.md | 32 ++ 03-behavioral/observer/StockMarket.java | 36 ++ 03-behavioral/observer/StockObserver.java | 6 + 03-behavioral/state/GreenState.java | 11 + 03-behavioral/state/Main.java | 26 + 03-behavioral/state/README.md | 33 ++ 03-behavioral/state/RedState.java | 11 + 03-behavioral/state/TrafficLight.java | 21 + 03-behavioral/state/TrafficLightState.java | 8 + 03-behavioral/state/YellowState.java | 11 + 03-behavioral/strategy/BubbleSort.java | 12 + 03-behavioral/strategy/Main.java | 37 ++ 03-behavioral/strategy/MergeSort.java | 30 ++ 03-behavioral/strategy/QuickSort.java | 26 + 03-behavioral/strategy/README.md | 33 ++ 03-behavioral/strategy/SortStrategy.java | 7 + 03-behavioral/strategy/Sorter.java | 24 + .../template-method/ApiMigration.java | 31 ++ .../template-method/CsvMigration.java | 27 + .../template-method/DataMigration.java | 50 ++ 03-behavioral/template-method/Main.java | 24 + 03-behavioral/template-method/README.md | 29 + 03-behavioral/visitor/AreaCalculator.java | 26 + 03-behavioral/visitor/Circle.java | 9 + 03-behavioral/visitor/Main.java | 38 ++ .../visitor/PerimeterCalculator.java | 28 + 03-behavioral/visitor/README.md | 33 ++ 03-behavioral/visitor/Rectangle.java | 10 + 03-behavioral/visitor/Shape.java | 7 + 03-behavioral/visitor/ShapeVisitor.java | 8 + 03-behavioral/visitor/Triangle.java | 10 + README.md | 502 ++++++++++++++++++ 164 files changed, 4371 insertions(+) create mode 100644 .gitignore create mode 100644 01-creational/abstract-factory/Application.java create mode 100644 01-creational/abstract-factory/Button.java create mode 100644 01-creational/abstract-factory/DarkButton.java create mode 100644 01-creational/abstract-factory/DarkTextField.java create mode 100644 01-creational/abstract-factory/DarkThemeFactory.java create mode 100644 01-creational/abstract-factory/LightButton.java create mode 100644 01-creational/abstract-factory/LightTextField.java create mode 100644 01-creational/abstract-factory/LightThemeFactory.java create mode 100644 01-creational/abstract-factory/Main.java create mode 100644 01-creational/abstract-factory/README.md create mode 100644 01-creational/abstract-factory/TextField.java create mode 100644 01-creational/abstract-factory/UIFactory.java create mode 100644 01-creational/builder/HttpClientConfig.java create mode 100644 01-creational/builder/HttpRequest.java create mode 100644 01-creational/builder/Main.java create mode 100644 01-creational/builder/README.md create mode 100644 01-creational/factory-method/EmailNotification.java create mode 100644 01-creational/factory-method/EmailSender.java create mode 100644 01-creational/factory-method/Main.java create mode 100644 01-creational/factory-method/Notification.java create mode 100644 01-creational/factory-method/NotificationSender.java create mode 100644 01-creational/factory-method/NotificationService.java create mode 100644 01-creational/factory-method/PushNotification.java create mode 100644 01-creational/factory-method/PushSender.java create mode 100644 01-creational/factory-method/README.md create mode 100644 01-creational/factory-method/SlackNotification.java create mode 100644 01-creational/factory-method/SlackSender.java create mode 100644 01-creational/factory-method/SmsNotification.java create mode 100644 01-creational/factory-method/SmsSender.java create mode 100644 01-creational/prototype/Copyable.java create mode 100644 01-creational/prototype/Document.java create mode 100644 01-creational/prototype/DocumentMetadata.java create mode 100644 01-creational/prototype/DocumentRegistry.java create mode 100644 01-creational/prototype/Main.java create mode 100644 01-creational/prototype/README.md create mode 100644 01-creational/singleton/AppConfigDCL.java create mode 100644 01-creational/singleton/AppConfigEager.java create mode 100644 01-creational/singleton/AppConfigEnum.java create mode 100644 01-creational/singleton/AppConfigHolder.java create mode 100644 01-creational/singleton/AppConfigNaiveLazy.java create mode 100644 01-creational/singleton/AppConfigSynchronized.java create mode 100644 01-creational/singleton/Main.java create mode 100644 01-creational/singleton/README.md create mode 100644 02-structural/adapter/Main.java create mode 100644 02-structural/adapter/OrderService.java create mode 100644 02-structural/adapter/PaymentGateway.java create mode 100644 02-structural/adapter/README.md create mode 100644 02-structural/adapter/StripeClient.java create mode 100644 02-structural/adapter/StripePaymentAdapter.java create mode 100644 02-structural/adapter/StripePaymentClassAdapter.java create mode 100644 02-structural/bridge/AdvancedRemote.java create mode 100644 02-structural/bridge/Device.java create mode 100644 02-structural/bridge/Main.java create mode 100644 02-structural/bridge/README.md create mode 100644 02-structural/bridge/Radio.java create mode 100644 02-structural/bridge/RemoteControl.java create mode 100644 02-structural/bridge/TV.java create mode 100644 02-structural/composite/Directory.java create mode 100644 02-structural/composite/File.java create mode 100644 02-structural/composite/FileSystemItem.java create mode 100644 02-structural/composite/Main.java create mode 100644 02-structural/composite/README.md create mode 100644 02-structural/decorator/Main.java create mode 100644 02-structural/decorator/PlainTextProcessor.java create mode 100644 02-structural/decorator/ProfanityFilterDecorator.java create mode 100644 02-structural/decorator/README.md create mode 100644 02-structural/decorator/TextDecorator.java create mode 100644 02-structural/decorator/TextProcessor.java create mode 100644 02-structural/decorator/TrimDecorator.java create mode 100644 02-structural/decorator/UpperCaseDecorator.java create mode 100644 02-structural/facade/AudioMixer.java create mode 100644 02-structural/facade/BitrateReader.java create mode 100644 02-structural/facade/Codec.java create mode 100644 02-structural/facade/CodecFactory.java create mode 100644 02-structural/facade/MPEG4CompressionCodec.java create mode 100644 02-structural/facade/Main.java create mode 100644 02-structural/facade/OggCompressionCodec.java create mode 100644 02-structural/facade/README.md create mode 100644 02-structural/facade/VideoConversionFacade.java create mode 100644 02-structural/facade/VideoFile.java create mode 100644 02-structural/flyweight/Main.java create mode 100644 02-structural/flyweight/README.md create mode 100644 02-structural/flyweight/Tree.java create mode 100644 02-structural/flyweight/TreeFactory.java create mode 100644 02-structural/flyweight/TreeType.java create mode 100644 02-structural/proxy/DatabaseConnection.java create mode 100644 02-structural/proxy/LazyConnectionProxy.java create mode 100644 02-structural/proxy/LoggingProxy.java create mode 100644 02-structural/proxy/Main.java create mode 100644 02-structural/proxy/README.md create mode 100644 02-structural/proxy/RealDatabaseConnection.java create mode 100644 03-behavioral/chain-of-responsibility/CriticalIncidentTeam.java create mode 100644 03-behavioral/chain-of-responsibility/Level1Support.java create mode 100644 03-behavioral/chain-of-responsibility/Level2Support.java create mode 100644 03-behavioral/chain-of-responsibility/Level3Support.java create mode 100644 03-behavioral/chain-of-responsibility/Main.java create mode 100644 03-behavioral/chain-of-responsibility/README.md create mode 100644 03-behavioral/chain-of-responsibility/SupportHandler.java create mode 100644 03-behavioral/chain-of-responsibility/SupportTicket.java create mode 100644 03-behavioral/command/Command.java create mode 100644 03-behavioral/command/CommandHistory.java create mode 100644 03-behavioral/command/DeleteCommand.java create mode 100644 03-behavioral/command/InsertCommand.java create mode 100644 03-behavioral/command/Main.java create mode 100644 03-behavioral/command/README.md create mode 100644 03-behavioral/command/TextEditor.java create mode 100644 03-behavioral/interpreter/AddExpression.java create mode 100644 03-behavioral/interpreter/Expression.java create mode 100644 03-behavioral/interpreter/Main.java create mode 100644 03-behavioral/interpreter/MultiplyExpression.java create mode 100644 03-behavioral/interpreter/NumberExpression.java create mode 100644 03-behavioral/interpreter/README.md create mode 100644 03-behavioral/interpreter/SubtractExpression.java create mode 100644 03-behavioral/iterator/Book.java create mode 100644 03-behavioral/iterator/BookIterator.java create mode 100644 03-behavioral/iterator/BookShelf.java create mode 100644 03-behavioral/iterator/Main.java create mode 100644 03-behavioral/iterator/README.md create mode 100644 03-behavioral/mediator/ChatMediator.java create mode 100644 03-behavioral/mediator/ChatRoom.java create mode 100644 03-behavioral/mediator/Main.java create mode 100644 03-behavioral/mediator/README.md create mode 100644 03-behavioral/mediator/User.java create mode 100644 03-behavioral/memento/Editor.java create mode 100644 03-behavioral/memento/EditorMemento.java create mode 100644 03-behavioral/memento/History.java create mode 100644 03-behavioral/memento/Main.java create mode 100644 03-behavioral/memento/README.md create mode 100644 03-behavioral/observer/AlertObserver.java create mode 100644 03-behavioral/observer/Main.java create mode 100644 03-behavioral/observer/PortfolioObserver.java create mode 100644 03-behavioral/observer/README.md create mode 100644 03-behavioral/observer/StockMarket.java create mode 100644 03-behavioral/observer/StockObserver.java create mode 100644 03-behavioral/state/GreenState.java create mode 100644 03-behavioral/state/Main.java create mode 100644 03-behavioral/state/README.md create mode 100644 03-behavioral/state/RedState.java create mode 100644 03-behavioral/state/TrafficLight.java create mode 100644 03-behavioral/state/TrafficLightState.java create mode 100644 03-behavioral/state/YellowState.java create mode 100644 03-behavioral/strategy/BubbleSort.java create mode 100644 03-behavioral/strategy/Main.java create mode 100644 03-behavioral/strategy/MergeSort.java create mode 100644 03-behavioral/strategy/QuickSort.java create mode 100644 03-behavioral/strategy/README.md create mode 100644 03-behavioral/strategy/SortStrategy.java create mode 100644 03-behavioral/strategy/Sorter.java create mode 100644 03-behavioral/template-method/ApiMigration.java create mode 100644 03-behavioral/template-method/CsvMigration.java create mode 100644 03-behavioral/template-method/DataMigration.java create mode 100644 03-behavioral/template-method/Main.java create mode 100644 03-behavioral/template-method/README.md create mode 100644 03-behavioral/visitor/AreaCalculator.java create mode 100644 03-behavioral/visitor/Circle.java create mode 100644 03-behavioral/visitor/Main.java create mode 100644 03-behavioral/visitor/PerimeterCalculator.java create mode 100644 03-behavioral/visitor/README.md create mode 100644 03-behavioral/visitor/Rectangle.java create mode 100644 03-behavioral/visitor/Shape.java create mode 100644 03-behavioral/visitor/ShapeVisitor.java create mode 100644 03-behavioral/visitor/Triangle.java create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ec352c9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +out/ +*.class +screenshots/*.png +*.b64.txt diff --git a/01-creational/abstract-factory/Application.java b/01-creational/abstract-factory/Application.java new file mode 100644 index 0000000..a1c76a9 --- /dev/null +++ b/01-creational/abstract-factory/Application.java @@ -0,0 +1,30 @@ +package abstractfactory; + +public class Application { + + // The client holds references to the Abstract Factory and Abstract Products. + // It never imports LightButton, DarkButton, or any concrete class. + private final UIFactory factory; + private Button submitButton; + private TextField usernameField; + + public Application(UIFactory factory) { + this.factory = factory; // theme is injected; client doesn't decide + } + + public void buildUI() { + // Ask the factory — get consistent products from the same family + submitButton = factory.createButton(); + usernameField = factory.createTextField(); + } + + public void render() { + System.out.println("Rendering login screen:"); + usernameField.render(); + submitButton.render(); + } + + public void simulateSubmit() { + submitButton.onClick(); + } +} diff --git a/01-creational/abstract-factory/Button.java b/01-creational/abstract-factory/Button.java new file mode 100644 index 0000000..a88f44c --- /dev/null +++ b/01-creational/abstract-factory/Button.java @@ -0,0 +1,7 @@ +package abstractfactory; + +// Every button, regardless of theme, must be able to render and handle clicks +public interface Button { + void render(); // draw on screen + void onClick(); // respond to a click event +} 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/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/Main.java b/01-creational/abstract-factory/Main.java new file mode 100644 index 0000000..24e3f9d --- /dev/null +++ b/01-creational/abstract-factory/Main.java @@ -0,0 +1,16 @@ +package abstractfactory; + +// Entry point: the ONLY place that knows the theme +public class Main { + public static void main(String[] args) { + + // Production: read theme from config or OS preference + String theme = System.getProperty("app.theme", "light"); + UIFactory factory = "dark".equals(theme) ? new DarkThemeFactory() : new LightThemeFactory(); + + Application app = new Application(factory); + app.buildUI(); + app.render(); + app.simulateSubmit(); + } +} diff --git a/01-creational/abstract-factory/README.md b/01-creational/abstract-factory/README.md new file mode 100644 index 0000000..ae42c20 --- /dev/null +++ b/01-creational/abstract-factory/README.md @@ -0,0 +1,33 @@ +# Abstract Factory Design Pattern — Java Example + +**Pattern:** Creational → Abstract Factory +**Article:** https://ankurm.com/abstract-factory-design-pattern-java/ + +## What this example shows + +A UI toolkit that produces families of related widgets — light theme or dark theme — without the client ever naming a concrete class. `Button` and `TextField` are abstract products; `LightButton`/`LightTextField` and `DarkButton`/`DarkTextField` are the two concrete product families. `UIFactory` declares the creation methods for the whole family; `LightThemeFactory` and `DarkThemeFactory` each produce one consistent family. `Application` is the client — it only depends on `UIFactory` and the abstract products, so swapping the entire theme is a one-line change. `Main` selects the factory via a system property. + +## How to run + +```bash +javac abstract-factory/*.java -d out/abstract-factory +java -cp out/abstract-factory abstractfactory.Main +# or, for the dark theme family: +java -Dapp.theme=dark -cp out/abstract-factory abstractfactory.Main +``` + +Requires Java 25. + +## Post Section ↔ File Mapping + +| Post Section | File(s) | +|---|---| +| Part 1 — The Abstract Products: Button and TextField | `Button.java`, `TextField.java` | +| Part 2 — The Concrete Products (Light family) | `LightButton.java`, `LightTextField.java` | +| Part 2 — The Concrete Products (Dark family) | `DarkButton.java`, `DarkTextField.java` | +| Part 3 — The Abstract Factory: UIFactory Interface | `UIFactory.java` | +| Part 4 — The Concrete Factories | `LightThemeFactory.java`, `DarkThemeFactory.java` | +| Part 5 — The Client: Application | `Application.java`, `Main.java` | + +Article: https://ankurm.com/abstract-factory-design-pattern-java/ +All patterns: https://ankurm.com/design-patterns-java/ 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/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 new file mode 100644 index 0000000..f8dc88f --- /dev/null +++ b/01-creational/builder/Main.java @@ -0,0 +1,42 @@ +package builder; + +public class Main { + + public static void main(String[] args) { + + // Simple GET — only the required field + HttpRequest ping = new HttpRequest.Builder() + .url("https://api.example.com/health") + .build(); + + System.out.println("Simple GET: " + ping); + + // Full POST with authentication and retry + HttpRequest post = new HttpRequest.Builder() + .url("https://api.example.com/orders") + .method("POST") + .body("{\"item\":\"book\",\"qty\":1}") + .header("Content-Type", "application/json") + .header("Authorization", "Bearer eyJ...") + .timeoutMs(10_000) // 10 seconds — name is self-documenting + .maxRetries(3) + .build(); + + System.out.println("POST: " + post); + + // build() throws if url is missing — validation at construction time + try { + HttpRequest bad = new HttpRequest.Builder().build(); // no URL! + } catch (IllegalStateException e) { + System.out.println("Caught: " + e.getMessage()); // url() is required + } + + // Using the Director for the two standard presets defined above + HttpClientConfig config = new HttpClientConfig(); + HttpRequest healthCheck = config.buildHealthCheck("https://api.example.com"); + System.out.println("Health check: " + healthCheck); + + HttpRequest order = config.buildResilientPost("https://api.example.com/orders", "{\"qty\":1}"); + System.out.println("Order: " + order); + } +} diff --git a/01-creational/builder/README.md b/01-creational/builder/README.md new file mode 100644 index 0000000..8009463 --- /dev/null +++ b/01-creational/builder/README.md @@ -0,0 +1,30 @@ +# Builder Design Pattern — Java Example + +**Pattern:** Creational → Builder +**Article:** https://ankurm.com/builder-design-pattern-java/ + +## What this example shows + +An immutable `HttpRequest` assembled step by step instead of through a constructor with a dozen optional parameters. `HttpRequest` is the product — built once, never mutated afterward. `HttpClientConfig` is a director-style helper that encapsulates common request configurations so callers don't repeat the same builder calls. `Main` wires it together and shows the fluent builder chain in action. + +## How to run + +```bash +javac builder/*.java -d out/builder +java -cp out/builder builder.Main +``` + +Requires Java 25. + +## Post Section ↔ File Mapping + +| Post Section | File(s) | +|---|---| +| Part 1 — The Product: HttpRequest (Immutable) | `HttpRequest.java` | +| Part 2 — The Director: Encapsulating Common Configurations | `HttpClientConfig.java` | +| Part 3 — Using the Builder: Client Code | `Main.java` | + +Note: the Lombok `@Builder` example and the JDK standard-library snippets (`HttpRequest.newBuilder()`, `StringBuilder`, `ProcessBuilder`) are illustrative only — they are not part of this repository's runnable example. + +Article: https://ankurm.com/builder-design-pattern-java/ +All patterns: https://ankurm.com/design-patterns-java/ diff --git a/01-creational/factory-method/EmailNotification.java b/01-creational/factory-method/EmailNotification.java new file mode 100644 index 0000000..47d5736 --- /dev/null +++ b/01-creational/factory-method/EmailNotification.java @@ -0,0 +1,13 @@ +package factorymethod; + +public class EmailNotification implements Notification { + + @Override + public void send(String recipient, String message) { + // In production: use JavaMail or Spring Mail + System.out.printf(" [EMAIL] To: %s | Body: %s%n", recipient, message); + } + + @Override + public String getType() { return "EMAIL"; } +} 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/Main.java b/01-creational/factory-method/Main.java new file mode 100644 index 0000000..8f3856a --- /dev/null +++ b/01-creational/factory-method/Main.java @@ -0,0 +1,23 @@ +package factorymethod; + +public class Main { + + public static void main(String[] args) { + + // The client only knows about NotificationSender (the Creator) + NotificationSender sender; + + sender = new EmailSender(); + sender.notify("alice@example.com", "Your order has shipped!"); + + sender = new SmsSender(); + sender.notify("+1-555-0100", "OTP: 482910"); + + sender = new PushSender(); + sender.notify("device-token-xyz", "New message from Bob"); + + // Adding Slack: just swap the creator. Client code doesn't change. + sender = new SlackSender(); // new class, nothing else touched + sender.notify("#alerts", "CPU usage above 90%"); + } +} diff --git a/01-creational/factory-method/Notification.java b/01-creational/factory-method/Notification.java new file mode 100644 index 0000000..bcec43c --- /dev/null +++ b/01-creational/factory-method/Notification.java @@ -0,0 +1,11 @@ +package factorymethod; + +public interface Notification { + + // Every notification must know how to deliver itself + void send(String recipient, String message); + + // Used by the Creator for logging — it needs to know the type + // without knowing the concrete class + String getType(); +} 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 new file mode 100644 index 0000000..3e1c26b --- /dev/null +++ b/01-creational/factory-method/NotificationService.java @@ -0,0 +1,24 @@ +package factorymethod; + +public class NotificationService { + + public void send(String type, String recipient, String message) { + + // Creation logic mixed with business logic + Notification notification; + if ("EMAIL".equals(type)) { + notification = new EmailNotification(); + } else if ("SMS".equals(type)) { + notification = new SmsNotification(); + } else if ("PUSH".equals(type)) { + notification = new PushNotification(); + } else { + throw new IllegalArgumentException("Unknown type: " + type); + } + + // Business logic that should never change + System.out.println("Sending " + type + " to " + recipient); + notification.send(recipient, message); + System.out.println("Delivered."); + } +} diff --git a/01-creational/factory-method/PushNotification.java b/01-creational/factory-method/PushNotification.java new file mode 100644 index 0000000..f6307a1 --- /dev/null +++ b/01-creational/factory-method/PushNotification.java @@ -0,0 +1,13 @@ +package factorymethod; + +public class PushNotification implements Notification { + + @Override + public void send(String recipient, String message) { + // In production: use Firebase FCM + System.out.printf(" [PUSH] To: %s | Alert: %s%n", recipient, message); + } + + @Override + public String getType() { return "PUSH"; } +} 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/README.md b/01-creational/factory-method/README.md new file mode 100644 index 0000000..b5c17cd --- /dev/null +++ b/01-creational/factory-method/README.md @@ -0,0 +1,32 @@ +# Factory Method Design Pattern — Java Example + +**Pattern:** Creational → Factory Method +**Article:** https://ankurm.com/factory-method-design-pattern-java/ + +## What this example shows + +A notification system where the "send" logic decides which channel object to create, instead of the client doing it with a switch statement. `Notification` is the product interface; `EmailNotification`, `SmsNotification`, and `PushNotification` are concrete products. `NotificationSender` declares the factory method and a template `send()` step shared by every channel; `EmailSender`, `SmsSender`, and `PushSender` each override the factory method to produce their own notification type. `NotificationService` is shown separately as the anti-pattern this pattern replaces — it is not called by `Main`. `Main` demonstrates the cost of adding a new channel (Slack) by adding one new product and one new creator, with zero changes to existing classes. + +## How to run + +```bash +javac factory-method/*.java -d out/factory-method +java -cp out/factory-method factorymethod.Main +``` + +Requires Java 25. + +## Post Section ↔ File Mapping + +| Post Section | File(s) | +|---|---| +| The Problem: Object Creation Leaks Into Business Logic | `NotificationService.java` (anti-pattern shown for contrast — not called by `Main`) | +| Part 1 — The Notification Interface | `Notification.java` | +| Part 2 — The Concrete Notification Classes | `EmailNotification.java`, `SmsNotification.java`, `PushNotification.java` | +| Part 3 — The NotificationSender Class | `NotificationSender.java` | +| Part 4 — The Concrete Sender Subclasses | `EmailSender.java`, `SmsSender.java`, `PushSender.java` | +| Part 5 — Running It: Client Code and Output | `Main.java` | +| Adding a New Channel (Slack) | `SlackNotification.java`, `SlackSender.java` | + +Article: https://ankurm.com/factory-method-design-pattern-java/ +All patterns: https://ankurm.com/design-patterns-java/ 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 new file mode 100644 index 0000000..ea8e25a --- /dev/null +++ b/01-creational/factory-method/SmsNotification.java @@ -0,0 +1,13 @@ +package factorymethod; + +public class SmsNotification implements Notification { + + @Override + public void send(String recipient, String message) { + // In production: use Twilio SDK + System.out.printf(" [SMS] To: %s | Text: %s%n", recipient, message); + } + + @Override + public String getType() { return "SMS"; } +} 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/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 new file mode 100644 index 0000000..bbcfebd --- /dev/null +++ b/01-creational/prototype/Main.java @@ -0,0 +1,47 @@ +package prototype; + +import java.time.LocalDate; +import java.util.List; + +public class Main { + + public static void main(String[] args) { + + // Create the original document (imagine this was loaded from a database) + DocumentMetadata metadata = new DocumentMetadata("Alice", LocalDate.of(2025, 1, 15), "1.0"); + Document original = new Document( + "Q1 Report", + "Revenue increased by 12%...", + List.of("finance", "quarterly"), + metadata + ); + + System.out.println("Original: " + original); + + // Clone the document and apply only the differences + Document draft = original.copy(); + draft.setTitle("Q1 Report — DRAFT"); + draft.addTag("draft"); + draft.getMetadata().setAuthor("Bob"); // only the copy's metadata changes + draft.getMetadata().setVersion("1.0-draft"); + + System.out.println("Draft: " + draft); + System.out.println("Original: " + original); // unchanged — deep copy worked + + // Verify independence + System.out.println("\nSame object? " + (original == draft)); // false + System.out.println("Same tags? " + (original.getTags() == draft.getTags())); // false + System.out.println("Same meta? " + (original.getMetadata() == draft.getMetadata())); // false + + // Registry demo: register a named template once, then hand out independent copies + DocumentRegistry registry = new DocumentRegistry(); + registry.register("q-report-template", new Document( + "Quarterly Report Template", "## Executive Summary\n...", + List.of("quarterly", "finance"), metadata)); + + Document myReport = registry.get("q-report-template"); // a fresh, independent copy + myReport.setTitle("Q2 2025 Report"); + myReport.addTag("q2"); + System.out.println("\nFrom registry: " + myReport); + } +} diff --git a/01-creational/prototype/README.md b/01-creational/prototype/README.md new file mode 100644 index 0000000..2444646 --- /dev/null +++ b/01-creational/prototype/README.md @@ -0,0 +1,32 @@ +# Prototype Design Pattern — Java Example + +**Pattern:** Creational → Prototype +**Article:** https://ankurm.com/prototype-design-pattern-java/ + +## What this example shows + +Documents cloned from existing instances instead of rebuilt from scratch. `Copyable` declares the cloning contract. `DocumentMetadata` is a nested object that must be copied correctly for a clone to be truly independent of its source. `Document` is the concrete prototype, implementing a real (not shallow) copy. `DocumentRegistry` stores a set of ready-made prototypes that callers clone on demand instead of constructing from raw parameters. `Main` demonstrates cloning from the registry and confirms the clone and original don't share mutable state. + +## How to run + +```bash +javac prototype/*.java -d out/prototype +java -cp out/prototype prototype.Main +``` + +Requires Java 25. + +## Post Section ↔ File Mapping + +| Post Section | File(s) | +|---|---| +| Part 1 — The Prototype Interface: Copyable | `Copyable.java` | +| Part 2 — The Nested Object: DocumentMetadata | `DocumentMetadata.java` | +| Part 3 — The Concrete Prototype: Document | `Document.java` | +| Part 4 — The Prototype Registry: DocumentRegistry | `DocumentRegistry.java` | +| Part 5 — Using the Prototype: Client Code | `Main.java` | + +Note: the `ShallowVsDeepDemo` snippet (shallow vs. deep copy) and the `Cloneable`-based example are illustrative only — they are not part of this repository's runnable example. + +Article: https://ankurm.com/prototype-design-pattern-java/ +All patterns: https://ankurm.com/design-patterns-java/ diff --git a/01-creational/singleton/AppConfigDCL.java b/01-creational/singleton/AppConfigDCL.java new file mode 100644 index 0000000..6772ab0 --- /dev/null +++ b/01-creational/singleton/AppConfigDCL.java @@ -0,0 +1,31 @@ +package singleton; + +public class AppConfigDCL { + + // volatile is MANDATORY for DCL to work correctly. + // Without it, the JVM can reorder the writes inside the constructor + // and return a partially-constructed object to a second thread. + private static volatile AppConfigDCL instance; + + private AppConfigDCL() { /* load config */ } + + public static AppConfigDCL getInstance() { + + // First check: no lock, instant return for the 99.99% case + // (instance already created, just return it) + if (instance == null) { + + // Lock only when first check says we might need to create + synchronized (AppConfigDCL.class) { + + // Second check: re-verify under the lock. + // Another thread might have created the instance between + // our first check and acquiring the lock. + if (instance == null) { + instance = new AppConfigDCL(); + } + } + } + return instance; + } +} diff --git a/01-creational/singleton/AppConfigEager.java b/01-creational/singleton/AppConfigEager.java new file mode 100644 index 0000000..0bcb6e1 --- /dev/null +++ b/01-creational/singleton/AppConfigEager.java @@ -0,0 +1,24 @@ +package singleton; + +public class AppConfigEager { + + // The JVM creates this instance when the class is first loaded. + // static guarantees it belongs to the class, not any instance. + // final guarantees it can never be reassigned. + private static final AppConfigEager INSTANCE = new AppConfigEager(); + + // Private constructor: prevents any other class from calling new AppConfigEager() + private AppConfigEager() { + System.out.println("Reading configuration from file..."); + // load properties, set up values, etc. + } + + // The only way to get the instance + public static AppConfigEager getInstance() { + return INSTANCE; + } + + public String getProperty(String key) { + return System.getProperty(key, "(not set)"); + } +} diff --git a/01-creational/singleton/AppConfigEnum.java b/01-creational/singleton/AppConfigEnum.java new file mode 100644 index 0000000..10d2f64 --- /dev/null +++ b/01-creational/singleton/AppConfigEnum.java @@ -0,0 +1,19 @@ +package singleton; + +public enum AppConfigEnum { + + INSTANCE; // The JVM creates this exactly once, period. + + // Enum instances can have fields and methods like any class + private final String configPath; + + AppConfigEnum() { + // Enum constructors run once, when the constant is first used + this.configPath = System.getProperty("config.path", "application.properties"); + System.out.println("Loading config from: " + configPath); + } + + public String getProperty(String key) { + return System.getProperty(key, "(not set)"); + } +} diff --git a/01-creational/singleton/AppConfigHolder.java b/01-creational/singleton/AppConfigHolder.java new file mode 100644 index 0000000..f79b1f3 --- /dev/null +++ b/01-creational/singleton/AppConfigHolder.java @@ -0,0 +1,19 @@ +package singleton; + +public class AppConfigHolder { + + private AppConfigHolder() { /* load config */ } + + // The JVM loads Holder only when getInstance() is first called. + // Class loading is inherently thread-safe — the JVM ensures it happens once. + // No synchronized, no volatile, no locks — just the JVM class loading guarantee. + private static class Holder { + static final AppConfigHolder INSTANCE = new AppConfigHolder(); + } + + public static AppConfigHolder getInstance() { + // This reference triggers Holder to load (if it hasn't already). + // After first call, Holder is already loaded and this is just a field read. + return Holder.INSTANCE; + } +} diff --git a/01-creational/singleton/AppConfigNaiveLazy.java b/01-creational/singleton/AppConfigNaiveLazy.java new file mode 100644 index 0000000..d8eca65 --- /dev/null +++ b/01-creational/singleton/AppConfigNaiveLazy.java @@ -0,0 +1,16 @@ +package singleton; + +public class AppConfigNaiveLazy { + + private static AppConfigNaiveLazy instance; // null until first request + + private AppConfigNaiveLazy() { /* load config */ } + + // BROKEN IN MULTITHREADED CODE — see explanation below + public static AppConfigNaiveLazy getInstance() { + if (instance == null) { // Thread A: checks, sees null + instance = new AppConfigNaiveLazy(); // Thread B: also sees null, also enters here + } // Both threads create a new AppConfigNaiveLazy! + return instance; + } +} diff --git a/01-creational/singleton/AppConfigSynchronized.java b/01-creational/singleton/AppConfigSynchronized.java new file mode 100644 index 0000000..1f0e76e --- /dev/null +++ b/01-creational/singleton/AppConfigSynchronized.java @@ -0,0 +1,16 @@ +package singleton; + +public class AppConfigSynchronized { + + private static AppConfigSynchronized instance; + + private AppConfigSynchronized() { /* load config */ } + + // synchronized: only one thread can execute this method at a time + public static synchronized AppConfigSynchronized getInstance() { + if (instance == null) { + instance = new AppConfigSynchronized(); // safe — no other thread can be here + } + return instance; + } +} diff --git a/01-creational/singleton/Main.java b/01-creational/singleton/Main.java new file mode 100644 index 0000000..8827d8b --- /dev/null +++ b/01-creational/singleton/Main.java @@ -0,0 +1,36 @@ +package singleton; + +public class Main { + public static void main(String[] args) { + + System.out.println("--- Implementation 1: Eager ---"); + AppConfigEager eager1 = AppConfigEager.getInstance(); + AppConfigEager eager2 = AppConfigEager.getInstance(); + System.out.println("Same instance? " + (eager1 == eager2)); + System.out.println("db.url = " + eager1.getProperty("db.url")); + + System.out.println("\n--- Implementation 2: Naive Lazy ---"); + AppConfigNaiveLazy lazy1 = AppConfigNaiveLazy.getInstance(); + AppConfigNaiveLazy lazy2 = AppConfigNaiveLazy.getInstance(); + System.out.println("Same instance? " + (lazy1 == lazy2)); + + System.out.println("\n--- Implementation 3: Synchronized Method ---"); + AppConfigSynchronized sync1 = AppConfigSynchronized.getInstance(); + AppConfigSynchronized sync2 = AppConfigSynchronized.getInstance(); + System.out.println("Same instance? " + (sync1 == sync2)); + + System.out.println("\n--- Implementation 4: Double-Checked Locking ---"); + AppConfigDCL dcl1 = AppConfigDCL.getInstance(); + AppConfigDCL dcl2 = AppConfigDCL.getInstance(); + System.out.println("Same instance? " + (dcl1 == dcl2)); + + System.out.println("\n--- Implementation 5: Holder Idiom ---"); + AppConfigHolder holder1 = AppConfigHolder.getInstance(); + AppConfigHolder holder2 = AppConfigHolder.getInstance(); + System.out.println("Same instance? " + (holder1 == holder2)); + + System.out.println("\n--- Implementation 6: Enum ---"); + AppConfigEnum enumConfig = AppConfigEnum.INSTANCE; + System.out.println("db.url = " + enumConfig.getProperty("db.url")); + } +} diff --git a/01-creational/singleton/README.md b/01-creational/singleton/README.md new file mode 100644 index 0000000..409920f --- /dev/null +++ b/01-creational/singleton/README.md @@ -0,0 +1,34 @@ +# Singleton Design Pattern — Java Example + +**Pattern:** Creational → Singleton +**Article:** https://ankurm.com/singleton-design-pattern-java/ + +## What this example shows + +Six different ways to implement a singleton, compiled and run side by side so their tradeoffs are directly comparable. `AppConfigEager` initializes at class-load time. `AppConfigNaiveLazy` defers initialization but is not thread-safe. `AppConfigSynchronized` fixes thread-safety at the cost of locking on every call. `AppConfigDCL` uses double-checked locking to avoid that per-call lock. `AppConfigHolder` uses the initialization-on-demand holder idiom for lazy, thread-safe initialization with no locking at all. `AppConfigEnum` uses a single-element enum, which the JVM guarantees is a singleton even against reflection and serialization attacks. `Main` exercises all six in one run. + +## How to run + +```bash +javac singleton/*.java -d out/singleton +java -cp out/singleton singleton.Main +``` + +Requires Java 25. + +## Post Section ↔ File Mapping + +| Post Section | File(s) | +|---|---| +| Implementation 1 — Eager Initialization | `AppConfigEager.java` | +| Implementation 2 — Naïve Lazy Initialization | `AppConfigNaiveLazy.java` | +| Implementation 3 — Synchronized Method | `AppConfigSynchronized.java` | +| Implementation 4 — Double-Checked Locking | `AppConfigDCL.java` | +| Implementation 5 — Initialization-on-Demand Holder | `AppConfigHolder.java` | +| Implementation 6 — Enum Singleton | `AppConfigEnum.java` | +| Running All Six Implementations Together | `Main.java` | + +Each implementation is renamed to its own class (e.g. `AppConfigEager`, `AppConfigHolder`) so all six can be compiled together in one package and compared directly. The "Breaking via Reflection," "Breaking via Serialization," and Spring `@Component` snippets are illustrative only — they are not part of this repository's runnable example. + +Article: https://ankurm.com/singleton-design-pattern-java/ +All patterns: https://ankurm.com/design-patterns-java/ diff --git a/02-structural/adapter/Main.java b/02-structural/adapter/Main.java new file mode 100644 index 0000000..523efe6 --- /dev/null +++ b/02-structural/adapter/Main.java @@ -0,0 +1,27 @@ +package adapter; + +public class Main { + + public static void main(String[] args) { + System.out.println("=== Adapter Design Pattern Demo ===\n"); + + // --- Object Adapter: Stripe --- + System.out.println("-- Using Stripe via Adapter --"); + StripeClient stripeClient = new StripeClient("sk_test_4eC39HqLyjWDarjtT1zdp7dc"); + PaymentGateway stripeGateway = new StripePaymentAdapter(stripeClient); + + OrderService orderService = new OrderService(stripeGateway); + orderService.processOrder("ORD-001", "cus_abc123", 99.99); + + System.out.println("\n-- Testing refund --"); + boolean refunded = stripeGateway.refund("ch_ORD-001", 99.99); + System.out.println("Refund issued: " + refunded); + + // --- JDK Example: InputStreamReader as Adapter --- + System.out.println("\n-- JDK Adapter: InputStreamReader --"); + System.out.println("InputStreamReader wraps System.in (InputStream) as a Reader."); + System.out.println("Your code reads chars; the adapter handles byte-to-char conversion."); + + System.out.println("\n=== Demo complete ==="); + } +} diff --git a/02-structural/adapter/OrderService.java b/02-structural/adapter/OrderService.java new file mode 100644 index 0000000..934b92e --- /dev/null +++ b/02-structural/adapter/OrderService.java @@ -0,0 +1,23 @@ +package adapter; +/** + * The Client — uses only the PaymentGateway interface. + * It has no idea whether it's talking to Stripe, PayPal, or Braintree. + */ +public class OrderService { + private final PaymentGateway gateway; + public OrderService(PaymentGateway gateway) { + this.gateway = gateway; + } + public void processOrder(String orderId, String customerId, double total) { + System.out.printf("%nProcessing order %s for customer %s, total: $%.2f%n", + orderId, customerId, total); + boolean charged = gateway.charge(customerId, total, "USD"); + if (charged) { + System.out.println("Payment accepted. Order confirmed."); + String status = gateway.getStatus("ch_" + orderId); + System.out.println("Transaction status: " + status); + } else { + System.out.println("Payment failed. Order rejected."); + } + } +} diff --git a/02-structural/adapter/PaymentGateway.java b/02-structural/adapter/PaymentGateway.java new file mode 100644 index 0000000..f0fa7c3 --- /dev/null +++ b/02-structural/adapter/PaymentGateway.java @@ -0,0 +1,10 @@ +package adapter; +/** + * The Target interface — what YOUR application's code expects. + * Every payment gateway in your system must implement this. + */ +public interface PaymentGateway { + boolean charge(String customerId, double amount, String currency); + boolean refund(String transactionId, double amount); + String getStatus(String transactionId); +} diff --git a/02-structural/adapter/README.md b/02-structural/adapter/README.md new file mode 100644 index 0000000..445f9a8 --- /dev/null +++ b/02-structural/adapter/README.md @@ -0,0 +1,36 @@ +# Adapter Design Pattern — Java Example + +**Pattern:** Structural → Adapter +**Article:** https://ankurm.com/adapter-design-pattern-java/ + +## What this example shows + +Wraps a third-party `StripeClient` (with its own API) behind a `PaymentGateway` interface that your application code expects. The `OrderService` client never touches `StripeClient` directly — it only sees `PaymentGateway`. Swapping payment providers requires changing one line. + +## How to run + +```bash +# From this folder: +javac adapter/*.java +java adapter.Main +``` + +Requires Java 25. + +## Files + +| Post Section | File(s) | +|---|---| +| Step 1 — Define the Target Interface | `PaymentGateway.java` | +| Step 2 — The Adaptee (What You're Wrapping) | `StripeClient.java` | +| Step 3 — The Object Adapter | `StripePaymentAdapter.java` | +| Step 4 — The Client (Knows Nothing About Stripe) | `OrderService.java` | +| Putting It Together: The Main Demo | `Main.java` | +| Object Adapter vs Class Adapter | `StripePaymentClassAdapter.java` | + +Note: the `InputStreamReader` JDK-adapter snippet is illustrative only — it is not part of this repository's runnable example. + +## See Also + +- Full article: https://ankurm.com/adapter-design-pattern-java/ +- All design patterns: https://ankurm.com/design-patterns-java/ diff --git a/02-structural/adapter/StripeClient.java b/02-structural/adapter/StripeClient.java new file mode 100644 index 0000000..72b893b --- /dev/null +++ b/02-structural/adapter/StripeClient.java @@ -0,0 +1,39 @@ +package adapter; +/** + * The Adaptee — a third-party payment SDK with a completely different interface. + * Imagine this is Stripe's actual SDK: you cannot modify this class, + * and it doesn't implement PaymentGateway. + */ +public class StripeClient { + private final String apiKey; + public StripeClient(String apiKey) { + this.apiKey = apiKey; + System.out.println("[Stripe] Initialized with key: " + apiKey.substring(0, 8) + "..."); + } + // Stripe uses cents, not decimal amounts + public StripeChargeResult createCharge(String customerId, long amountInCents, String currency) { + System.out.printf("[Stripe] Charging customer=%s, amount=%d cents, currency=%s%n", + customerId, amountInCents, currency); + return new StripeChargeResult("ch_" + System.currentTimeMillis(), true, null); + } + // Stripe's refund method takes a charge ID and uses different naming + public boolean issueRefund(String chargeId, long amountInCents) { + System.out.printf("[Stripe] Refunding charge=%s, amount=%d cents%n", chargeId, amountInCents); + return true; + } + // Stripe uses 'retrieve' not 'getStatus', and returns an object + public StripeChargeResult retrieveCharge(String chargeId) { + System.out.printf("[Stripe] Retrieving charge=%s%n", chargeId); + return new StripeChargeResult(chargeId, true, "succeeded"); + } + public static class StripeChargeResult { + public final String chargeId; + public final boolean success; + public final String status; + public StripeChargeResult(String chargeId, boolean success, String status) { + this.chargeId = chargeId; + this.success = success; + this.status = status; + } + } +} diff --git a/02-structural/adapter/StripePaymentAdapter.java b/02-structural/adapter/StripePaymentAdapter.java new file mode 100644 index 0000000..5a586dd --- /dev/null +++ b/02-structural/adapter/StripePaymentAdapter.java @@ -0,0 +1,36 @@ +package adapter; +/** + * The Adapter — bridges StripeClient to PaymentGateway. + * + * Object Adapter variant: holds a StripeClient instance via composition. + * This means it can wrap any StripeClient including subclasses. + */ +public class StripePaymentAdapter implements PaymentGateway { + private final StripeClient stripe; + public StripePaymentAdapter(StripeClient stripe) { + this.stripe = stripe; + } + @Override + public boolean charge(String customerId, double amount, String currency) { + // Translation 1: dollars → cents + long amountInCents = Math.round(amount * 100); + // Translation 2: charge() → createCharge(), lowercase currency + StripeClient.StripeChargeResult result = + stripe.createCharge(customerId, amountInCents, currency.toLowerCase()); + // Translation 3: StripeChargeResult → boolean + return result.success; + } + @Override + public boolean refund(String transactionId, double amount) { + long amountInCents = Math.round(amount * 100); + // Translation: refund() → issueRefund(), your "transactionId" is Stripe's "chargeId" + return stripe.issueRefund(transactionId, amountInCents); + } + @Override + public String getStatus(String transactionId) { + // Translation: getStatus() → retrieveCharge(), StripeChargeResult → String + StripeClient.StripeChargeResult result = stripe.retrieveCharge(transactionId); + if (!result.success) return "FAILED"; + return result.status != null ? result.status.toUpperCase() : "UNKNOWN"; + } +} diff --git a/02-structural/adapter/StripePaymentClassAdapter.java b/02-structural/adapter/StripePaymentClassAdapter.java new file mode 100644 index 0000000..28324b3 --- /dev/null +++ b/02-structural/adapter/StripePaymentClassAdapter.java @@ -0,0 +1,23 @@ +package adapter; +// Class Adapter: extends Adaptee, implements Target +// Can only be used when you CAN extend the Adaptee (it's not final) +public class StripePaymentClassAdapter extends StripeClient implements PaymentGateway { + public StripePaymentClassAdapter(String apiKey) { + super(apiKey); + } + @Override + public boolean charge(String customerId, double amount, String currency) { + long cents = Math.round(amount * 100); + return createCharge(customerId, cents, currency.toLowerCase()).success; + // Note: calls inherited method directly — no 'stripe.' prefix needed + } + @Override + public boolean refund(String transactionId, double amount) { + return issueRefund(transactionId, Math.round(amount * 100)); + } + @Override + public String getStatus(String transactionId) { + StripeChargeResult r = retrieveCharge(transactionId); + return r.success ? (r.status != null ? r.status.toUpperCase() : "UNKNOWN") : "FAILED"; + } +} diff --git a/02-structural/bridge/AdvancedRemote.java b/02-structural/bridge/AdvancedRemote.java new file mode 100644 index 0000000..9e0e46e --- /dev/null +++ b/02-structural/bridge/AdvancedRemote.java @@ -0,0 +1,21 @@ +package bridge; +/** + * Refined Abstraction — extends RemoteControl with additional capabilities. + * + * The device hierarchy doesn't change at all. TV and Radio + * automatically support mute() and jumpToChannel() because they + * implement Device. Zero new code on the implementation side. + */ +public class AdvancedRemote extends RemoteControl { + public AdvancedRemote(Device device) { + super(device); + } + public void mute() { + System.out.println(" Muting " + device.getName()); + device.setVolume(0); + } + public void jumpToChannel(int channel) { + System.out.println(" Jumping to channel " + channel); + device.setChannel(channel); + } +} diff --git a/02-structural/bridge/Device.java b/02-structural/bridge/Device.java new file mode 100644 index 0000000..98daaa6 --- /dev/null +++ b/02-structural/bridge/Device.java @@ -0,0 +1,16 @@ +package bridge; +/** + * Implementor — the "implementation" side of the bridge. + * All devices (TV, Radio, SmartSpeaker, etc.) implement this. + * The remote controls only know about this interface, never about specific devices. + */ +public interface Device { + boolean isEnabled(); + void enable(); + void disable(); + int getVolume(); + void setVolume(int percent); + int getChannel(); + void setChannel(int channel); + String getName(); +} diff --git a/02-structural/bridge/Main.java b/02-structural/bridge/Main.java new file mode 100644 index 0000000..358604c --- /dev/null +++ b/02-structural/bridge/Main.java @@ -0,0 +1,29 @@ +package bridge; +public class Main { + public static void main(String[] args) { + System.out.println("=== Bridge Design Pattern Demo ===\n"); + // Combination 1: Basic Remote + TV + System.out.println("-- Basic Remote controlling TV --"); + RemoteControl remote1 = new RemoteControl(new TV()); + remote1.togglePower(); + remote1.volumeUp(); + remote1.channelUp(); + System.out.println(); + // Combination 2: Advanced Remote + Radio + System.out.println("-- Advanced Remote controlling Radio --"); + AdvancedRemote remote2 = new AdvancedRemote(new Radio()); + remote2.togglePower(); + remote2.volumeUp(); + remote2.mute(); + remote2.jumpToChannel(91); + System.out.println(); + // Combination 3: Advanced Remote + TV (no new classes needed!) + System.out.println("-- Advanced Remote controlling TV --"); + AdvancedRemote remote3 = new AdvancedRemote(new TV()); + remote3.togglePower(); + remote3.jumpToChannel(5); + remote3.mute(); + System.out.println("\n=== Demo complete ==="); + System.out.println("3 different remote+device combinations, 0 new classes needed."); + } +} diff --git a/02-structural/bridge/README.md b/02-structural/bridge/README.md new file mode 100644 index 0000000..99f9d05 --- /dev/null +++ b/02-structural/bridge/README.md @@ -0,0 +1,32 @@ +# Bridge Design Pattern — Java Example + +**Pattern:** Structural → Bridge +**Article:** https://ankurm.com/bridge-design-pattern-java/ + +## What this example shows + +Decouples remote controls (Abstraction) from devices (Implementation). A basic remote and an advanced remote each work with any device (TV, Radio) without creating N×M subclasses. + +## How to run + +```bash +javac bridge/*.java -d out/bridge +java -cp out/bridge bridge.Main +``` + +Requires Java 25. + +## Post Section ↔ File Mapping + +| Post Section | File(s) | +|---|---| +| Step 1 — The Implementor Interface | `Device.java` | +| Step 2 — Concrete Implementors (TV and Radio) | `TV.java`, `Radio.java` | +| Step 3 — The Abstraction (RemoteControl) | `RemoteControl.java` | +| Step 4 — Refined Abstraction (AdvancedRemote) | `AdvancedRemote.java` | +| Wiring It Together: Main Demo | `Main.java` | + +Note: the "Class Explosion Problem — Visualised" pseudocode and the JDBC/SLF4J snippets shown in the article are illustrative only — they are not part of this repository's runnable example. + +Article: https://ankurm.com/bridge-design-pattern-java/ +All patterns: https://ankurm.com/design-patterns-java/ diff --git a/02-structural/bridge/Radio.java b/02-structural/bridge/Radio.java new file mode 100644 index 0000000..f538fea --- /dev/null +++ b/02-structural/bridge/Radio.java @@ -0,0 +1,18 @@ +package bridge; +// Concrete Implementor #2: Radio +public class Radio implements Device { + private boolean on = false; + private int volume = 20; + private int channel = 1; + @Override public boolean isEnabled() { return on; } + @Override public void enable() { on = true; System.out.println(" [Radio] Powered ON"); } + @Override public void disable() { on = false; System.out.println(" [Radio] Powered OFF"); } + @Override public int getVolume() { return volume; } + @Override public void setVolume(int percent) { + this.volume = Math.max(0, Math.min(100, percent)); + System.out.println(" [Radio] Volume set to " + this.volume); + } + @Override public int getChannel() { return channel; } + @Override public void setChannel(int ch) { this.channel = ch; System.out.println(" [Radio] Frequency -> " + ch); } + @Override public String getName() { return "JBL Radio"; } +} diff --git a/02-structural/bridge/RemoteControl.java b/02-structural/bridge/RemoteControl.java new file mode 100644 index 0000000..5107fc8 --- /dev/null +++ b/02-structural/bridge/RemoteControl.java @@ -0,0 +1,27 @@ +package bridge; +/** + * Abstraction — the remote control. + * + * Key point: RemoteControl holds a Device (the bridge) and delegates + * all actual work to it. It adds higher-level semantics on top: + * "togglePower" instead of separate enable()/disable() calls. + */ +public class RemoteControl { + protected Device device; // THE BRIDGE + public RemoteControl(Device device) { + this.device = device; + System.out.println("Remote paired with: " + device.getName()); + } + // User action → device operation translation + public void togglePower() { + if (device.isEnabled()) { + device.disable(); + } else { + device.enable(); + } + } + public void volumeUp() { device.setVolume(device.getVolume() + 10); } + public void volumeDown() { device.setVolume(device.getVolume() - 10); } + public void channelUp() { device.setChannel(device.getChannel() + 1); } + public void channelDown() { device.setChannel(device.getChannel() - 1); } +} diff --git a/02-structural/bridge/TV.java b/02-structural/bridge/TV.java new file mode 100644 index 0000000..dbb46fc --- /dev/null +++ b/02-structural/bridge/TV.java @@ -0,0 +1,18 @@ +package bridge; +// Concrete Implementor #1: Television +public class TV implements Device { + private boolean on = false; + private int volume = 30; + private int channel = 1; + @Override public boolean isEnabled() { return on; } + @Override public void enable() { on = true; System.out.println(" [TV] Powered ON"); } + @Override public void disable() { on = false; System.out.println(" [TV] Powered OFF"); } + @Override public int getVolume() { return volume; } + @Override public void setVolume(int percent) { + this.volume = Math.max(0, Math.min(100, percent)); + System.out.println(" [TV] Volume set to " + this.volume); + } + @Override public int getChannel() { return channel; } + @Override public void setChannel(int ch) { this.channel = ch; System.out.println(" [TV] Channel -> " + ch); } + @Override public String getName() { return "Samsung TV"; } +} diff --git a/02-structural/composite/Directory.java b/02-structural/composite/Directory.java new file mode 100644 index 0000000..7dca2ad --- /dev/null +++ b/02-structural/composite/Directory.java @@ -0,0 +1,39 @@ +package composite; +import java.util.ArrayList; +import java.util.List; +/** + * Composite — a directory that holds both Files (leaves) and other Directories. + * + * getSize() is recursive: asks each child for its size and sums them. + * print() recurses with deeper indentation. + * + * The caller doesn't care whether a child is a File or Directory — + * both answer getSize() and print() the same way. + */ +public class Directory implements FileSystemItem { + private final String name; + private final List children = new ArrayList<>(); + public Directory(String name) { this.name = name; } + // Fluent add() — allows chaining: dir.add(file1).add(file2).add(subDir) + public Directory add(FileSystemItem item) { + children.add(item); + return this; + } + public void remove(FileSystemItem item) { children.remove(item); } + @Override public String getName() { return name; } + @Override + public long getSize() { + // Each child knows its own size: files return bytes, directories recurse. + // This is the Composite's core: uniform delegation down the tree. + return children.stream() + .mapToLong(FileSystemItem::getSize) + .sum(); + } + @Override + public void print(String indent) { + System.out.printf("%s[DIR] %s/ (%,d bytes total)%n", indent, name, getSize()); + for (FileSystemItem child : children) { + child.print(indent + " "); // each level indents 4 more spaces + } + } +} diff --git a/02-structural/composite/File.java b/02-structural/composite/File.java new file mode 100644 index 0000000..d9bfca9 --- /dev/null +++ b/02-structural/composite/File.java @@ -0,0 +1,20 @@ +package composite; +/** + * Leaf — a single file with no children. + * getSize() returns its own bytes. print() outputs a single line. + * No knowledge of directories or nesting exists in this class. + */ +public class File implements FileSystemItem { + private final String name; + private final long size; + public File(String name, long sizeBytes) { + this.name = name; + this.size = sizeBytes; + } + @Override public String getName() { return name; } + @Override public long getSize() { return size; } + @Override + public void print(String indent) { + System.out.printf("%s[FILE] %s (%,d bytes)%n", indent, name, size); + } +} diff --git a/02-structural/composite/FileSystemItem.java b/02-structural/composite/FileSystemItem.java new file mode 100644 index 0000000..cf6b68f --- /dev/null +++ b/02-structural/composite/FileSystemItem.java @@ -0,0 +1,10 @@ +package composite; +/** + * Component — the common interface for both files (leaves) and directories (composites). + * Clients work through this interface and never need to know which type they're dealing with. + */ +public interface FileSystemItem { + String getName(); + long getSize(); // total size in bytes — recursive for directories + void print(String indent); // display the tree at the given indentation level +} diff --git a/02-structural/composite/Main.java b/02-structural/composite/Main.java new file mode 100644 index 0000000..80181a0 --- /dev/null +++ b/02-structural/composite/Main.java @@ -0,0 +1,36 @@ +package composite; +public class Main { + public static void main(String[] args) { + System.out.println("=== Composite Design Pattern Demo ===\n"); + // Build the tree — mix files and directories freely + Directory root = new Directory("project"); + Directory src = new Directory("src"); + Directory main = new Directory("main"); + main.add(new File("App.java", 4_200)) + .add(new File("Config.java", 1_800)) + .add(new File("Application.yml", 3_100)); + Directory test = new Directory("test"); + test.add(new File("AppTest.java", 2_600)) + .add(new File("ConfigTest.java", 1_200)); + src.add(main).add(test); + Directory resources = new Directory("resources"); + resources.add(new File("application.yml", 1_500)) + .add(new File("logback.xml", 900)) + .add(new File("banner.txt", 200)); + root.add(src) + .add(resources) + .add(new File("pom.xml", 8_400)) + .add(new File("README.md", 2_100)); + // Print the entire tree — recursion is automatic + System.out.println("File system tree:"); + root.print(""); + System.out.printf("%nTotal project size: %,d bytes%n", root.getSize()); + // The key demonstration: File and Directory through the same interface + System.out.println("\n-- Treating File and Directory uniformly --"); + FileSystemItem[] items = { new File("standalone.txt", 500), src }; + for (FileSystemItem item : items) { + System.out.printf("%s -> size: %,d bytes%n", item.getName(), item.getSize()); + } + System.out.println("\n=== Demo complete ==="); + } +} diff --git a/02-structural/composite/README.md b/02-structural/composite/README.md new file mode 100644 index 0000000..4f2c62e --- /dev/null +++ b/02-structural/composite/README.md @@ -0,0 +1,30 @@ +# Composite Design Pattern — Java Example + +**Pattern:** Structural → Composite +**Article:** https://ankurm.com/composite-design-pattern-java/ + +## What this example shows + +Builds a file system tree where files (leaves) and directories (composites) share the same `FileSystemItem` interface. Callers compute size or print the tree without ever checking whether a node is a file or a directory. + +## How to run + +```bash +javac composite/*.java -d out/composite +java -cp out/composite composite.Main +``` + +Requires Java 25. + +## Post Section ↔ File Mapping + +| Post Section | File(s) | +|---|---| +| The Problem: Treating Leaves and Containers Uniformly | illustrative only — not part of this repository's runnable example | +| Step 1 — The Component Interface | `FileSystemItem.java` | +| Step 2 — The Leaf (File) | `File.java` | +| Step 3 — The Composite (Directory) | `Directory.java` | +| Building and Using the Tree | `Main.java` | + +Article: https://ankurm.com/composite-design-pattern-java/ +All patterns: https://ankurm.com/design-patterns-java/ diff --git a/02-structural/decorator/Main.java b/02-structural/decorator/Main.java new file mode 100644 index 0000000..e08d3c4 --- /dev/null +++ b/02-structural/decorator/Main.java @@ -0,0 +1,42 @@ +package decorator; +public class Main { + public static void main(String[] args) { + System.out.println("=== Decorator Design Pattern Demo ===\n"); + + String input = " hello world, badword is here "; + System.out.println("Input: \"" + input + "\"\n"); + + // Stack 1: just trim + TextProcessor trimOnly = new TrimDecorator(new PlainTextProcessor()); + System.out.println("Trim only: \"" + trimOnly.process(input) + "\""); + + // Stack 2: trim first (innermost), then uppercase (outermost) + // Execution order: PlainText → Trim → UpperCase + TextProcessor trimThenUpper = + new UpperCaseDecorator( + new TrimDecorator( + new PlainTextProcessor())); + System.out.println("Trim + UpperCase: \"" + trimThenUpper.process(input) + "\""); + + // Stack 3: trim, filter profanity, then uppercase + TextProcessor full = + new UpperCaseDecorator( + new ProfanityFilterDecorator( + new TrimDecorator( + new PlainTextProcessor()))); + System.out.println("Trim + Filter + Upper: \"" + full.process(input) + "\""); + + // Stack 4: filter THEN trim — different result because order changed + TextProcessor filterFirst = + new TrimDecorator( + new ProfanityFilterDecorator( + new PlainTextProcessor())); + System.out.println("Filter + Trim: \"" + filterFirst.process(input) + "\""); + + System.out.println("\n--- JDK equivalent ---"); + System.out.println("new BufferedReader(new InputStreamReader(socket.getInputStream()))"); + System.out.println("Same pattern: each wrapper adds one behaviour, order matters."); + + System.out.println("\n=== Demo complete ==="); + } +} diff --git a/02-structural/decorator/PlainTextProcessor.java b/02-structural/decorator/PlainTextProcessor.java new file mode 100644 index 0000000..5b99c92 --- /dev/null +++ b/02-structural/decorator/PlainTextProcessor.java @@ -0,0 +1,12 @@ +package decorator; +/** + * Concrete Component — the starting point for decoration. + * Returns text unchanged. Decorators wrap around this + * and transform the result one layer at a time. + */ +public class PlainTextProcessor implements TextProcessor { + @Override + public String process(String text) { + return text; // no transformation — base case + } +} diff --git a/02-structural/decorator/ProfanityFilterDecorator.java b/02-structural/decorator/ProfanityFilterDecorator.java new file mode 100644 index 0000000..9535849 --- /dev/null +++ b/02-structural/decorator/ProfanityFilterDecorator.java @@ -0,0 +1,14 @@ +package decorator; +/** Concrete Decorator: replaces profane words with asterisks. */ +public class ProfanityFilterDecorator extends TextDecorator { + private static final String[] BAD_WORDS = {"badword", "spam"}; + public ProfanityFilterDecorator(TextProcessor wrapped) { super(wrapped); } + @Override + public String process(String text) { + String result = super.process(text); + for (String word : BAD_WORDS) { + result = result.replaceAll("(?i)" + word, "*".repeat(word.length())); + } + return result; + } +} diff --git a/02-structural/decorator/README.md b/02-structural/decorator/README.md new file mode 100644 index 0000000..611025a --- /dev/null +++ b/02-structural/decorator/README.md @@ -0,0 +1,32 @@ +# Decorator Design Pattern — Java Example + +**Pattern:** Structural → Decorator +**Article:** https://ankurm.com/decorator-design-pattern-java/ + +## What this example shows + +Builds a text-processing pipeline where behaviours (trim, uppercase, profanity filter) are stacked as wrapper objects around a `PlainTextProcessor`, all sharing the `TextProcessor` interface — the same idea behind `new BufferedReader(new InputStreamReader(...))` in the JDK. + +## How to run + +```bash +javac decorator/*.java -d out/decorator +java -cp out/decorator decorator.Main +``` + +Requires Java 25. + +## Post Section ↔ File Mapping + +| Post Section | File(s) | +|---|---| +| Step 1 — Component Interface | `TextProcessor.java` | +| Step 2 — Concrete Component | `PlainTextProcessor.java` | +| Step 3 — Base Decorator | `TextDecorator.java` | +| Step 4 — Concrete Decorators | `UpperCaseDecorator.java`, `TrimDecorator.java`, `ProfanityFilterDecorator.java` | +| Stacking the Decorators — Order Matters | `Main.java` | + +Note: the JDK `InputStream`/`BufferedInputStream`/`GZIPInputStream` snippet under "Decorator in the JDK: I/O Streams" is illustrative only — it is not part of this repository's runnable example. + +Article: https://ankurm.com/decorator-design-pattern-java/ +All patterns: https://ankurm.com/design-patterns-java/ diff --git a/02-structural/decorator/TextDecorator.java b/02-structural/decorator/TextDecorator.java new file mode 100644 index 0000000..91eefa2 --- /dev/null +++ b/02-structural/decorator/TextDecorator.java @@ -0,0 +1,22 @@ +package decorator; +/** + * Base Decorator — holds a reference to the wrapped TextProcessor + * and delegates to it. Concrete decorators extend this class. + * + * Why an abstract class rather than another interface implementation? + * To avoid repeating the wrapping boilerplate (the field + constructor + + * delegation call) in every single concrete decorator. + */ +public abstract class TextDecorator implements TextProcessor { + protected final TextProcessor wrapped; + protected TextDecorator(TextProcessor wrapped) { + this.wrapped = wrapped; + } + @Override + public String process(String text) { + // Default: pass through to the wrapped processor. + // Concrete decorators call super.process(text) to invoke this, + // then apply their own transformation to the result. + return wrapped.process(text); + } +} diff --git a/02-structural/decorator/TextProcessor.java b/02-structural/decorator/TextProcessor.java new file mode 100644 index 0000000..dffff0a --- /dev/null +++ b/02-structural/decorator/TextProcessor.java @@ -0,0 +1,9 @@ +package decorator; +/** + * Component — defines what all text processors do. + * Both the base implementation AND every decorator implement this. + * Sharing this type is what makes decorators stackable. + */ +public interface TextProcessor { + String process(String text); +} diff --git a/02-structural/decorator/TrimDecorator.java b/02-structural/decorator/TrimDecorator.java new file mode 100644 index 0000000..d98eda2 --- /dev/null +++ b/02-structural/decorator/TrimDecorator.java @@ -0,0 +1,9 @@ +package decorator; +/** Concrete Decorator: trims leading and trailing whitespace. */ +public class TrimDecorator extends TextDecorator { + public TrimDecorator(TextProcessor wrapped) { super(wrapped); } + @Override + public String process(String text) { + return super.process(text).trim(); + } +} diff --git a/02-structural/decorator/UpperCaseDecorator.java b/02-structural/decorator/UpperCaseDecorator.java new file mode 100644 index 0000000..7f49757 --- /dev/null +++ b/02-structural/decorator/UpperCaseDecorator.java @@ -0,0 +1,13 @@ +package decorator; +/** Concrete Decorator: converts result to upper case. */ +public class UpperCaseDecorator extends TextDecorator { + public UpperCaseDecorator(TextProcessor wrapped) { + super(wrapped); + } + @Override + public String process(String text) { + // 1. Get result from everything below in the stack + // 2. Apply OUR transformation: uppercase + return super.process(text).toUpperCase(); + } +} diff --git a/02-structural/facade/AudioMixer.java b/02-structural/facade/AudioMixer.java new file mode 100644 index 0000000..cd53159 --- /dev/null +++ b/02-structural/facade/AudioMixer.java @@ -0,0 +1,9 @@ +package facade; + +/** AudioMixer — normalises audio tracks after conversion. */ +class AudioMixer { + static VideoFile fix(VideoFile result) { + System.out.println(" AudioMixer: fixing audio tracks"); + return new VideoFile(result.getFilename()); + } +} diff --git a/02-structural/facade/BitrateReader.java b/02-structural/facade/BitrateReader.java new file mode 100644 index 0000000..2e2d6f8 --- /dev/null +++ b/02-structural/facade/BitrateReader.java @@ -0,0 +1,15 @@ +package facade; + +/** BitrateReader — reads the video buffer and converts it between codecs. */ +class BitrateReader { + static VideoFile read(VideoFile file, Codec codec) { + System.out.println(" BitrateReader: reading " + file.getFilename() + + " with codec " + codec.getName()); + return new VideoFile(file.getFilename()); + } + + static VideoFile convert(VideoFile buffer, Codec codec) { + System.out.println(" BitrateReader: converting to " + codec.getName()); + return new VideoFile(buffer.getFilename()); + } +} diff --git a/02-structural/facade/Codec.java b/02-structural/facade/Codec.java new file mode 100644 index 0000000..29748fc --- /dev/null +++ b/02-structural/facade/Codec.java @@ -0,0 +1,6 @@ +package facade; + +/** Codec — common interface implemented by each concrete compression codec. */ +interface Codec { + String getName(); +} diff --git a/02-structural/facade/CodecFactory.java b/02-structural/facade/CodecFactory.java new file mode 100644 index 0000000..3dd1d90 --- /dev/null +++ b/02-structural/facade/CodecFactory.java @@ -0,0 +1,10 @@ +package facade; + +/** CodecFactory — inspects a VideoFile and returns the matching Codec. */ +class CodecFactory { + static Codec extract(VideoFile file) { + System.out.println(" CodecFactory: extracting codec from " + file.getFilename()); + if ("mpeg4".equals(file.getCodecType())) return new MPEG4CompressionCodec(); + return new OggCompressionCodec(); + } +} diff --git a/02-structural/facade/MPEG4CompressionCodec.java b/02-structural/facade/MPEG4CompressionCodec.java new file mode 100644 index 0000000..d16d664 --- /dev/null +++ b/02-structural/facade/MPEG4CompressionCodec.java @@ -0,0 +1,6 @@ +package facade; + +/** Concrete Codec: MPEG-4 compression. */ +class MPEG4CompressionCodec implements Codec { + @Override public String getName() { return "mpeg4"; } +} diff --git a/02-structural/facade/Main.java b/02-structural/facade/Main.java new file mode 100644 index 0000000..d72de06 --- /dev/null +++ b/02-structural/facade/Main.java @@ -0,0 +1,30 @@ +package facade; + +/** + * Facade Design Pattern — Runnable Demo + * + * Demonstrates reducing a complex video conversion subsystem + * to a single method call via a Facade. + * + * Run: javac facade/*.java && java facade.Main + * Article: https://ankurm.com/facade-design-pattern-java/ + */ +public class Main { + + public static void main(String[] args) { + System.out.println("=== Facade Design Pattern Demo ===\n"); + + VideoConversionFacade converter = new VideoConversionFacade(); + + System.out.println("-- Client: just one method call --"); + String result1 = converter.convertVideo("holiday.ogg", "mp4"); + System.out.println("Output: " + result1); + + System.out.println(); + String result2 = converter.convertVideo("presentation.mp4", "ogg"); + System.out.println("Output: " + result2); + + System.out.println("\nClient code: 1 line. Subsystem: 6 classes. Facade hides the complexity."); + System.out.println("\n=== Demo complete ==="); + } +} diff --git a/02-structural/facade/OggCompressionCodec.java b/02-structural/facade/OggCompressionCodec.java new file mode 100644 index 0000000..c2abc0d --- /dev/null +++ b/02-structural/facade/OggCompressionCodec.java @@ -0,0 +1,6 @@ +package facade; + +/** Concrete Codec: Ogg compression. */ +class OggCompressionCodec implements Codec { + @Override public String getName() { return "ogg"; } +} diff --git a/02-structural/facade/README.md b/02-structural/facade/README.md new file mode 100644 index 0000000..0e32ae9 --- /dev/null +++ b/02-structural/facade/README.md @@ -0,0 +1,35 @@ +# Facade Design Pattern — Java Example + +**Pattern:** Structural → Facade +**Article:** https://ankurm.com/facade-design-pattern-java/ + +## What this example shows + +A video-conversion subsystem (`VideoFile`, `Codec`, `MPEG4CompressionCodec`, `OggCompressionCodec`, `CodecFactory`, `BitrateReader`, `AudioMixer`) is wrapped behind one class, `VideoConversionFacade`, that exposes a single `convertVideo()` method. Callers never touch the six subsystem classes directly, but they remain public and usable on their own for advanced use cases. + +## How to run + +```bash +javac facade/*.java -d out/facade +java -cp out/facade facade.Main +``` + +Requires Java 25. + +## Post Section ↔ File Mapping + +| Post Section | File(s) | +|---|---| +| The Subsystem (Complex, But Unchanged) — VideoFile | `VideoFile.java` | +| The Subsystem (Complex, But Unchanged) — Codec interface | `Codec.java` | +| The Subsystem (Complex, But Unchanged) — Concrete Codecs | `MPEG4CompressionCodec.java`, `OggCompressionCodec.java` | +| The Subsystem (Complex, But Unchanged) — CodecFactory | `CodecFactory.java` | +| The Subsystem (Complex, But Unchanged) — BitrateReader | `BitrateReader.java` | +| The Subsystem (Complex, But Unchanged) — AudioMixer | `AudioMixer.java` | +| The Facade | `VideoConversionFacade.java` | +| Client Code: One Line | `Main.java` | + +Note: the SLF4J `LoggerFactory`, JDBC `DriverManager`, and Spring `JdbcTemplate` snippets under "Facade in the JDK and Real Frameworks" are illustrative only — they are not part of this repository's runnable example. + +Article: https://ankurm.com/facade-design-pattern-java/ +All patterns: https://ankurm.com/design-patterns-java/ diff --git a/02-structural/facade/VideoConversionFacade.java b/02-structural/facade/VideoConversionFacade.java new file mode 100644 index 0000000..65c0daf --- /dev/null +++ b/02-structural/facade/VideoConversionFacade.java @@ -0,0 +1,39 @@ +package facade; + +/** + * Facade — the single, simple entry point to a complex video subsystem. + * + * Without this class, clients need to know about CodecFactory, + * BitrateReader, AudioMixer, and VideoFile — 4 classes, dozens of methods. + * The facade reduces that to ONE method call. + * + * The subsystem classes still exist and can be used directly + * by advanced users who need fine-grained control. + */ +public class VideoConversionFacade { + + public String convertVideo(String inputFile, String targetFormat) { + System.out.println("VideoConversionFacade: starting conversion of " + inputFile); + + // Step 1: open the file and detect codec + VideoFile file = new VideoFile(inputFile); + Codec sourceCodec = CodecFactory.extract(file); + + // Step 2: prepare destination codec + Codec destCodec; + if ("mp4".equals(targetFormat)) { + destCodec = new MPEG4CompressionCodec(); + } else { + destCodec = new OggCompressionCodec(); + } + + // Step 3: read, mix audio, encode + VideoFile buffer = BitrateReader.read(file, sourceCodec); + VideoFile intermediateResult = BitrateReader.convert(buffer, destCodec); + VideoFile result = AudioMixer.fix(intermediateResult); + + String outputFilename = inputFile.replaceAll("\\.[^.]+$", "." + targetFormat); + System.out.println("VideoConversionFacade: conversion complete -> " + outputFilename); + return outputFilename; + } +} diff --git a/02-structural/facade/VideoFile.java b/02-structural/facade/VideoFile.java new file mode 100644 index 0000000..f3d07ab --- /dev/null +++ b/02-structural/facade/VideoFile.java @@ -0,0 +1,23 @@ +package facade; + +/** + * Complex subsystem classes — these are what the Facade hides. + * Each class has its own complex API; clients shouldn't need to know all of them. + */ +class VideoFile { + private final String filename; + private final String codecType; + + VideoFile(String filename) { + this(filename, filename.endsWith(".mp4") ? "mpeg4" : "ogg"); + } + + VideoFile(String filename, String codec) { + this.filename = filename; + this.codecType = codec; + System.out.println(" VideoFile: " + filename + " [codec: " + codecType + "]"); + } + + public String getFilename() { return filename; } + public String getCodecType() { return codecType; } +} diff --git a/02-structural/flyweight/Main.java b/02-structural/flyweight/Main.java new file mode 100644 index 0000000..6442ade --- /dev/null +++ b/02-structural/flyweight/Main.java @@ -0,0 +1,50 @@ +package flyweight; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +/** + * Flyweight Design Pattern — Runnable Demo + * + * Creates 1000 trees of only 3 species. Without Flyweight: 1000 TreeType + * objects. With Flyweight: 3 TreeType objects (one per species), shared. + * + * Run: javac flyweight/*.java && java flyweight.Main + * Article: https://ankurm.com/flyweight-design-pattern-java/ + */ +public class Main { + + public static void main(String[] args) { + System.out.println("=== Flyweight Design Pattern Demo ===\n"); + + List forest = new ArrayList<>(); + Random rnd = new Random(42); + + String[][] treeSpecs = { + {"Oak", "dark-green", "rough-bark"}, + {"Pine", "blue-green", "needle-texture"}, + {"Birch","light-green","smooth-white-bark"} + }; + + System.out.println("Creating 1,000 trees (only 3 TreeType objects should be created):"); + for (int i = 0; i < 1000; i++) { + String[] spec = treeSpecs[i % 3]; + TreeType type = TreeFactory.getTreeType(spec[0], spec[1], spec[2]); + forest.add(new Tree(rnd.nextInt(800), rnd.nextInt(600), type)); + } + + System.out.println("\nForest created. Drawing first 5 trees:"); + for (int i = 0; i < 5; i++) { + forest.get(i).draw(); + } + + System.out.println("\n--- Memory summary ---"); + System.out.println("Trees in forest : " + forest.size()); + System.out.println("Unique TreeType objects in pool: " + TreeFactory.getPoolSize()); + System.out.println("Without Flyweight : 1,000 TreeType objects"); + System.out.println("With Flyweight : " + TreeFactory.getPoolSize() + " TreeType objects shared"); + + System.out.println("\n=== Demo complete ==="); + } +} diff --git a/02-structural/flyweight/README.md b/02-structural/flyweight/README.md new file mode 100644 index 0000000..5696845 --- /dev/null +++ b/02-structural/flyweight/README.md @@ -0,0 +1,31 @@ +# Flyweight Design Pattern — Java Example + +**Pattern:** Structural → Flyweight +**Article:** https://ankurm.com/flyweight-design-pattern-java/ + +## What this example shows + +A forest of 1,000 trees shares just 3 `TreeType` flyweight objects (one per species) instead of allocating a new heavy object per tree. `TreeType` holds the intrinsic (shared) state — name, color, texture. `Tree` holds only the extrinsic (unique) state — x/y position — plus a reference to its shared `TreeType`. `TreeFactory` is the pool manager: `Map.computeIfAbsent()` returns an existing flyweight or creates and caches one. + +## How to run + +```bash +javac flyweight/*.java -d out/flyweight +java -cp out/flyweight flyweight.Main +``` + +Requires Java 25. + +## Post Section ↔ File Mapping + +| Post Section | File(s) | +|---|---| +| Step 1 — The Flyweight (TreeType) | `TreeType.java` | +| Step 2 — The Factory (TreeFactory) | `TreeFactory.java` | +| Step 3 — The Context (Tree) | `Tree.java` | +| Putting the Forest Together | `Main.java` | + +Note: the "Flyweight in the JDK: String Pool and Integer Cache" and "Measuring the Benefit" snippets are illustrative only — they are not part of this repository's runnable example. + +Article: https://ankurm.com/flyweight-design-pattern-java/ +All patterns: https://ankurm.com/design-patterns-java/ diff --git a/02-structural/flyweight/Tree.java b/02-structural/flyweight/Tree.java new file mode 100644 index 0000000..09cf6f8 --- /dev/null +++ b/02-structural/flyweight/Tree.java @@ -0,0 +1,31 @@ +package flyweight; + +/** + * Context — stores the UNIQUE (extrinsic) state for each tree instance. + * This is NOT the flyweight itself; it's the lightweight object that + * holds position data and delegates rendering to a shared TreeType. + * + * 10,000 Tree objects × (x:4 bytes + y:4 bytes + reference:8 bytes) = ~160 KB + * vs. + * 10,000 Tree objects × (name + color + texture + x + y) = potentially MBs + */ +public class Tree { + + // Extrinsic (unique) state — different per tree + private final int x; + private final int y; + + // Reference to the SHARED flyweight + private final TreeType type; + + public Tree(int x, int y, TreeType type) { + this.x = x; + this.y = y; + this.type = type; + } + + public void draw() { + // Passes extrinsic state (position) into the shared flyweight + type.draw(x, y); + } +} diff --git a/02-structural/flyweight/TreeFactory.java b/02-structural/flyweight/TreeFactory.java new file mode 100644 index 0000000..3b14530 --- /dev/null +++ b/02-structural/flyweight/TreeFactory.java @@ -0,0 +1,29 @@ +package flyweight; + +import java.util.HashMap; +import java.util.Map; + +/** + * Flyweight Factory — the cache that ensures each unique TreeType + * is only created once, no matter how many trees use it. + * + * This is the piece that makes Flyweight work: + * it intercepts creation requests and returns an existing + * shared instance if one already exists. + */ +public class TreeFactory { + + // The pool of shared flyweights + private static final Map treeTypes = new HashMap<>(); + + public static TreeType getTreeType(String name, String color, String texture) { + String key = name + "_" + color + "_" + texture; + + // Only create a new TreeType if we haven't seen this combination before + return treeTypes.computeIfAbsent(key, k -> new TreeType(name, color, texture)); + } + + public static int getPoolSize() { + return treeTypes.size(); + } +} diff --git a/02-structural/flyweight/TreeType.java b/02-structural/flyweight/TreeType.java new file mode 100644 index 0000000..fd4f16f --- /dev/null +++ b/02-structural/flyweight/TreeType.java @@ -0,0 +1,29 @@ +package flyweight; + +/** + * Flyweight — holds the SHARED (intrinsic) state. + * TreeType is shared between all trees of the same species. + * + * If you have 10,000 oak trees, there's ONE OakType object in memory. + * Each individual tree only stores its unique position (extrinsic state). + */ +public class TreeType { + + // Intrinsic (shared) state — same for all trees of this species + private final String name; + private final String color; + private final String texture; // imagine a large texture bitmap here + + public TreeType(String name, String color, String texture) { + this.name = name; + this.color = color; + this.texture = texture; + System.out.println(" [TreeType created: " + name + "]"); // see how few are created + } + + // Extrinsic state (x, y) is passed IN at render time — NOT stored here + public void draw(int x, int y) { + System.out.printf(" Drawing %s tree [%s/%s] at (%d, %d)%n", + name, color, texture, x, y); + } +} diff --git a/02-structural/proxy/DatabaseConnection.java b/02-structural/proxy/DatabaseConnection.java new file mode 100644 index 0000000..f95f421 --- /dev/null +++ b/02-structural/proxy/DatabaseConnection.java @@ -0,0 +1,11 @@ +package proxy; + +/** + * Subject interface — defines what both the real object and proxy expose. + * Clients depend on this, not on the concrete class. + */ +public interface DatabaseConnection { + void connect(); + String executeQuery(String sql); + void disconnect(); +} diff --git a/02-structural/proxy/LazyConnectionProxy.java b/02-structural/proxy/LazyConnectionProxy.java new file mode 100644 index 0000000..9327a61 --- /dev/null +++ b/02-structural/proxy/LazyConnectionProxy.java @@ -0,0 +1,52 @@ +package proxy; + +/** + * Virtual Proxy — delays creating the RealDatabaseConnection until + * the first actual query is made. If no query is ever made (e.g., + * the service is initialized but never used in this request), + * the expensive connection is never opened. + * + * This is exactly how Hibernate proxies work: entities are not + * loaded from the database until you access a field on them. + */ +public class LazyConnectionProxy implements DatabaseConnection { + + private final String url; + private RealDatabaseConnection real; // null until first use + + public LazyConnectionProxy(String url) { + this.url = url; + System.out.println("[Proxy] Created for " + url + " (real connection NOT opened yet)"); + } + + // Lazy initialization — create and connect only on first real need + private void initIfNeeded() { + if (real == null) { + System.out.println("[Proxy] First access — initializing real connection..."); + real = new RealDatabaseConnection(url); + real.connect(); + } + } + + @Override + public void connect() { + // Proxy absorbs the connect() call — real connection opened lazily + System.out.println("[Proxy] connect() called — deferring to first query"); + } + + @Override + public String executeQuery(String sql) { + initIfNeeded(); // NOW we actually need the connection + return real.executeQuery(sql); + } + + @Override + public void disconnect() { + if (real != null) { + real.disconnect(); + real = null; + } else { + System.out.println("[Proxy] disconnect() called but connection was never opened"); + } + } +} diff --git a/02-structural/proxy/LoggingProxy.java b/02-structural/proxy/LoggingProxy.java new file mode 100644 index 0000000..ae0111d --- /dev/null +++ b/02-structural/proxy/LoggingProxy.java @@ -0,0 +1,41 @@ +package proxy; + +import java.time.Instant; + +/** + * Logging Proxy — adds timing and audit logging around every query + * without touching RealDatabaseConnection or any of its callers. + * + * This is the "cross-cutting concern" use case of Proxy, + * the same mechanism behind Spring AOP's @Around advice. + */ +public class LoggingProxy implements DatabaseConnection { + + private final DatabaseConnection target; + + public LoggingProxy(DatabaseConnection target) { + this.target = target; + } + + @Override + public void connect() { + System.out.println("[Log] connect() at " + Instant.now()); + target.connect(); + } + + @Override + public String executeQuery(String sql) { + long start = System.currentTimeMillis(); + System.out.println("[Log] QUERY START: " + sql); + String result = target.executeQuery(sql); + long elapsed = System.currentTimeMillis() - start; + System.out.println("[Log] QUERY END: " + elapsed + "ms | result: " + result); + return result; + } + + @Override + public void disconnect() { + System.out.println("[Log] disconnect() at " + Instant.now()); + target.disconnect(); + } +} diff --git a/02-structural/proxy/Main.java b/02-structural/proxy/Main.java new file mode 100644 index 0000000..cbd3978 --- /dev/null +++ b/02-structural/proxy/Main.java @@ -0,0 +1,51 @@ +package proxy; + +/** + * Proxy Design Pattern — Runnable Demo + * + * Shows two proxy types: + * 1. Virtual Proxy (lazy connection) + * 2. Logging Proxy (cross-cutting concern) + * 3. Proxy chaining (both together) + * + * Run: javac proxy/*.java && java proxy.Main + * Article: https://ankurm.com/proxy-design-pattern-java/ + */ +public class Main { + + public static void main(String[] args) throws InterruptedException { + System.out.println("=== Proxy Design Pattern Demo ===\n"); + + // --- Virtual Proxy: lazy connection --- + System.out.println("-- Virtual Proxy (lazy loading) --"); + DatabaseConnection lazy = new LazyConnectionProxy("jdbc:postgresql://localhost/mydb"); + lazy.connect(); // absorbed by proxy, no real connection yet + System.out.println("(no real connection yet — saved startup time)"); + System.out.println("Result: " + lazy.executeQuery("SELECT * FROM users WHERE id=1")); + System.out.println("Result: " + lazy.executeQuery("SELECT COUNT(*) FROM orders")); + lazy.disconnect(); + + System.out.println(); + + // --- Logging Proxy: wraps the real connection --- + System.out.println("-- Logging Proxy --"); + DatabaseConnection real = new RealDatabaseConnection("jdbc:mysql://localhost/shopdb"); + real.connect(); + DatabaseConnection logged = new LoggingProxy(real); + logged.executeQuery("SELECT * FROM products LIMIT 10"); + logged.disconnect(); + + System.out.println(); + + // --- Proxy chaining: lazy + logging --- + System.out.println("-- Chained Proxies: Lazy + Logging --"); + DatabaseConnection chain = + new LoggingProxy( + new LazyConnectionProxy("jdbc:oracle://localhost/warehouse")); + chain.connect(); + chain.executeQuery("SELECT SUM(quantity) FROM inventory"); + chain.disconnect(); + + System.out.println("\n=== Demo complete ==="); + } +} diff --git a/02-structural/proxy/README.md b/02-structural/proxy/README.md new file mode 100644 index 0000000..07f6610 --- /dev/null +++ b/02-structural/proxy/README.md @@ -0,0 +1,32 @@ +# Proxy Design Pattern — Java Example + +**Pattern:** Structural → Proxy +**Article:** https://ankurm.com/proxy-design-pattern-java/ + +## What this example shows + +A `DatabaseConnection` interface is implemented by a real, expensive connection (`RealDatabaseConnection`) and by two proxies that sit in front of it: `LazyConnectionProxy` defers opening the real connection until the first query actually runs (virtual proxy), and `LoggingProxy` wraps any `DatabaseConnection` — real or proxied — and adds timing/audit logging around every call (cross-cutting concern proxy). Because both proxies implement the same interface as the real subject, they compose: `Main` chains `LoggingProxy` around a `LazyConnectionProxy` to get lazy loading and logging together. + +## How to run + +```bash +javac proxy/*.java -d out/proxy +java -cp out/proxy proxy.Main +``` + +Requires Java 25. + +## Post Section ↔ File Mapping + +| Post Section | File(s) | +|---|---| +| The Subject Interface | `DatabaseConnection.java` | +| The Real Subject | `RealDatabaseConnection.java` | +| Virtual Proxy: Lazy Initialisation | `LazyConnectionProxy.java` | +| Logging Proxy: Cross-Cutting Concerns | `LoggingProxy.java` | +| Wiring It Together: Proxy Chaining | `Main.java` | + +Note: the "Dynamic Proxy with java.lang.reflect.Proxy" snippet is illustrative only — it is not part of this repository's runnable example. + +Article: https://ankurm.com/proxy-design-pattern-java/ +All patterns: https://ankurm.com/design-patterns-java/ diff --git a/02-structural/proxy/RealDatabaseConnection.java b/02-structural/proxy/RealDatabaseConnection.java new file mode 100644 index 0000000..ae2c75a --- /dev/null +++ b/02-structural/proxy/RealDatabaseConnection.java @@ -0,0 +1,33 @@ +package proxy; + +/** + * Real Subject — the actual, expensive database connection. + * Opening it takes time. We want to delay this until truly needed. + */ +public class RealDatabaseConnection implements DatabaseConnection { + + private final String url; + + public RealDatabaseConnection(String url) { + this.url = url; + } + + @Override + public void connect() { + System.out.println("[Real DB] Connecting to " + url + " (expensive operation)..."); + // Simulate connection setup time + try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } + System.out.println("[Real DB] Connected."); + } + + @Override + public String executeQuery(String sql) { + System.out.println("[Real DB] Executing: " + sql); + return "ResultSet{rows=42}"; // simulated result + } + + @Override + public void disconnect() { + System.out.println("[Real DB] Disconnecting from " + url); + } +} diff --git a/03-behavioral/chain-of-responsibility/CriticalIncidentTeam.java b/03-behavioral/chain-of-responsibility/CriticalIncidentTeam.java new file mode 100644 index 0000000..d1b0d0c --- /dev/null +++ b/03-behavioral/chain-of-responsibility/CriticalIncidentTeam.java @@ -0,0 +1,15 @@ +package chain; + +/** Handles CRITICAL tickets — all-hands incident response */ +public class CriticalIncidentTeam extends SupportHandler { + + @Override + protected boolean canHandle(SupportTicket ticket) { + return ticket.getPriority() == SupportTicket.Priority.CRITICAL; + } + + @Override + protected void handle(SupportTicket ticket) { + System.out.println(" [CRITICAL TEAM] All-hands war room opened: " + ticket); + } +} diff --git a/03-behavioral/chain-of-responsibility/Level1Support.java b/03-behavioral/chain-of-responsibility/Level1Support.java new file mode 100644 index 0000000..0185473 --- /dev/null +++ b/03-behavioral/chain-of-responsibility/Level1Support.java @@ -0,0 +1,15 @@ +package chain; + +/** Handles LOW priority tickets — basic FAQ and documentation responses */ +public class Level1Support extends SupportHandler { + + @Override + protected boolean canHandle(SupportTicket ticket) { + return ticket.getPriority() == SupportTicket.Priority.LOW; + } + + @Override + protected void handle(SupportTicket ticket) { + System.out.println(" [Level-1] Resolved with FAQ: " + ticket); + } +} diff --git a/03-behavioral/chain-of-responsibility/Level2Support.java b/03-behavioral/chain-of-responsibility/Level2Support.java new file mode 100644 index 0000000..2172501 --- /dev/null +++ b/03-behavioral/chain-of-responsibility/Level2Support.java @@ -0,0 +1,15 @@ +package chain; + +/** Handles MEDIUM priority tickets — technical troubleshooting */ +public class Level2Support extends SupportHandler { + + @Override + protected boolean canHandle(SupportTicket ticket) { + return ticket.getPriority() == SupportTicket.Priority.MEDIUM; + } + + @Override + protected void handle(SupportTicket ticket) { + System.out.println(" [Level-2] Diagnosed and fixed: " + ticket); + } +} diff --git a/03-behavioral/chain-of-responsibility/Level3Support.java b/03-behavioral/chain-of-responsibility/Level3Support.java new file mode 100644 index 0000000..ccf1898 --- /dev/null +++ b/03-behavioral/chain-of-responsibility/Level3Support.java @@ -0,0 +1,15 @@ +package chain; + +/** Handles HIGH priority tickets — senior engineers */ +public class Level3Support extends SupportHandler { + + @Override + protected boolean canHandle(SupportTicket ticket) { + return ticket.getPriority() == SupportTicket.Priority.HIGH; + } + + @Override + protected void handle(SupportTicket ticket) { + System.out.println(" [Level-3] Engineering deep-dive completed: " + ticket); + } +} diff --git a/03-behavioral/chain-of-responsibility/Main.java b/03-behavioral/chain-of-responsibility/Main.java new file mode 100644 index 0000000..3330d53 --- /dev/null +++ b/03-behavioral/chain-of-responsibility/Main.java @@ -0,0 +1,38 @@ +package chain; + +/** + * Chain of Responsibility Design Pattern — Runnable Demo + * + * A support ticket routing system where each handler + * either resolves a ticket or passes it up the chain. + * + * Run: javac chain/*.java -d out/chain && java -cp out/chain chain.Main + * Article: https://ankurm.com/chain-of-responsibility-design-pattern-java/ + */ +public class Main { + + public static void main(String[] args) { + System.out.println("=== Chain of Responsibility Demo ===\n"); + + // Build the chain: L1 -> L2 -> L3 -> Critical + SupportHandler l1 = new Level1Support(); + l1.setNext(new Level2Support()) + .setNext(new Level3Support()) + .setNext(new CriticalIncidentTeam()); + + SupportTicket[] tickets = { + new SupportTicket("Can't find the login button", SupportTicket.Priority.LOW), + new SupportTicket("API returns 500 on /checkout", SupportTicket.Priority.MEDIUM), + new SupportTicket("Database replication lag > 30s", SupportTicket.Priority.HIGH), + new SupportTicket("Complete payment system outage", SupportTicket.Priority.CRITICAL), + }; + + for (SupportTicket t : tickets) { + System.out.println("Ticket: " + t); + l1.handleRequest(t); + System.out.println(); + } + + System.out.println("=== Demo complete ==="); + } +} diff --git a/03-behavioral/chain-of-responsibility/README.md b/03-behavioral/chain-of-responsibility/README.md new file mode 100644 index 0000000..d2eb92d --- /dev/null +++ b/03-behavioral/chain-of-responsibility/README.md @@ -0,0 +1,31 @@ +# Chain of Responsibility Design Pattern — Java Example + +**Pattern:** Behavioral → Chain of Responsibility +**Article:** https://ankurm.com/chain-of-responsibility-design-pattern-java/ + +## What this example shows + +A support-ticket routing system: four handlers (`Level1Support`, `Level2Support`, `Level3Support`, `CriticalIncidentTeam`) are chained together via `SupportHandler.setNext()`. Each handler checks `canHandle()` for the ticket's priority — if it can't handle the ticket, it logs that it's passing the ticket up and delegates to the next handler in the chain. The sender (`Main`) only ever talks to the first handler; it has no idea which handler will ultimately resolve each ticket. + +## How to run + +```bash +javac chain-of-responsibility/*.java -d out/chain +java -cp out/chain chain.Main +``` + +Requires Java 25. + +## Post Section ↔ File Mapping + +| Post Section | File(s) | +|---|---| +| The Problem: Rigid Routing Logic | illustrative only — not part of this repository's runnable example | +| Implementation: Support Ticket Escalation — base handler | `SupportHandler.java` | +| Implementation: Support Ticket Escalation — the ticket | `SupportTicket.java` | +| Each concrete handler checks only its own responsibility — Level 1 & 2 | `Level1Support.java`, `Level2Support.java` | +| Level3Support and CriticalIncidentTeam follow exactly the same shape | `Level3Support.java`, `CriticalIncidentTeam.java` | +| The chain is assembled in one place | `Main.java` | + +Article: https://ankurm.com/chain-of-responsibility-design-pattern-java/ +All patterns: https://ankurm.com/design-patterns-java/ diff --git a/03-behavioral/chain-of-responsibility/SupportHandler.java b/03-behavioral/chain-of-responsibility/SupportHandler.java new file mode 100644 index 0000000..edd13f6 --- /dev/null +++ b/03-behavioral/chain-of-responsibility/SupportHandler.java @@ -0,0 +1,31 @@ +package chain; + +/** + * Handler interface — defines the chain contract. + * Each handler knows its next handler and can either + * handle the request itself or pass it along. + */ +public abstract class SupportHandler { + + private SupportHandler next; + + public SupportHandler setNext(SupportHandler next) { + this.next = next; + return next; // fluent API: h1.setNext(h2).setNext(h3) + } + + // Template method: subclasses implement handle(); base manages chaining + public final void handleRequest(SupportTicket ticket) { + if (canHandle(ticket)) { + handle(ticket); + } else if (next != null) { + System.out.println(" [" + getClass().getSimpleName() + "] passing up..."); + next.handleRequest(ticket); + } else { + System.out.println(" [UNHANDLED] No handler for: " + ticket); + } + } + + protected abstract boolean canHandle(SupportTicket ticket); + protected abstract void handle(SupportTicket ticket); +} diff --git a/03-behavioral/chain-of-responsibility/SupportTicket.java b/03-behavioral/chain-of-responsibility/SupportTicket.java new file mode 100644 index 0000000..5bce18a --- /dev/null +++ b/03-behavioral/chain-of-responsibility/SupportTicket.java @@ -0,0 +1,21 @@ +package chain; + +public class SupportTicket { + + public enum Priority { LOW, MEDIUM, HIGH, CRITICAL } + + private final String description; + private final Priority priority; + + public SupportTicket(String description, Priority priority) { + this.description = description; + this.priority = priority; + } + + public Priority getPriority() { return priority; } + + @Override + public String toString() { + return "[" + priority + "] " + description; + } +} diff --git a/03-behavioral/command/Command.java b/03-behavioral/command/Command.java new file mode 100644 index 0000000..3345d32 --- /dev/null +++ b/03-behavioral/command/Command.java @@ -0,0 +1,8 @@ +package command; + +/** Command interface: every action is an object */ +public interface Command { + void execute(); + void undo(); + String getDescription(); +} diff --git a/03-behavioral/command/CommandHistory.java b/03-behavioral/command/CommandHistory.java new file mode 100644 index 0000000..98abde2 --- /dev/null +++ b/03-behavioral/command/CommandHistory.java @@ -0,0 +1,28 @@ +package command; + +import java.util.ArrayDeque; +import java.util.Deque; + +/** + * Invoker — holds command history and triggers execute/undo. + * It doesn't know what the commands do; it just calls execute() and undo(). + */ +public class CommandHistory { + private final Deque history = new ArrayDeque<>(); + + public void execute(Command cmd) { + cmd.execute(); + history.push(cmd); + System.out.println(" Executed: " + cmd.getDescription()); + } + + public void undo() { + if (history.isEmpty()) { + System.out.println(" Nothing to undo."); + return; + } + Command cmd = history.pop(); + cmd.undo(); + System.out.println(" Undone: " + cmd.getDescription()); + } +} diff --git a/03-behavioral/command/DeleteCommand.java b/03-behavioral/command/DeleteCommand.java new file mode 100644 index 0000000..e18b86a --- /dev/null +++ b/03-behavioral/command/DeleteCommand.java @@ -0,0 +1,23 @@ +package command; + +public class DeleteCommand implements Command { + private final TextEditor editor; + private final int start; + private final int length; + private String deletedText; // saved for undo + + public DeleteCommand(TextEditor editor, int start, int length) { + this.editor = editor; + this.start = start; + this.length = length; + } + + @Override + public void execute() { + deletedText = editor.getText().substring(start, start + length); + editor.deleteText(start, length); + } + + @Override public void undo() { editor.insertText(deletedText, start); } + @Override public String getDescription() { return "Delete " + length + " chars at " + start; } +} diff --git a/03-behavioral/command/InsertCommand.java b/03-behavioral/command/InsertCommand.java new file mode 100644 index 0000000..88e63ae --- /dev/null +++ b/03-behavioral/command/InsertCommand.java @@ -0,0 +1,17 @@ +package command; + +public class InsertCommand implements Command { + private final TextEditor editor; + private final String text; + private final int position; + + public InsertCommand(TextEditor editor, String text, int position) { + this.editor = editor; + this.text = text; + this.position = position; + } + + @Override public void execute() { editor.insertText(text, position); } + @Override public void undo() { editor.deleteText(position, text.length()); } + @Override public String getDescription() { return "Insert \"" + text + "\" at " + position; } +} diff --git a/03-behavioral/command/Main.java b/03-behavioral/command/Main.java new file mode 100644 index 0000000..951bca1 --- /dev/null +++ b/03-behavioral/command/Main.java @@ -0,0 +1,40 @@ +package command; + +/** + * Command Design Pattern — Runnable Demo + * Run: javac command/*.java -d out/command && java -cp out/command command.Main + * Article: https://ankurm.com/command-design-pattern-java/ + */ +public class Main { + public static void main(String[] args) { + System.out.println("=== Command Design Pattern Demo ===\n"); + + TextEditor editor = new TextEditor(); + CommandHistory history = new CommandHistory(); + + System.out.println("Initial: " + editor); + + history.execute(new InsertCommand(editor, "Hello", 0)); + System.out.println("After: " + editor); + + history.execute(new InsertCommand(editor, " World", 5)); + System.out.println("After: " + editor); + + history.execute(new DeleteCommand(editor, 5, 6)); + System.out.println("After: " + editor); + + System.out.println("\n-- Undo sequence --"); + history.undo(); + System.out.println("After undo: " + editor); + + history.undo(); + System.out.println("After undo: " + editor); + + history.undo(); + System.out.println("After undo: " + editor); + + history.undo(); // nothing left + + System.out.println("\n=== Demo complete ==="); + } +} diff --git a/03-behavioral/command/README.md b/03-behavioral/command/README.md new file mode 100644 index 0000000..6990a20 --- /dev/null +++ b/03-behavioral/command/README.md @@ -0,0 +1,32 @@ +# Command Design Pattern — Java Example + +**Pattern:** Behavioral → Command +**Article:** https://ankurm.com/command-design-pattern-java/ + +## What this example shows + +A text editor with full undo support. `Command` declares `execute()`/`undo()`/`getDescription()`. `InsertCommand` and `DeleteCommand` are concrete commands that call back into the receiver, `TextEditor`. `DeleteCommand` captures the text it's about to delete *before* deleting it, so `undo()` can restore it. `CommandHistory` is the invoker: it pushes executed commands onto a stack and pops/undoes them on request, without knowing what any command actually does. + +## How to run + +```bash +javac command/*.java -d out/command +java -cp out/command command.Main +``` + +Requires Java 25. + +## Post Section ↔ File Mapping + +| Post Section | File(s) | +|---|---| +| Step 1 — Command Interface | `Command.java` | +| Step 2 — The Receiver (TextEditor) | `TextEditor.java` | +| Step 3 — Concrete Commands | `InsertCommand.java`, `DeleteCommand.java` | +| Step 4 — The Invoker (CommandHistory) | `CommandHistory.java` | +| Demo | `Main.java` | + +Note: the `MacroCommand` snippet (Composite applied to Command) and the `Runnable`/`Callable`/`CompletableFuture` JDK snippets are illustrative only — they are not part of this repository's runnable example. + +Article: https://ankurm.com/command-design-pattern-java/ +All patterns: https://ankurm.com/design-patterns-java/ diff --git a/03-behavioral/command/TextEditor.java b/03-behavioral/command/TextEditor.java new file mode 100644 index 0000000..19a1927 --- /dev/null +++ b/03-behavioral/command/TextEditor.java @@ -0,0 +1,21 @@ +package command; + +/** + * Receiver — contains the actual text editing logic. + * The commands call methods on this object. + */ +public class TextEditor { + private final StringBuilder text = new StringBuilder(); + + public void insertText(String content, int position) { + text.insert(position, content); + } + + public void deleteText(int start, int length) { + text.delete(start, start + length); + } + + public String getText() { return text.toString(); } + + @Override public String toString() { return "Editor[\"" + text + "\"]"; } +} diff --git a/03-behavioral/interpreter/AddExpression.java b/03-behavioral/interpreter/AddExpression.java new file mode 100644 index 0000000..ee6df07 --- /dev/null +++ b/03-behavioral/interpreter/AddExpression.java @@ -0,0 +1,19 @@ +package interpreter; + +/** + * NonTerminalExpression — addition: left + right. + */ +public class AddExpression implements Expression { + private final Expression left; + private final Expression right; + + public AddExpression(Expression left, Expression right) { + this.left = left; + this.right = right; + } + + @Override + public int interpret() { + return left.interpret() + right.interpret(); + } +} diff --git a/03-behavioral/interpreter/Expression.java b/03-behavioral/interpreter/Expression.java new file mode 100644 index 0000000..7500056 --- /dev/null +++ b/03-behavioral/interpreter/Expression.java @@ -0,0 +1,9 @@ +package interpreter; + +/** + * AbstractExpression — declares the interpret operation. + * All terminal and non-terminal expressions implement this. + */ +public interface Expression { + int interpret(); +} diff --git a/03-behavioral/interpreter/Main.java b/03-behavioral/interpreter/Main.java new file mode 100644 index 0000000..aa14053 --- /dev/null +++ b/03-behavioral/interpreter/Main.java @@ -0,0 +1,38 @@ +package interpreter; + +public class Main { + public static void main(String[] args) { + // (5 + 3) * 2 → 16 + Expression expr1 = new MultiplyExpression( + new AddExpression( + new NumberExpression(5), + new NumberExpression(3) + ), + new NumberExpression(2) + ); + System.out.println("(5 + 3) * 2 = " + expr1.interpret()); + + // 10 - (4 + 2) → 4 + Expression expr2 = new SubtractExpression( + new NumberExpression(10), + new AddExpression( + new NumberExpression(4), + new NumberExpression(2) + ) + ); + System.out.println("10 - (4 + 2) = " + expr2.interpret()); + + // (3 * 4) + (10 - 6) → 16 + Expression expr3 = new AddExpression( + new MultiplyExpression( + new NumberExpression(3), + new NumberExpression(4) + ), + new SubtractExpression( + new NumberExpression(10), + new NumberExpression(6) + ) + ); + System.out.println("(3 * 4) + (10 - 6) = " + expr3.interpret()); + } +} diff --git a/03-behavioral/interpreter/MultiplyExpression.java b/03-behavioral/interpreter/MultiplyExpression.java new file mode 100644 index 0000000..6fb4102 --- /dev/null +++ b/03-behavioral/interpreter/MultiplyExpression.java @@ -0,0 +1,19 @@ +package interpreter; + +/** + * NonTerminalExpression — multiplication: left * right. + */ +public class MultiplyExpression implements Expression { + private final Expression left; + private final Expression right; + + public MultiplyExpression(Expression left, Expression right) { + this.left = left; + this.right = right; + } + + @Override + public int interpret() { + return left.interpret() * right.interpret(); + } +} diff --git a/03-behavioral/interpreter/NumberExpression.java b/03-behavioral/interpreter/NumberExpression.java new file mode 100644 index 0000000..c0367b7 --- /dev/null +++ b/03-behavioral/interpreter/NumberExpression.java @@ -0,0 +1,18 @@ +package interpreter; + +/** + * TerminalExpression — a number literal. + * Leaf node in the AST; has no child expressions. + */ +public class NumberExpression implements Expression { + private final int number; + + public NumberExpression(int number) { + this.number = number; + } + + @Override + public int interpret() { + return number; + } +} diff --git a/03-behavioral/interpreter/README.md b/03-behavioral/interpreter/README.md new file mode 100644 index 0000000..c8db249 --- /dev/null +++ b/03-behavioral/interpreter/README.md @@ -0,0 +1,31 @@ +# Interpreter Design Pattern — Java Example + +**Pattern:** Behavioral → Interpreter +**Article:** https://ankurm.com/interpreter-design-pattern-java/ + +## What this example shows + +A math expression evaluator built as an abstract syntax tree. `Expression` declares `interpret()` — every AST node implements it. `NumberExpression` is the only terminal: a leaf with no children that returns its value directly. `AddExpression`, `SubtractExpression`, and `MultiplyExpression` are non-terminals: each holds two child expressions and applies its operator after recursively interpreting them. `Main` is the client — it assembles three small trees by hand (`(5 + 3) * 2`, `10 - (4 + 2)`, `(3 * 4) + (10 - 6)`) and evaluates each by calling `interpret()` on the root. + +## How to run + +```bash +javac interpreter/*.java -d out/interpreter +java -cp out/interpreter interpreter.Main +``` + +Requires Java 25. + +## Post Section ↔ File Mapping + +| Post Section | File(s) | +|---|---| +| Implementation: Math Expression Evaluator — the AbstractExpression | `Expression.java` | +| Implementation: Math Expression Evaluator — the TerminalExpression | `NumberExpression.java` | +| Implementation: Math Expression Evaluator — AddExpression | `AddExpression.java` | +| Implementation: Math Expression Evaluator — SubtractExpression | `SubtractExpression.java` | +| Implementation: Math Expression Evaluator — MultiplyExpression | `MultiplyExpression.java` | +| Implementation: Math Expression Evaluator — wiring it together | `Main.java` | + +Article: https://ankurm.com/interpreter-design-pattern-java/ +All patterns: https://ankurm.com/design-patterns-java/ diff --git a/03-behavioral/interpreter/SubtractExpression.java b/03-behavioral/interpreter/SubtractExpression.java new file mode 100644 index 0000000..00ff14c --- /dev/null +++ b/03-behavioral/interpreter/SubtractExpression.java @@ -0,0 +1,19 @@ +package interpreter; + +/** + * NonTerminalExpression — subtraction: left - right. + */ +public class SubtractExpression implements Expression { + private final Expression left; + private final Expression right; + + public SubtractExpression(Expression left, Expression right) { + this.left = left; + this.right = right; + } + + @Override + public int interpret() { + return left.interpret() - right.interpret(); + } +} diff --git a/03-behavioral/iterator/Book.java b/03-behavioral/iterator/Book.java new file mode 100644 index 0000000..ea51a39 --- /dev/null +++ b/03-behavioral/iterator/Book.java @@ -0,0 +1,14 @@ +package iterator; + +public class Book { + private final String title; + private final String author; + private final int year; + + public Book(String title, String author, int year) { + this.title = title; this.author = author; this.year = year; + } + public String getTitle() { return title; } + public int getYear() { return year; } + @Override public String toString() { return "\"" + title + "\" by " + author + " (" + year + ")"; } +} diff --git a/03-behavioral/iterator/BookIterator.java b/03-behavioral/iterator/BookIterator.java new file mode 100644 index 0000000..9e31514 --- /dev/null +++ b/03-behavioral/iterator/BookIterator.java @@ -0,0 +1,7 @@ +package iterator; + +/** Our custom Iterator interface (mirrors java.util.Iterator) */ +public interface BookIterator { + boolean hasNext(); + Book next(); +} diff --git a/03-behavioral/iterator/BookShelf.java b/03-behavioral/iterator/BookShelf.java new file mode 100644 index 0000000..14bf2d6 --- /dev/null +++ b/03-behavioral/iterator/BookShelf.java @@ -0,0 +1,57 @@ +package iterator; + +import java.util.ArrayList; +import java.util.List; + +/** + * Aggregate — the collection. Exposes iterators without + * revealing its internal storage structure. + */ +public class BookShelf { + private final List books = new ArrayList<>(); + + public void addBook(Book book) { books.add(book); } + + /** Standard forward iterator */ + public BookIterator iterator() { + return new ForwardIterator(); + } + + /** Filtered iterator — only books from a specific decade */ + public BookIterator iteratorByDecade(int decade) { + return new DecadeIterator(decade); + } + + // --- Inner iterator implementations --- + + private class ForwardIterator implements BookIterator { + private int index = 0; + + @Override public boolean hasNext() { return index < books.size(); } + @Override public Book next() { return books.get(index++); } + } + + private class DecadeIterator implements BookIterator { + private final int decade; + private int index = 0; + private Book nextBook; + + DecadeIterator(int decade) { + this.decade = decade; + advance(); + } + + private void advance() { + nextBook = null; + while (index < books.size()) { + Book b = books.get(index++); + if (b.getYear() / 10 * 10 == decade) { nextBook = b; break; } + } + } + + @Override public boolean hasNext() { return nextBook != null; } + @Override public Book next() { + Book b = nextBook; advance(); return b; + } + } +} diff --git a/03-behavioral/iterator/Main.java b/03-behavioral/iterator/Main.java new file mode 100644 index 0000000..5e73732 --- /dev/null +++ b/03-behavioral/iterator/Main.java @@ -0,0 +1,37 @@ +package iterator; + +/** + * Iterator Design Pattern — Runnable Demo + * Run: javac iterator/*.java -d out/iterator && java -cp out/iterator iterator.Main + * Article: https://ankurm.com/iterator-design-pattern-java/ + */ +public class Main { + public static void main(String[] args) { + System.out.println("=== Iterator Design Pattern Demo ===\n"); + + BookShelf shelf = new BookShelf(); + shelf.addBook(new Book("Clean Code", "Robert Martin", 2008)); + shelf.addBook(new Book("The Pragmatic Programmer","Andrew Hunt", 1999)); + shelf.addBook(new Book("Effective Java", "Joshua Bloch", 2001)); + shelf.addBook(new Book("Design Patterns", "Gang of Four", 1994)); + shelf.addBook(new Book("Refactoring", "Martin Fowler", 2018)); + shelf.addBook(new Book("Working Effectively with Legacy Code", "Michael Feathers", 2004)); + + System.out.println("-- All books (forward iterator) --"); + BookIterator it = shelf.iterator(); + while (it.hasNext()) { + System.out.println(" " + it.next()); + } + + System.out.println("\n-- Books from the 2000s --"); + BookIterator it2000s = shelf.iteratorByDecade(2000); + while (it2000s.hasNext()) { + System.out.println(" " + it2000s.next()); + } + + System.out.println("\n-- JDK Iterable: same pattern, different vocabulary --"); + System.out.println(" java.util.Iterator is our BookIterator; for-each uses it under the hood"); + + System.out.println("\n=== Demo complete ==="); + } +} diff --git a/03-behavioral/iterator/README.md b/03-behavioral/iterator/README.md new file mode 100644 index 0000000..5ae040b --- /dev/null +++ b/03-behavioral/iterator/README.md @@ -0,0 +1,31 @@ +# Iterator Design Pattern — Java Example + +**Pattern:** Behavioral → Iterator +**Article:** https://ankurm.com/iterator-design-pattern-java/ + +## What this example shows + +A `BookShelf` stores `Book` objects in an internal `ArrayList` but never exposes it. Callers only see the `BookIterator` interface (`hasNext()`/`next()`). `BookShelf` hands out two different concrete iterators: `ForwardIterator` walks every book in order, and `DecadeIterator` filters to only books published in a given decade, computing the next match lazily as `advance()` is called. `Main` exercises both iterators, then prints a line noting that `java.util.Iterator`/for-each work the same way under the hood. + +## How to run + +```bash +javac iterator/*.java -d out/iterator +java -cp out/iterator iterator.Main +``` + +Requires Java 25. + +## Post Section ↔ File Mapping + +| Post Section | File(s) | +|---|---| +| Custom Iterator Implementation — the Iterator interface | `BookIterator.java` | +| Custom Iterator Implementation — the element type | `Book.java` | +| Custom Iterator Implementation — the Aggregate (BookShelf, ForwardIterator, DecadeIterator) | `BookShelf.java` | +| Custom Iterator Implementation — wiring it together | `Main.java` | + +Note: the "How Java's For-Each Loop Uses Iterator" snippet (`Iterable` example) is illustrative only — it is not part of this repository's runnable example. + +Article: https://ankurm.com/iterator-design-pattern-java/ +All patterns: https://ankurm.com/design-patterns-java/ diff --git a/03-behavioral/mediator/ChatMediator.java b/03-behavioral/mediator/ChatMediator.java new file mode 100644 index 0000000..2564788 --- /dev/null +++ b/03-behavioral/mediator/ChatMediator.java @@ -0,0 +1,7 @@ +package mediator; + +/** Mediator interface — defines how colleagues communicate through the hub */ +public interface ChatMediator { + void sendMessage(String message, User sender); + void addUser(User user); +} diff --git a/03-behavioral/mediator/ChatRoom.java b/03-behavioral/mediator/ChatRoom.java new file mode 100644 index 0000000..3a9a578 --- /dev/null +++ b/03-behavioral/mediator/ChatRoom.java @@ -0,0 +1,27 @@ +package mediator; + +import java.util.ArrayList; +import java.util.List; + +/** + * Concrete Mediator — the chat room that routes messages between users. + * Users talk to ChatRoom; ChatRoom talks to users. Nobody else talks to anybody. + */ +public class ChatRoom implements ChatMediator { + private final List users = new ArrayList<>(); + + @Override + public void addUser(User user) { + users.add(user); + System.out.println(" [ChatRoom] " + user.getName() + " joined the room"); + } + + @Override + public void sendMessage(String message, User sender) { + for (User user : users) { + if (user != sender) { // don't echo back to sender + user.receive(message, sender.getName()); + } + } + } +} diff --git a/03-behavioral/mediator/Main.java b/03-behavioral/mediator/Main.java new file mode 100644 index 0000000..51d0201 --- /dev/null +++ b/03-behavioral/mediator/Main.java @@ -0,0 +1,35 @@ +package mediator; + +/** + * Mediator Design Pattern — Runnable Demo + * Run: javac mediator/*.java -d out/mediator && java -cp out/mediator mediator.Main + * Article: https://ankurm.com/mediator-design-pattern-java/ + */ +public class Main { + public static void main(String[] args) { + System.out.println("=== Mediator Design Pattern Demo ===\n"); + + ChatRoom room = new ChatRoom(); + + User alice = new User("Alice", room); + User bob = new User("Bob", room); + User carol = new User("Carol", room); + + room.addUser(alice); + room.addUser(bob); + room.addUser(carol); + + System.out.println(); + alice.send("Hey everyone!"); + System.out.println(); + bob.send("Hi Alice and Carol!"); + System.out.println(); + carol.send("Good morning!"); + + System.out.println("\n-- Connections without Mediator: " + 3 + " users need " + (3 * 2) + " direct links --"); + System.out.println("-- With Mediator: " + 3 + " users each connect only to the room --"); + System.out.println("-- With N users: O(N) connections instead of O(N²) --"); + + System.out.println("\n=== Demo complete ==="); + } +} diff --git a/03-behavioral/mediator/README.md b/03-behavioral/mediator/README.md new file mode 100644 index 0000000..6f4dcd4 --- /dev/null +++ b/03-behavioral/mediator/README.md @@ -0,0 +1,31 @@ +# Mediator Design Pattern — Java Example + +**Pattern:** Behavioral → Mediator +**Article:** https://ankurm.com/mediator-design-pattern-java/ + +## What this example shows + +A chat room where users never talk to each other directly. `ChatMediator` declares the hub contract (`sendMessage()`/`addUser()`). `User` is a colleague — it knows only the mediator, never other users. `ChatRoom` is the concrete mediator: it holds the list of users and routes every message to everyone except the sender. `Main` adds three users and has each one broadcast a message, then prints the O(N) vs O(N²) connection-count comparison. + +## How to run + +```bash +javac mediator/*.java -d out/mediator +java -cp out/mediator mediator.Main +``` + +Requires Java 25. + +## Post Section ↔ File Mapping + +| Post Section | File(s) | +|---|---| +| Implementation: Chat Room — the Mediator interface | `ChatMediator.java` | +| Implementation: Chat Room — the Colleague | `User.java` | +| Implementation: Chat Room — the Concrete Mediator | `ChatRoom.java` | +| Implementation: Chat Room — wiring it together | `Main.java` | + +Note: the "Mediator in Practice: MVC and Spring Events" snippet (`ApplicationEventPublisher`/`@EventListener` example) is illustrative only — it is not part of this repository's runnable example. + +Article: https://ankurm.com/mediator-design-pattern-java/ +All patterns: https://ankurm.com/design-patterns-java/ diff --git a/03-behavioral/mediator/User.java b/03-behavioral/mediator/User.java new file mode 100644 index 0000000..166eb9a --- /dev/null +++ b/03-behavioral/mediator/User.java @@ -0,0 +1,26 @@ +package mediator; + +/** + * Colleague — knows only the mediator, not other users. + * Sends messages through the mediator; receives via receive(). + */ +public class User { + private final String name; + private final ChatMediator mediator; + + public User(String name, ChatMediator mediator) { + this.name = name; + this.mediator = mediator; + } + + public String getName() { return name; } + + public void send(String message) { + System.out.println("[" + name + "] sends: " + message); + mediator.sendMessage(message, this); + } + + public void receive(String message, String from) { + System.out.println(" [" + name + "] received from " + from + ": " + message); + } +} diff --git a/03-behavioral/memento/Editor.java b/03-behavioral/memento/Editor.java new file mode 100644 index 0000000..85671d1 --- /dev/null +++ b/03-behavioral/memento/Editor.java @@ -0,0 +1,43 @@ +package memento; + +/** + * Originator — the text editor whose state we're saving and restoring. + * Creates mementos and restores from them; no history management here. + */ +public class Editor { + private String content = ""; + private int cursorPosition = 0; + private String selectedText = ""; + + public void type(String text) { + content = content.substring(0, cursorPosition) + text + content.substring(cursorPosition); + cursorPosition += text.length(); + } + + public void selectText(int start, int end) { + this.selectedText = content.substring(start, end); + System.out.println(" Selected: \"" + selectedText + "\""); + } + + public void deleteSelection() { + content = content.replace(selectedText, ""); + selectedText = ""; + } + + /** Create a snapshot of current state */ + public EditorMemento save() { + return new EditorMemento(content, cursorPosition, selectedText); + } + + /** Restore state from a snapshot */ + public void restore(EditorMemento memento) { + this.content = memento.getContent(); + this.cursorPosition = memento.getCursorPosition(); + this.selectedText = memento.getSelectedText(); + } + + @Override + public String toString() { + return "Editor[\"" + content + "\", cursor=" + cursorPosition + "]"; + } +} diff --git a/03-behavioral/memento/EditorMemento.java b/03-behavioral/memento/EditorMemento.java new file mode 100644 index 0000000..f07fe01 --- /dev/null +++ b/03-behavioral/memento/EditorMemento.java @@ -0,0 +1,28 @@ +package memento; + +/** + * Memento — a snapshot of the editor's state. + * Immutable: once created, the state cannot be changed. + * The Caretaker holds these; the Originator creates and restores from them. + */ +public final class EditorMemento { + private final String content; + private final int cursorPosition; + private final String selectedText; + + EditorMemento(String content, int cursorPosition, String selectedText) { + this.content = content; + this.cursorPosition = cursorPosition; + this.selectedText = selectedText; + } + + // Package-private: only the Editor (Originator) should read the state back + String getContent() { return content; } + int getCursorPosition() { return cursorPosition; } + String getSelectedText() { return selectedText; } + + @Override + public String toString() { + return "Snapshot[content=\"" + content + "\", cursor=" + cursorPosition + "]"; + } +} diff --git a/03-behavioral/memento/History.java b/03-behavioral/memento/History.java new file mode 100644 index 0000000..9551091 --- /dev/null +++ b/03-behavioral/memento/History.java @@ -0,0 +1,24 @@ +package memento; + +import java.util.ArrayDeque; +import java.util.Deque; + +/** + * Caretaker — manages the stack of mementos. + * It does NOT open or inspect the mementos; it just stores and returns them. + */ +public class History { + private final Deque snapshots = new ArrayDeque<>(); + + public void save(EditorMemento memento) { + snapshots.push(memento); + System.out.println(" [History] Saved: " + memento); + } + + public EditorMemento undo() { + if (snapshots.isEmpty()) return null; + return snapshots.pop(); + } + + public int size() { return snapshots.size(); } +} diff --git a/03-behavioral/memento/Main.java b/03-behavioral/memento/Main.java new file mode 100644 index 0000000..22c62be --- /dev/null +++ b/03-behavioral/memento/Main.java @@ -0,0 +1,38 @@ +package memento; + +/** + * Memento Design Pattern — Runnable Demo + * Run: javac memento/*.java -d out/memento && java -cp out/memento memento.Main + * Article: https://ankurm.com/memento-design-pattern-java/ + */ +public class Main { + public static void main(String[] args) { + System.out.println("=== Memento Design Pattern Demo ===\n"); + + Editor editor = new Editor(); + History history = new History(); + + System.out.println("Initial: " + editor); + + editor.type("Hello"); + history.save(editor.save()); + System.out.println("After type: " + editor); + + editor.type(", World"); + history.save(editor.save()); + System.out.println("After type: " + editor); + + editor.type("! How are you?"); + System.out.println("After type: " + editor); + + System.out.println("\n-- Undo --"); + editor.restore(history.undo()); + System.out.println("After undo: " + editor); + + editor.restore(history.undo()); + System.out.println("After undo: " + editor); + + System.out.println("\nHistory empty: " + (history.size() == 0)); + System.out.println("\n=== Demo complete ==="); + } +} diff --git a/03-behavioral/memento/README.md b/03-behavioral/memento/README.md new file mode 100644 index 0000000..f2d3b9c --- /dev/null +++ b/03-behavioral/memento/README.md @@ -0,0 +1,31 @@ +# Memento Design Pattern — Java Example + +**Pattern:** Behavioral → Memento +**Article:** https://ankurm.com/memento-design-pattern-java/ + +## What this example shows + +A text editor with undo support that doesn't leak its internal state. `EditorMemento` is an immutable snapshot with package-private accessors — only `Editor` (in the same package) can read it back; everyone else can only call `toString()`. `Editor` is the originator: it packs its fields into a memento on `save()` and unpacks them on `restore()`. `History` is the caretaker: it pushes and pops mementos on a stack without ever inspecting their contents. `Main` types text, saves checkpoints after most edits (deliberately skipping one, to show it's lost on undo), then undoes twice. + +## How to run + +```bash +javac memento/*.java -d out/memento +java -cp out/memento memento.Main +``` + +Requires Java 25. + +## Post Section ↔ File Mapping + +| Post Section | File(s) | +|---|---| +| The Memento: Immutable and Opaque | `EditorMemento.java` | +| The Originator (Editor) | `Editor.java` | +| The Caretaker (History) | `History.java` | +| Wiring it together (Main demo) | `Main.java` | + +Note: the "Serialization as Memento" snippet (`ObjectOutputStream`/`ObjectInputStream` example) is illustrative only — it is not part of this repository's runnable example. + +Article: https://ankurm.com/memento-design-pattern-java/ +All patterns: https://ankurm.com/design-patterns-java/ diff --git a/03-behavioral/observer/AlertObserver.java b/03-behavioral/observer/AlertObserver.java new file mode 100644 index 0000000..6c5cd35 --- /dev/null +++ b/03-behavioral/observer/AlertObserver.java @@ -0,0 +1,22 @@ +package observer; + +/** Fires an alert when the price changes by more than a threshold % */ +public class AlertObserver implements StockObserver { + private final String name; + private final double thresholdPercent; + + public AlertObserver(String name, double thresholdPercent) { + this.name = name; + this.thresholdPercent = thresholdPercent; + } + + @Override + public void onPriceChanged(String ticker, double oldPrice, double newPrice) { + double change = Math.abs((newPrice - oldPrice) / oldPrice * 100); + if (change >= thresholdPercent) { + System.out.printf(" [ALERT:%s] %s moved %.1f%% — ALERT TRIGGERED%n", name, ticker, change); + } else { + System.out.printf(" [Alert:%s] %s moved %.1f%% — within threshold%n", name, ticker, change); + } + } +} diff --git a/03-behavioral/observer/Main.java b/03-behavioral/observer/Main.java new file mode 100644 index 0000000..865398d --- /dev/null +++ b/03-behavioral/observer/Main.java @@ -0,0 +1,36 @@ +package observer; + +/** + * Observer Design Pattern — Runnable Demo + * Run: javac observer/*.java -d out/observer && java -cp out/observer observer.Main + * Article: https://ankurm.com/observer-design-pattern-java/ + */ +public class Main { + public static void main(String[] args) { + System.out.println("=== Observer Design Pattern Demo ===\n"); + + StockMarket aapl = new StockMarket("AAPL", 175.00); + + StockObserver alert = new AlertObserver("RiskEngine", 2.0); + StockObserver alice = new PortfolioObserver("Alice", 100); + StockObserver bob = new PortfolioObserver("Bob", 50); + + aapl.subscribe(alert); + aapl.subscribe(alice); + aapl.subscribe(bob); + + System.out.println(); + aapl.setPrice(178.50); // +2% — should trigger alert + + System.out.println(); + aapl.setPrice(179.00); // small move + + System.out.println("\n-- Bob unsubscribes --"); + aapl.unsubscribe(bob); + + System.out.println(); + aapl.setPrice(165.00); // big drop — Bob doesn't hear it + + System.out.println("\n=== Demo complete ==="); + } +} diff --git a/03-behavioral/observer/PortfolioObserver.java b/03-behavioral/observer/PortfolioObserver.java new file mode 100644 index 0000000..3aca994 --- /dev/null +++ b/03-behavioral/observer/PortfolioObserver.java @@ -0,0 +1,19 @@ +package observer; + +/** Updates portfolio value whenever a held stock changes price */ +public class PortfolioObserver implements StockObserver { + private final String ownerName; + private final int sharesHeld; + + public PortfolioObserver(String ownerName, int sharesHeld) { + this.ownerName = ownerName; + this.sharesHeld = sharesHeld; + } + + @Override + public void onPriceChanged(String ticker, double oldPrice, double newPrice) { + double gain = (newPrice - oldPrice) * sharesHeld; + System.out.printf(" [Portfolio:%s] %s×%d P&L change: %+.2f%n", + ownerName, ticker, sharesHeld, gain); + } +} diff --git a/03-behavioral/observer/README.md b/03-behavioral/observer/README.md new file mode 100644 index 0000000..d354296 --- /dev/null +++ b/03-behavioral/observer/README.md @@ -0,0 +1,32 @@ +# Observer Design Pattern — Java Example + +**Pattern:** Behavioral → Observer +**Article:** https://ankurm.com/observer-design-pattern-java/ + +## What this example shows + +A stock market subject that notifies subscribers without knowing what they do with the notification. `StockObserver` is the one-method interface every subscriber implements. `StockMarket` is the subject: it holds the observer list and pushes both the old and new price to every subscriber whenever `setPrice()` is called. `AlertObserver` and `PortfolioObserver` are two concrete observers that react completely differently to the same notification — one watches for threshold breaches, the other computes a P&L delta. `Main` subscribes all three, moves the price twice, then unsubscribes Bob before a third move so his portfolio observer never sees it. + +## How to run + +```bash +javac observer/*.java -d out/observer +java -cp out/observer observer.Main +``` + +Requires Java 25. + +## Post Section ↔ File Mapping + +| Post Section | File(s) | +|---|---| +| Implementation: Stock Price Monitoring — the Observer interface | `StockObserver.java` | +| Implementation: Stock Price Monitoring — the Subject | `StockMarket.java` | +| Implementation: Stock Price Monitoring — AlertObserver | `AlertObserver.java` | +| Implementation: Stock Price Monitoring — PortfolioObserver | `PortfolioObserver.java` | +| Implementation: Stock Price Monitoring — wiring it together | `Main.java` | + +Note: the "Push vs Pull Notification Models" pull-model snippet (`PullAlertObserver`) and the "Thread Safety" `CopyOnWriteArrayList` snippet are illustrative only — they are not part of this repository's runnable example. + +Article: https://ankurm.com/observer-design-pattern-java/ +All patterns: https://ankurm.com/design-patterns-java/ diff --git a/03-behavioral/observer/StockMarket.java b/03-behavioral/observer/StockMarket.java new file mode 100644 index 0000000..33ddf68 --- /dev/null +++ b/03-behavioral/observer/StockMarket.java @@ -0,0 +1,36 @@ +package observer; + +import java.util.ArrayList; +import java.util.List; + +/** + * Subject (Observable) — maintains a list of observers and notifies them + * when the stock price changes. Observers register and deregister freely. + */ +public class StockMarket { + private final String ticker; + private double price; + private final List observers = new ArrayList<>(); + + public StockMarket(String ticker, double initialPrice) { + this.ticker = ticker; + this.price = initialPrice; + System.out.println("[Market] " + ticker + " initialised at $" + initialPrice); + } + + public void subscribe(StockObserver observer) { observers.add(observer); } + public void unsubscribe(StockObserver observer) { observers.remove(observer); } + + public void setPrice(double newPrice) { + double old = this.price; + this.price = newPrice; + System.out.printf("[Market] %s price: $%.2f -> $%.2f%n", ticker, old, newPrice); + notifyObservers(old, newPrice); + } + + private void notifyObservers(double old, double newPrice) { + for (StockObserver o : observers) { + o.onPriceChanged(ticker, old, newPrice); + } + } +} diff --git a/03-behavioral/observer/StockObserver.java b/03-behavioral/observer/StockObserver.java new file mode 100644 index 0000000..4403d8d --- /dev/null +++ b/03-behavioral/observer/StockObserver.java @@ -0,0 +1,6 @@ +package observer; + +/** Observer interface — all subscribers implement this */ +public interface StockObserver { + void onPriceChanged(String ticker, double oldPrice, double newPrice); +} diff --git a/03-behavioral/state/GreenState.java b/03-behavioral/state/GreenState.java new file mode 100644 index 0000000..acb5277 --- /dev/null +++ b/03-behavioral/state/GreenState.java @@ -0,0 +1,11 @@ +package state; + +public class GreenState implements TrafficLightState { + @Override public void onEnter(TrafficLight light) { + System.out.println(" [GREEN] Go. Traffic flows."); + } + @Override public void next(TrafficLight light) { + light.setState(new YellowState()); + } + @Override public String getColor() { return "GREEN"; } +} diff --git a/03-behavioral/state/Main.java b/03-behavioral/state/Main.java new file mode 100644 index 0000000..d3b53ae --- /dev/null +++ b/03-behavioral/state/Main.java @@ -0,0 +1,26 @@ +package state; + +/** + * State Design Pattern — Runnable Demo + * Run: javac state/*.java -d out/state && java -cp out/state state.Main + * Article: https://ankurm.com/state-design-pattern-java/ + */ +public class Main { + public static void main(String[] args) { + System.out.println("=== State Design Pattern Demo ===\n"); + + TrafficLight light = new TrafficLight(); + System.out.println("Starting state: " + light.getColor()); + + System.out.println("\n-- Cycling through states --"); + for (int i = 0; i < 6; i++) { + light.next(); + } + + System.out.println("\n-- Without State pattern: a single class with if/else --"); + System.out.println(" Every new state adds to every method's if-else chain."); + System.out.println(" With State: add one new class, touch nothing else."); + + System.out.println("\n=== Demo complete ==="); + } +} diff --git a/03-behavioral/state/README.md b/03-behavioral/state/README.md new file mode 100644 index 0000000..b481703 --- /dev/null +++ b/03-behavioral/state/README.md @@ -0,0 +1,33 @@ +# State Design Pattern — Java Example + +**Pattern:** Behavioral → State +**Article:** https://ankurm.com/state-design-pattern-java/ + +## What this example shows + +A traffic light with zero if-else state machines. `TrafficLightState` declares what every state must do: handle entry (`onEnter`), handle the transition trigger (`next`), and report its own color. `TrafficLight` is the context — it holds a reference to the current state and delegates every call to it. `RedState`, `GreenState`, and `YellowState` each know only their own entry message and their own next state (Red→Green→Yellow→Red); none of them knows about the full cycle. `Main` creates the light, cycles through six transitions, and prints why this beats a single class full of if-else branches. + +## How to run + +```bash +javac state/*.java -d out/state +java -cp out/state state.Main +``` + +Requires Java 25. + +## Post Section ↔ File Mapping + +| Post Section | File(s) | +|---|---| +| With State: Traffic Light — the State interface | `TrafficLightState.java` | +| With State: Traffic Light — the Context | `TrafficLight.java` | +| With State: Traffic Light — RedState | `RedState.java` | +| With State: Traffic Light — GreenState | `GreenState.java` | +| With State: Traffic Light — YellowState | `YellowState.java` | +| With State: Traffic Light — wiring it together | `Main.java` | + +Note: the "Without State: The if-else Machine" snippet (the fragile `TrafficLight` with a `String state` field) is illustrative only — it is not part of this repository's runnable example. + +Article: https://ankurm.com/state-design-pattern-java/ +All patterns: https://ankurm.com/design-patterns-java/ diff --git a/03-behavioral/state/RedState.java b/03-behavioral/state/RedState.java new file mode 100644 index 0000000..83c82a8 --- /dev/null +++ b/03-behavioral/state/RedState.java @@ -0,0 +1,11 @@ +package state; + +public class RedState implements TrafficLightState { + @Override public void onEnter(TrafficLight light) { + System.out.println(" [RED] Stop. Pedestrians cross."); + } + @Override public void next(TrafficLight light) { + light.setState(new GreenState()); + } + @Override public String getColor() { return "RED"; } +} diff --git a/03-behavioral/state/TrafficLight.java b/03-behavioral/state/TrafficLight.java new file mode 100644 index 0000000..9b46c9d --- /dev/null +++ b/03-behavioral/state/TrafficLight.java @@ -0,0 +1,21 @@ +package state; + +/** Context — holds the current state and delegates behaviour to it */ +public class TrafficLight { + private TrafficLightState state; + + public TrafficLight() { + setState(new RedState()); + } + + public void setState(TrafficLightState newState) { + this.state = newState; + state.onEnter(this); + } + + public void next() { + state.next(this); + } + + public String getColor() { return state.getColor(); } +} diff --git a/03-behavioral/state/TrafficLightState.java b/03-behavioral/state/TrafficLightState.java new file mode 100644 index 0000000..6749c02 --- /dev/null +++ b/03-behavioral/state/TrafficLightState.java @@ -0,0 +1,8 @@ +package state; + +/** State interface — each concrete state handles events differently */ +public interface TrafficLightState { + void onEnter(TrafficLight light); + void next(TrafficLight light); + String getColor(); +} diff --git a/03-behavioral/state/YellowState.java b/03-behavioral/state/YellowState.java new file mode 100644 index 0000000..b772d37 --- /dev/null +++ b/03-behavioral/state/YellowState.java @@ -0,0 +1,11 @@ +package state; + +public class YellowState implements TrafficLightState { + @Override public void onEnter(TrafficLight light) { + System.out.println(" [YELLOW] Slow down. Prepare to stop."); + } + @Override public void next(TrafficLight light) { + light.setState(new RedState()); + } + @Override public String getColor() { return "YELLOW"; } +} diff --git a/03-behavioral/strategy/BubbleSort.java b/03-behavioral/strategy/BubbleSort.java new file mode 100644 index 0000000..991d14c --- /dev/null +++ b/03-behavioral/strategy/BubbleSort.java @@ -0,0 +1,12 @@ +package strategy; + +public class BubbleSort implements SortStrategy { + @Override + public void sort(int[] data) { + System.out.println(" [BubbleSort] O(n²) — small arrays only"); + for (int i = 0; i < data.length - 1; i++) + for (int j = 0; j < data.length - 1 - i; j++) + if (data[j] > data[j + 1]) { int t = data[j]; data[j] = data[j+1]; data[j+1] = t; } + } + @Override public String getName() { return "BubbleSort"; } +} diff --git a/03-behavioral/strategy/Main.java b/03-behavioral/strategy/Main.java new file mode 100644 index 0000000..9d0f071 --- /dev/null +++ b/03-behavioral/strategy/Main.java @@ -0,0 +1,37 @@ +package strategy; + +import java.util.Arrays; + +/** + * Strategy Design Pattern — Runnable Demo + * Run: javac strategy/*.java -d out/strategy && java -cp out/strategy strategy.Main + * Article: https://ankurm.com/strategy-design-pattern-java/ + */ +public class Main { + public static void main(String[] args) { + System.out.println("=== Strategy Design Pattern Demo ===\n"); + + int[] data = {64, 34, 25, 12, 22, 11, 90}; + System.out.println("Input: " + Arrays.toString(data)); + + Sorter sorter = new Sorter(new BubbleSort()); + System.out.println("\n-- BubbleSort --"); + System.out.println("Sorted: " + Arrays.toString(sorter.sort(data))); + + sorter.setStrategy(new MergeSort()); + System.out.println("\n-- MergeSort --"); + System.out.println("Sorted: " + Arrays.toString(sorter.sort(data))); + + sorter.setStrategy(new QuickSort()); + System.out.println("\n-- QuickSort --"); + System.out.println("Sorted: " + Arrays.toString(sorter.sort(data))); + + System.out.println("\n-- Runtime strategy selection (simulating large dataset) --"); + int size = 10_000; + SortStrategy chosen = size > 1000 ? new QuickSort() : new BubbleSort(); + sorter.setStrategy(chosen); + System.out.println(" Chose " + chosen.getName() + " for size=" + size); + + System.out.println("\n=== Demo complete ==="); + } +} diff --git a/03-behavioral/strategy/MergeSort.java b/03-behavioral/strategy/MergeSort.java new file mode 100644 index 0000000..4be72ad --- /dev/null +++ b/03-behavioral/strategy/MergeSort.java @@ -0,0 +1,30 @@ +package strategy; + +import java.util.Arrays; + +public class MergeSort implements SortStrategy { + @Override + public void sort(int[] data) { + System.out.println(" [MergeSort] O(n log n) — stable, good for large datasets"); + mergeSort(data, 0, data.length - 1); + } + + private void mergeSort(int[] a, int l, int r) { + if (l >= r) return; + int m = (l + r) / 2; + mergeSort(a, l, m); mergeSort(a, m + 1, r); + merge(a, l, m, r); + } + + private void merge(int[] a, int l, int m, int r) { + int[] left = Arrays.copyOfRange(a, l, m + 1); + int[] right = Arrays.copyOfRange(a, m + 1, r + 1); + int i = 0, j = 0, k = l; + while (i < left.length && j < right.length) + a[k++] = left[i] <= right[j] ? left[i++] : right[j++]; + while (i < left.length) a[k++] = left[i++]; + while (j < right.length) a[k++] = right[j++]; + } + + @Override public String getName() { return "MergeSort"; } +} diff --git a/03-behavioral/strategy/QuickSort.java b/03-behavioral/strategy/QuickSort.java new file mode 100644 index 0000000..e5cb201 --- /dev/null +++ b/03-behavioral/strategy/QuickSort.java @@ -0,0 +1,26 @@ +package strategy; + +public class QuickSort implements SortStrategy { + @Override + public void sort(int[] data) { + System.out.println(" [QuickSort] O(n log n) avg — fast in practice, not stable"); + quickSort(data, 0, data.length - 1); + } + + private void quickSort(int[] a, int lo, int hi) { + if (lo >= hi) return; + int p = partition(a, lo, hi); + quickSort(a, lo, p - 1); + quickSort(a, p + 1, hi); + } + + private int partition(int[] a, int lo, int hi) { + int pivot = a[hi], i = lo; + for (int j = lo; j < hi; j++) + if (a[j] <= pivot) { int t = a[i]; a[i++] = a[j]; a[j] = t; } + int t = a[i]; a[i] = a[hi]; a[hi] = t; + return i; + } + + @Override public String getName() { return "QuickSort"; } +} diff --git a/03-behavioral/strategy/README.md b/03-behavioral/strategy/README.md new file mode 100644 index 0000000..ba540ca --- /dev/null +++ b/03-behavioral/strategy/README.md @@ -0,0 +1,33 @@ +# Strategy Design Pattern — Java Example + +**Pattern:** Behavioral → Strategy +**Article:** https://ankurm.com/strategy-design-pattern-java/ + +## What this example shows + +Three sorting algorithms behind one interface, swappable at runtime. `SortStrategy` declares `sort()` and `getName()`. `BubbleSort`, `MergeSort`, and `QuickSort` each implement the full algorithm — not stubs, the real divide-and-conquer and partition logic. `Sorter` is the context: it holds whichever strategy is active and copies the input array before sorting so callers can compare results without mutating their data. `Main` sorts the same array with all three strategies in turn, then simulates choosing a strategy based on input size. + +## How to run + +```bash +javac strategy/*.java -d out/strategy +java -cp out/strategy strategy.Main +``` + +Requires Java 25. + +## Post Section ↔ File Mapping + +| Post Section | File(s) | +|---|---| +| Implementation: Pluggable Sorting — the Strategy interface | `SortStrategy.java` | +| Implementation: Pluggable Sorting — BubbleSort | `BubbleSort.java` | +| Implementation: Pluggable Sorting — MergeSort | `MergeSort.java` | +| Implementation: Pluggable Sorting — QuickSort | `QuickSort.java` | +| Implementation: Pluggable Sorting — the Context (Sorter) | `Sorter.java` | +| Implementation: Pluggable Sorting — wiring it together | `Main.java` | + +Note: the "Strategy in Java 8+: Lambdas as Strategies" snippet (`Comparator`/lambda example) is illustrative only — it is not part of this repository's runnable example. + +Article: https://ankurm.com/strategy-design-pattern-java/ +All patterns: https://ankurm.com/design-patterns-java/ diff --git a/03-behavioral/strategy/SortStrategy.java b/03-behavioral/strategy/SortStrategy.java new file mode 100644 index 0000000..88236f1 --- /dev/null +++ b/03-behavioral/strategy/SortStrategy.java @@ -0,0 +1,7 @@ +package strategy; + +/** Strategy interface — all sorting algorithms implement this */ +public interface SortStrategy { + void sort(int[] data); + String getName(); +} diff --git a/03-behavioral/strategy/Sorter.java b/03-behavioral/strategy/Sorter.java new file mode 100644 index 0000000..649d43d --- /dev/null +++ b/03-behavioral/strategy/Sorter.java @@ -0,0 +1,24 @@ +package strategy; + +import java.util.Arrays; + +/** + * Context — uses a SortStrategy. The strategy can be swapped at runtime. + * Sorter doesn't care which algorithm is used; it just calls sort(). + */ +public class Sorter { + private SortStrategy strategy; + + public Sorter(SortStrategy strategy) { this.strategy = strategy; } + + public void setStrategy(SortStrategy strategy) { + System.out.println(" Switching to: " + strategy.getName()); + this.strategy = strategy; + } + + public int[] sort(int[] data) { + int[] copy = Arrays.copyOf(data, data.length); + strategy.sort(copy); + return copy; + } +} diff --git a/03-behavioral/template-method/ApiMigration.java b/03-behavioral/template-method/ApiMigration.java new file mode 100644 index 0000000..609366b --- /dev/null +++ b/03-behavioral/template-method/ApiMigration.java @@ -0,0 +1,31 @@ +package template; + +public class ApiMigration extends DataMigration { + + private final String endpoint; + + public ApiMigration(String endpoint) { this.endpoint = endpoint; } + + @Override protected String getSourceName() { return "API:" + endpoint; } + + @Override + protected void connect() { + System.out.println(" Authenticating with API at " + endpoint + "..."); + } + + @Override + protected int readData() { + System.out.println(" Paginating through API responses..."); + return 320; + } + + @Override + protected int transformData(int rawCount) { + System.out.println(" Flattening JSON, deduplicating (" + rawCount + " records)..."); + return rawCount - 15; // 15 duplicates removed + } + + // Override hook: silent migrations from APIs, no email spam + @Override + protected boolean sendNotification() { return false; } +} diff --git a/03-behavioral/template-method/CsvMigration.java b/03-behavioral/template-method/CsvMigration.java new file mode 100644 index 0000000..705caf9 --- /dev/null +++ b/03-behavioral/template-method/CsvMigration.java @@ -0,0 +1,27 @@ +package template; + +public class CsvMigration extends DataMigration { + + private final String filePath; + + public CsvMigration(String filePath) { this.filePath = filePath; } + + @Override protected String getSourceName() { return "CSV:" + filePath; } + + @Override + protected void connect() { + System.out.println(" Opening CSV file: " + filePath); + } + + @Override + protected int readData() { + System.out.println(" Parsing CSV rows..."); + return 1_500; // simulated row count + } + + @Override + protected int transformData(int rawCount) { + System.out.println(" Mapping CSV columns to target schema (" + rawCount + " rows)..."); + return rawCount; // no rows lost + } +} diff --git a/03-behavioral/template-method/DataMigration.java b/03-behavioral/template-method/DataMigration.java new file mode 100644 index 0000000..92eb734 --- /dev/null +++ b/03-behavioral/template-method/DataMigration.java @@ -0,0 +1,50 @@ +package template; + +/** + * Abstract Class with Template Method. + * The overall migration algorithm is fixed here; subclasses fill in + * the source-specific steps (connect, read, transform). + */ +public abstract class DataMigration { + + // THE TEMPLATE METHOD — defines the fixed algorithm skeleton + public final void migrate() { + System.out.println("\n[" + getSourceName() + "] Starting migration..."); + connect(); + validate(); + int rowCount = readData(); + int transformed = transformData(rowCount); + writeData(transformed); + if (sendNotification()) { + notifyStakeholders(); + } + disconnect(); + System.out.println("[" + getSourceName() + "] Migration complete.\n"); + } + + // Abstract steps — must be implemented by each subclass + protected abstract String getSourceName(); + protected abstract void connect(); + protected abstract int readData(); + protected abstract int transformData(int rawCount); + + // Concrete steps — common to all migrations + protected void validate() { + System.out.println(" Validating schema compatibility..."); + } + + protected void writeData(int count) { + System.out.println(" Writing " + count + " rows to target..."); + } + + protected void disconnect() { + System.out.println(" Closing source connection."); + } + + // Hook — subclasses may override to change behaviour + protected boolean sendNotification() { return true; } + + protected void notifyStakeholders() { + System.out.println(" Sending completion email to team."); + } +} diff --git a/03-behavioral/template-method/Main.java b/03-behavioral/template-method/Main.java new file mode 100644 index 0000000..7386f47 --- /dev/null +++ b/03-behavioral/template-method/Main.java @@ -0,0 +1,24 @@ +package template; + +/** + * Template Method Design Pattern — Runnable Demo + * Run: javac template/*.java -d out/template && java -cp out/template template.Main + * Article: https://ankurm.com/template-method-design-pattern-java/ + */ +public class Main { + public static void main(String[] args) { + System.out.println("=== Template Method Design Pattern Demo ==="); + + DataMigration csvJob = new CsvMigration("customers_export.csv"); + csvJob.migrate(); + + DataMigration apiJob = new ApiMigration("https://api.partner.com/v2/orders"); + apiJob.migrate(); + + System.out.println("-- Both jobs used the same migrate() skeleton --"); + System.out.println("-- Each provided its own connect/read/transform implementation --"); + System.out.println("-- ApiMigration overrode the sendNotification() hook: no email --"); + + System.out.println("\n=== Demo complete ==="); + } +} diff --git a/03-behavioral/template-method/README.md b/03-behavioral/template-method/README.md new file mode 100644 index 0000000..a8717ac --- /dev/null +++ b/03-behavioral/template-method/README.md @@ -0,0 +1,29 @@ +# Template Method Design Pattern — Java Example + +**Pattern:** Behavioral → Template Method +**Article:** https://ankurm.com/template-method-design-pattern-java/ + +## What this example shows + +A data migration pipeline with a fixed skeleton and variable steps. `DataMigration` declares `migrate()` as `final` — the sequence (connect, validate, read, transform, write, notify, disconnect) can never be reordered or skipped by a subclass. `getSourceName()`, `connect()`, `readData()`, and `transformData()` are abstract steps each subclass must supply; `validate()`, `writeData()`, and `disconnect()` are concrete steps shared by everyone; `sendNotification()` is a hook with a default of `true` that a subclass can override. `CsvMigration` uses every default unchanged. `ApiMigration` overrides the hook to stay silent. `Main` runs both through the identical skeleton. + +## How to run + +```bash +javac template-method/*.java -d out/template-method +java -cp out/template-method template.Main +``` + +Requires Java 25. + +## Post Section ↔ File Mapping + +| Post Section | File(s) | +|---|---| +| Implementation: Data Migration Pipeline — the abstract class & template method | `DataMigration.java` | +| Implementation: Data Migration Pipeline — CsvMigration | `CsvMigration.java` | +| Implementation: Data Migration Pipeline — ApiMigration | `ApiMigration.java` | +| Implementation: Data Migration Pipeline — wiring it together | `Main.java` | + +Article: https://ankurm.com/template-method-design-pattern-java/ +All patterns: https://ankurm.com/design-patterns-java/ diff --git a/03-behavioral/visitor/AreaCalculator.java b/03-behavioral/visitor/AreaCalculator.java new file mode 100644 index 0000000..991e15f --- /dev/null +++ b/03-behavioral/visitor/AreaCalculator.java @@ -0,0 +1,26 @@ +package visitor; + +/** + * Concrete Visitor 1 — calculates area for each shape type. + * Area formulas live here, not scattered across shape classes. + */ +public class AreaCalculator implements ShapeVisitor { + + @Override + public void visit(Circle c) { + double area = Math.PI * c.getRadius() * c.getRadius(); + System.out.printf(" Area of %s = %.2f%n", c.getName(), area); + } + + @Override + public void visit(Rectangle r) { + double area = r.getWidth() * r.getHeight(); + System.out.printf(" Area of %s = %.2f%n", r.getName(), area); + } + + @Override + public void visit(Triangle t) { + double area = 0.5 * t.getBase() * t.getHeight(); + System.out.printf(" Area of %s = %.2f%n", t.getName(), area); + } +} diff --git a/03-behavioral/visitor/Circle.java b/03-behavioral/visitor/Circle.java new file mode 100644 index 0000000..db187bf --- /dev/null +++ b/03-behavioral/visitor/Circle.java @@ -0,0 +1,9 @@ +package visitor; + +public class Circle implements Shape { + private final double radius; + public Circle(double radius) { this.radius = radius; } + public double getRadius() { return radius; } + @Override public String getName() { return "Circle(r=" + radius + ")"; } + @Override public void accept(ShapeVisitor visitor) { visitor.visit(this); } +} diff --git a/03-behavioral/visitor/Main.java b/03-behavioral/visitor/Main.java new file mode 100644 index 0000000..004f8e9 --- /dev/null +++ b/03-behavioral/visitor/Main.java @@ -0,0 +1,38 @@ +package visitor; + +import java.util.List; + +/** + * Visitor Design Pattern — Runnable Demo + * Run: javac visitor/*.java -d out/visitor && java -cp out/visitor visitor.Main + * Article: https://ankurm.com/visitor-design-pattern-java/ + */ +public class Main { + public static void main(String[] args) { + System.out.println("=== Visitor Design Pattern Demo ===\n"); + + List shapes = List.of( + new Circle(5), + new Rectangle(4, 6), + new Triangle(3, 8) + ); + + System.out.println("-- Area Calculator Visitor --"); + ShapeVisitor areaCalc = new AreaCalculator(); + for (Shape shape : shapes) { + shape.accept(areaCalc); + } + + System.out.println("\n-- Perimeter Calculator Visitor --"); + ShapeVisitor perimCalc = new PerimeterCalculator(); + for (Shape shape : shapes) { + shape.accept(perimCalc); + } + + System.out.println("\n-- Key insight --"); + System.out.println("Added PerimeterCalculator without touching Shape, Circle, Rectangle, Triangle."); + System.out.println("To add another operation: write one new Visitor class."); + + System.out.println("\n=== Demo complete ==="); + } +} diff --git a/03-behavioral/visitor/PerimeterCalculator.java b/03-behavioral/visitor/PerimeterCalculator.java new file mode 100644 index 0000000..6d6ee17 --- /dev/null +++ b/03-behavioral/visitor/PerimeterCalculator.java @@ -0,0 +1,28 @@ +package visitor; + +/** + * Concrete Visitor 2 — calculates perimeter. + * Adding this visitor added zero code to Shape, Circle, Rectangle, Triangle. + */ +public class PerimeterCalculator implements ShapeVisitor { + + @Override + public void visit(Circle c) { + double p = 2 * Math.PI * c.getRadius(); + System.out.printf(" Perimeter of %s = %.2f%n", c.getName(), p); + } + + @Override + public void visit(Rectangle r) { + double p = 2 * (r.getWidth() + r.getHeight()); + System.out.printf(" Perimeter of %s = %.2f%n", r.getName(), p); + } + + @Override + public void visit(Triangle t) { + // Simplified: assume isoceles — base + 2 equal sides approximated + double side = Math.sqrt(Math.pow(t.getBase() / 2, 2) + Math.pow(t.getHeight(), 2)); + double p = t.getBase() + 2 * side; + System.out.printf(" Perimeter of %s = %.2f%n", t.getName(), p); + } +} diff --git a/03-behavioral/visitor/README.md b/03-behavioral/visitor/README.md new file mode 100644 index 0000000..6bd2c40 --- /dev/null +++ b/03-behavioral/visitor/README.md @@ -0,0 +1,33 @@ +# Visitor Design Pattern — Java Example + +**Pattern:** Behavioral → Visitor +**Article:** https://ankurm.com/visitor-design-pattern-java/ + +## What this example shows + +A shape hierarchy that never changes, with operations added as separate visitor classes instead of methods on each shape. `ShapeVisitor` declares one `visit()` overload per concrete type. `Shape` declares `accept(ShapeVisitor)` — the double-dispatch trick: the shape hands itself to the visitor as a concretely-typed `this`, so overload resolution picks the right `visit()` method with no `instanceof` checks anywhere. `Circle`, `Rectangle`, and `Triangle` are plain data holders that know nothing about area or perimeter. `AreaCalculator` and `PerimeterCalculator` are two concrete visitors — adding the second one required zero changes to any shape class. `Main` runs both visitors across the same three shapes. + +## How to run + +```bash +javac visitor/*.java -d out/visitor +java -cp out/visitor visitor.Main +``` + +Requires Java 25. + +## Post Section ↔ File Mapping + +| Post Section | File(s) | +|---|---| +| Implementation: Shape Geometry Operations — the Visitor interface | `ShapeVisitor.java` | +| Implementation: Shape Geometry Operations — the Element interface | `Shape.java` | +| Implementation: Shape Geometry Operations — Circle | `Circle.java` | +| Implementation: Shape Geometry Operations — Rectangle | `Rectangle.java` | +| Implementation: Shape Geometry Operations — Triangle | `Triangle.java` | +| Implementation: Shape Geometry Operations — AreaCalculator | `AreaCalculator.java` | +| Implementation: Shape Geometry Operations — PerimeterCalculator | `PerimeterCalculator.java` | +| Implementation: Shape Geometry Operations — wiring it together | `Main.java` | + +Article: https://ankurm.com/visitor-design-pattern-java/ +All patterns: https://ankurm.com/design-patterns-java/ diff --git a/03-behavioral/visitor/Rectangle.java b/03-behavioral/visitor/Rectangle.java new file mode 100644 index 0000000..8c21417 --- /dev/null +++ b/03-behavioral/visitor/Rectangle.java @@ -0,0 +1,10 @@ +package visitor; + +public class Rectangle implements Shape { + private final double width, height; + public Rectangle(double width, double height) { this.width = width; this.height = height; } + public double getWidth() { return width; } + public double getHeight() { return height; } + @Override public String getName() { return "Rectangle(" + width + "x" + height + ")"; } + @Override public void accept(ShapeVisitor visitor) { visitor.visit(this); } +} diff --git a/03-behavioral/visitor/Shape.java b/03-behavioral/visitor/Shape.java new file mode 100644 index 0000000..af3340e --- /dev/null +++ b/03-behavioral/visitor/Shape.java @@ -0,0 +1,7 @@ +package visitor; + +/** Element interface — accepts a visitor */ +public interface Shape { + void accept(ShapeVisitor visitor); + String getName(); +} diff --git a/03-behavioral/visitor/ShapeVisitor.java b/03-behavioral/visitor/ShapeVisitor.java new file mode 100644 index 0000000..ac781fd --- /dev/null +++ b/03-behavioral/visitor/ShapeVisitor.java @@ -0,0 +1,8 @@ +package visitor; + +/** Visitor interface — one visit() method per concrete element type */ +public interface ShapeVisitor { + void visit(Circle circle); + void visit(Rectangle rectangle); + void visit(Triangle triangle); +} diff --git a/03-behavioral/visitor/Triangle.java b/03-behavioral/visitor/Triangle.java new file mode 100644 index 0000000..3139c75 --- /dev/null +++ b/03-behavioral/visitor/Triangle.java @@ -0,0 +1,10 @@ +package visitor; + +public class Triangle implements Shape { + private final double base, height; + public Triangle(double base, double height) { this.base = base; this.height = height; } + public double getBase() { return base; } + public double getHeight() { return height; } + @Override public String getName() { return "Triangle(b=" + base + ",h=" + height + ")"; } + @Override public void accept(ShapeVisitor visitor) { visitor.visit(this); } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..0885be9 --- /dev/null +++ b/README.md @@ -0,0 +1,502 @@ +# GoF Design Patterns in Java + +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. + +## Structure + +``` +design-patterns/ +├── 01-creational/ +│ ├── factory-method/ +│ ├── abstract-factory/ +│ ├── builder/ +│ ├── prototype/ +│ └── singleton/ +├── 02-structural/ +│ ├── adapter/ +│ ├── bridge/ +│ ├── composite/ +│ ├── decorator/ +│ ├── facade/ +│ ├── flyweight/ +│ └── proxy/ +└── 03-behavioral/ + ├── chain-of-responsibility/ + ├── command/ + ├── interpreter/ + ├── iterator/ + ├── mediator/ + ├── memento/ + ├── observer/ + ├── state/ + ├── strategy/ + ├── template-method/ + └── visitor/ +``` + +## Prerequisites + +- Java 25+ (Eclipse Temurin recommended) +- No build tool required — plain `javac` / `java` + +## Run any pattern + +```bash +# Linux / macOS / Git Bash (Windows) +javac 03-behavioral/strategy/*.java -d out/strategy +java -cp out/strategy strategy.Main +``` + +```cmd +REM Windows Command Prompt +javac 03-behavioral\strategy\*.java -d out\strategy +java -cp out\strategy strategy.Main +``` + +## Articles + +| Pattern | Category | Article | +|---------|----------|---------| +| Factory Method | Creational | https://ankurm.com/factory-method-design-pattern-java/ | +| Abstract Factory | Creational | https://ankurm.com/abstract-factory-design-pattern-java/ | +| Builder | Creational | https://ankurm.com/builder-design-pattern-java/ | +| Prototype | Creational | https://ankurm.com/prototype-design-pattern-java/ | +| Singleton | Creational | https://ankurm.com/singleton-design-pattern-java/ | +| Adapter | Structural | https://ankurm.com/adapter-design-pattern-java/ | +| Bridge | Structural | https://ankurm.com/bridge-design-pattern-java/ | +| Composite | Structural | https://ankurm.com/composite-design-pattern-java/ | +| Decorator | Structural | https://ankurm.com/decorator-design-pattern-java/ | +| Facade | Structural | https://ankurm.com/facade-design-pattern-java/ | +| Flyweight | Structural | https://ankurm.com/flyweight-design-pattern-java/ | +| Proxy | Structural | https://ankurm.com/proxy-design-pattern-java/ | +| Chain of Responsibility | Behavioral | https://ankurm.com/chain-of-responsibility-design-pattern-java/ | +| Command | Behavioral | https://ankurm.com/command-design-pattern-java/ | +| Interpreter | Behavioral | https://ankurm.com/interpreter-design-pattern-java/ | +| Iterator | Behavioral | https://ankurm.com/iterator-design-pattern-java/ | +| Mediator | Behavioral | https://ankurm.com/mediator-design-pattern-java/ | +| Memento | Behavioral | https://ankurm.com/memento-design-pattern-java/ | +| Observer | Behavioral | https://ankurm.com/observer-design-pattern-java/ | +| State | Behavioral | https://ankurm.com/state-design-pattern-java/ | +| Strategy | Behavioral | https://ankurm.com/strategy-design-pattern-java/ | +| 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 +``` + +### Singleton (`01-creational/singleton/`) + +| Post Section | File(s) | +|---|---| +| Implementation 1 — Eager Initialization | `AppConfigEager.java` | +| Implementation 2 — Naïve Lazy Initialization | `AppConfigNaiveLazy.java` | +| Implementation 3 — Synchronized Method | `AppConfigSynchronized.java` | +| Implementation 4 — Double-Checked Locking | `AppConfigDCL.java` | +| Implementation 5 — Initialization-on-Demand Holder | `AppConfigHolder.java` | +| Implementation 6 — Enum Singleton | `AppConfigEnum.java` | +| Running All Six Implementations Together | `Main.java` | + +Each implementation is renamed to its own class (e.g. `AppConfigEager`, `AppConfigHolder`) so all six can be compiled together in one package and compared directly. `Main.java` exercises all six in one run. The "Breaking via Reflection," "Breaking via Serialization," and Spring `@Component` snippets are illustrative only — they are not part of this repository's runnable example. + +Run it: +```bash +javac 01-creational/singleton/*.java -d out/singleton +java -cp out/singleton singleton.Main +``` + +### Adapter (`02-structural/adapter/`) + +| Post Section | File(s) | +|---|---| +| Step 1 — Define the Target Interface | `PaymentGateway.java` | +| Step 2 — The Adaptee (What You're Wrapping) | `StripeClient.java` | +| Step 3 — The Object Adapter | `StripePaymentAdapter.java` | +| Step 4 — The Client (Knows Nothing About Stripe) | `OrderService.java` | +| Putting It Together: The Main Demo | `Main.java` | +| Object Adapter vs Class Adapter | `StripePaymentClassAdapter.java` | + +Note: the `InputStreamReader` JDK-adapter snippet is illustrative only — it is not part of this repository's runnable example. + +Run it: +```bash +javac 02-structural/adapter/*.java -d out/adapter +java -cp out/adapter adapter.Main +``` + +### Bridge (`02-structural/bridge/`) + +| Post Section | File(s) | +|---|---| +| Step 1 — The Implementor Interface | `Device.java` | +| Step 2 — Concrete Implementors (TV and Radio) | `TV.java`, `Radio.java` | +| Step 3 — The Abstraction (RemoteControl) | `RemoteControl.java` | +| Step 4 — Refined Abstraction (AdvancedRemote) | `AdvancedRemote.java` | +| Wiring It Together: Main Demo | `Main.java` | + +Note: the "Class Explosion Problem — Visualised" pseudocode and the JDBC/SLF4J snippets are illustrative only — they are not part of this repository's runnable example. + +Run it: +```bash +javac 02-structural/bridge/*.java -d out/bridge +java -cp out/bridge bridge.Main +``` + +### Composite (`02-structural/composite/`) + +| Post Section | File(s) | +|---|---| +| The Problem: Treating Leaves and Containers Uniformly | illustrative only — not part of this repository's runnable example | +| Step 1 — The Component Interface | `FileSystemItem.java` | +| Step 2 — The Leaf (File) | `File.java` | +| Step 3 — The Composite (Directory) | `Directory.java` | +| Building and Using the Tree | `Main.java` | + +Run it: +```bash +javac 02-structural/composite/*.java -d out/composite +java -cp out/composite composite.Main +``` + +### Decorator (`02-structural/decorator/`) + +| Post Section | File(s) | +|---|---| +| Step 1 — Component Interface | `TextProcessor.java` | +| Step 2 — Concrete Component | `PlainTextProcessor.java` | +| Step 3 — Base Decorator | `TextDecorator.java` | +| Step 4 — Concrete Decorators | `UpperCaseDecorator.java`, `TrimDecorator.java`, `ProfanityFilterDecorator.java` | +| Stacking the Decorators — Order Matters | `Main.java` | + +Note: the JDK `InputStream`/`BufferedInputStream`/`GZIPInputStream` snippet is illustrative only — it is not part of this repository's runnable example. + +Run it: +```bash +javac 02-structural/decorator/*.java -d out/decorator +java -cp out/decorator decorator.Main +``` + +### Facade (`02-structural/facade/`) + +| Post Section | File(s) | +|---|---| +| The Subsystem (Complex, But Unchanged) — VideoFile | `VideoFile.java` | +| The Subsystem (Complex, But Unchanged) — Codec interface | `Codec.java` | +| The Subsystem (Complex, But Unchanged) — Concrete Codecs | `MPEG4CompressionCodec.java`, `OggCompressionCodec.java` | +| The Subsystem (Complex, But Unchanged) — CodecFactory | `CodecFactory.java` | +| The Subsystem (Complex, But Unchanged) — BitrateReader | `BitrateReader.java` | +| The Subsystem (Complex, But Unchanged) — AudioMixer | `AudioMixer.java` | +| The Facade | `VideoConversionFacade.java` | +| Client Code: One Line | `Main.java` | + +Note: the SLF4J `LoggerFactory`, JDBC `DriverManager`, and Spring `JdbcTemplate` snippets under "Facade in the JDK and Real Frameworks" are illustrative only — they are not part of this repository's runnable example. + +Run it: +```bash +javac 02-structural/facade/*.java -d out/facade +java -cp out/facade facade.Main +``` + +### Flyweight (`02-structural/flyweight/`) + +| Post Section | File(s) | +|---|---| +| Step 1 — The Flyweight (TreeType) | `TreeType.java` | +| Step 2 — The Factory (TreeFactory) | `TreeFactory.java` | +| Step 3 — The Context (Tree) | `Tree.java` | +| Putting the Forest Together | `Main.java` | + +Note: the "Flyweight in the JDK: String Pool and Integer Cache" and "Measuring the Benefit" snippets are illustrative only — they are not part of this repository's runnable example. + +Run it: +```bash +javac 02-structural/flyweight/*.java -d out/flyweight +java -cp out/flyweight flyweight.Main +``` + +### Proxy (`02-structural/proxy/`) + +| Post Section | File(s) | +|---|---| +| The Subject Interface | `DatabaseConnection.java` | +| The Real Subject | `RealDatabaseConnection.java` | +| Virtual Proxy: Lazy Initialisation | `LazyConnectionProxy.java` | +| Logging Proxy: Cross-Cutting Concerns | `LoggingProxy.java` | +| Wiring It Together: Proxy Chaining | `Main.java` | + +Note: the "Dynamic Proxy with java.lang.reflect.Proxy" snippet is illustrative only — it is not part of this repository's runnable example. + +Run it: +```bash +javac 02-structural/proxy/*.java -d out/proxy +java -cp out/proxy proxy.Main +``` + +### Chain of Responsibility (`03-behavioral/chain-of-responsibility/`) + +| Post Section | File(s) | +|---|---| +| The Problem: Rigid Routing Logic | illustrative only — not part of this repository's runnable example | +| Implementation: Support Ticket Escalation — base handler | `SupportHandler.java` | +| Implementation: Support Ticket Escalation — the ticket | `SupportTicket.java` | +| Each concrete handler checks only its own responsibility — Level 1 & 2 | `Level1Support.java`, `Level2Support.java` | +| Level3Support and CriticalIncidentTeam follow exactly the same shape | `Level3Support.java`, `CriticalIncidentTeam.java` | +| The chain is assembled in one place | `Main.java` | + +Run it: +```bash +javac 03-behavioral/chain-of-responsibility/*.java -d out/chain +java -cp out/chain chain.Main +``` + +### Command (`03-behavioral/command/`) + +| Post Section | File(s) | +|---|---| +| Step 1 — Command Interface | `Command.java` | +| Step 2 — The Receiver (TextEditor) | `TextEditor.java` | +| Step 3 — Concrete Commands | `InsertCommand.java`, `DeleteCommand.java` | +| Step 4 — The Invoker (CommandHistory) | `CommandHistory.java` | +| Demo | `Main.java` | + +Run it: +```bash +javac 03-behavioral/command/*.java -d out/command +java -cp out/command command.Main +``` + +### Iterator (`03-behavioral/iterator/`) + +| Post Section | File(s) | +|---|---| +| Custom Iterator Implementation — the Iterator interface | `BookIterator.java` | +| Custom Iterator Implementation — the element type | `Book.java` | +| Custom Iterator Implementation — the Aggregate (BookShelf, ForwardIterator, DecadeIterator) | `BookShelf.java` | +| Custom Iterator Implementation — wiring it together | `Main.java` | + +Note: the "How Java's For-Each Loop Uses Iterator" snippet (`Iterable` example) is illustrative only — it is not part of this repository's runnable example. + +Run it: +```bash +javac 03-behavioral/iterator/*.java -d out/iterator +java -cp out/iterator iterator.Main +``` + +### Mediator (`03-behavioral/mediator/`) + +| Post Section | File(s) | +|---|---| +| Implementation: Chat Room — the Mediator interface | `ChatMediator.java` | +| Implementation: Chat Room — the Colleague | `User.java` | +| Implementation: Chat Room — the Concrete Mediator | `ChatRoom.java` | +| Implementation: Chat Room — wiring it together | `Main.java` | + +Note: the "Mediator in Practice: MVC and Spring Events" snippet (`ApplicationEventPublisher`/`@EventListener` example) is illustrative only — it is not part of this repository's runnable example. + +Run it: +```bash +javac 03-behavioral/mediator/*.java -d out/mediator +java -cp out/mediator mediator.Main +``` + +### Memento (`03-behavioral/memento/`) + +| Post Section | File(s) | +|---|---| +| The Memento: Immutable and Opaque | `EditorMemento.java` | +| The Originator (Editor) | `Editor.java` | +| The Caretaker (History) | `History.java` | +| Wiring it together (Main demo) | `Main.java` | + +Note: the "Serialization as Memento" snippet (`ObjectOutputStream`/`ObjectInputStream` example) is illustrative only — it is not part of this repository's runnable example. + +Run it: +```bash +javac 03-behavioral/memento/*.java -d out/memento +java -cp out/memento memento.Main +``` + +### Observer (`03-behavioral/observer/`) + +| Post Section | File(s) | +|---|---| +| Implementation: Stock Price Monitoring — the Observer interface | `StockObserver.java` | +| Implementation: Stock Price Monitoring — the Subject | `StockMarket.java` | +| Implementation: Stock Price Monitoring — AlertObserver | `AlertObserver.java` | +| Implementation: Stock Price Monitoring — PortfolioObserver | `PortfolioObserver.java` | +| Implementation: Stock Price Monitoring — wiring it together | `Main.java` | + +Note: the "Push vs Pull Notification Models" pull-model snippet and the "Thread Safety" `CopyOnWriteArrayList` snippet are illustrative only — they are not part of this repository's runnable example. + +Run it: +```bash +javac 03-behavioral/observer/*.java -d out/observer +java -cp out/observer observer.Main +``` + +### State (`03-behavioral/state/`) + +| Post Section | File(s) | +|---|---| +| With State: Traffic Light — the State interface | `TrafficLightState.java` | +| With State: Traffic Light — the Context | `TrafficLight.java` | +| With State: Traffic Light — RedState | `RedState.java` | +| With State: Traffic Light — GreenState | `GreenState.java` | +| With State: Traffic Light — YellowState | `YellowState.java` | +| With State: Traffic Light — wiring it together | `Main.java` | + +Note: the "Without State: The if-else Machine" snippet is illustrative only — it is not part of this repository's runnable example. + +Run it: +```bash +javac 03-behavioral/state/*.java -d out/state +java -cp out/state state.Main +``` + +### Strategy (`03-behavioral/strategy/`) + +| Post Section | File(s) | +|---|---| +| Implementation: Pluggable Sorting — the Strategy interface | `SortStrategy.java` | +| Implementation: Pluggable Sorting — BubbleSort | `BubbleSort.java` | +| Implementation: Pluggable Sorting — MergeSort | `MergeSort.java` | +| Implementation: Pluggable Sorting — QuickSort | `QuickSort.java` | +| Implementation: Pluggable Sorting — the Context (Sorter) | `Sorter.java` | +| Implementation: Pluggable Sorting — wiring it together | `Main.java` | + +Note: the "Strategy in Java 8+: Lambdas as Strategies" snippet is illustrative only — it is not part of this repository's runnable example. + +Run it: +```bash +javac 03-behavioral/strategy/*.java -d out/strategy +java -cp out/strategy strategy.Main +``` + +### Template Method (`03-behavioral/template-method/`) + +| Post Section | File(s) | +|---|---| +| Implementation: Data Migration Pipeline — the abstract class & template method | `DataMigration.java` | +| Implementation: Data Migration Pipeline — CsvMigration | `CsvMigration.java` | +| Implementation: Data Migration Pipeline — ApiMigration | `ApiMigration.java` | +| Implementation: Data Migration Pipeline — wiring it together | `Main.java` | + +Run it: +```bash +javac 03-behavioral/template-method/*.java -d out/template-method +java -cp out/template-method template.Main +``` + +### Visitor (`03-behavioral/visitor/`) + +| Post Section | File(s) | +|---|---| +| Implementation: Shape Geometry Operations — the Visitor interface | `ShapeVisitor.java` | +| Implementation: Shape Geometry Operations — the Element interface | `Shape.java` | +| Implementation: Shape Geometry Operations — Circle | `Circle.java` | +| Implementation: Shape Geometry Operations — Rectangle | `Rectangle.java` | +| Implementation: Shape Geometry Operations — Triangle | `Triangle.java` | +| Implementation: Shape Geometry Operations — AreaCalculator | `AreaCalculator.java` | +| Implementation: Shape Geometry Operations — PerimeterCalculator | `PerimeterCalculator.java` | +| Implementation: Shape Geometry Operations — wiring it together | `Main.java` | + +Run it: +```bash +javac 03-behavioral/visitor/*.java -d out/visitor +java -cp out/visitor visitor.Main +``` + +### Interpreter (`03-behavioral/interpreter/`) + +| Post Section | File(s) | +|---|---| +| Implementation: Math Expression Evaluator — the AbstractExpression | `Expression.java` | +| Implementation: Math Expression Evaluator — the TerminalExpression | `NumberExpression.java` | +| Implementation: Math Expression Evaluator — AddExpression | `AddExpression.java` | +| Implementation: Math Expression Evaluator — SubtractExpression | `SubtractExpression.java` | +| Implementation: Math Expression Evaluator — MultiplyExpression | `MultiplyExpression.java` | +| Implementation: Math Expression Evaluator — wiring it together | `Main.java` | + +Run it: +```bash +javac 03-behavioral/interpreter/*.java -d out/interpreter +java -cp out/interpreter interpreter.Main +``` + +## Reference + +- *Design Patterns: Elements of Reusable Object-Oriented Software* — Gamma, Helm, Johnson, Vlissides +- https://refactoring.guru/design-patterns