From b1f8258778bf8b52a7b63ea40a32ecab5a50399c Mon Sep 17 00:00:00 2001 From: Janaka Premathilaka Date: Fri, 10 Apr 2026 14:01:58 -0700 Subject: [PATCH] Add Phase 3 modern Java pattern variants and concurrency pack --- README.md | 4 +- docs/phase-3-modern-java-variants.md | 42 ++++++ .../example/modern_java/CommandVariants.java | 56 ++++++++ .../modern_java/ConcurrencyPatternsPack.java | 108 ++++++++++++++++ .../example/modern_java/ObserverVariants.java | 57 +++++++++ .../example/modern_java/StrategyVariants.java | 61 +++++++++ .../java/example/ModernJavaVariantsTest.java | 120 ++++++++++++++++++ 7 files changed, 447 insertions(+), 1 deletion(-) create mode 100644 docs/phase-3-modern-java-variants.md create mode 100644 src/main/java/org/example/modern_java/CommandVariants.java create mode 100644 src/main/java/org/example/modern_java/ConcurrencyPatternsPack.java create mode 100644 src/main/java/org/example/modern_java/ObserverVariants.java create mode 100644 src/main/java/org/example/modern_java/StrategyVariants.java create mode 100644 src/test/java/example/ModernJavaVariantsTest.java diff --git a/README.md b/README.md index 375117a..55a83f9 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ This project is intentionally optimized for learning-by-reading and learning-by- Phase tracking: - [Phase 1 — Production-ready reference baseline](docs/phase-1-production-readiness.md) - [Phase 2 — Expand from GoF to enterprise patterns](docs/phase-2-enterprise-patterns.md) +- [Phase 3 — Modern Java implementation variants](docs/phase-3-modern-java-variants.md) --- @@ -175,7 +176,8 @@ src/ │ ├── event_sourcing_pattern/ │ ├── fluent_api_pattern/ │ ├── immutable_pattern/ -│ └── reactive/ +│ ├── reactive/ +│ └── modern_java/ └── test/java/example/ └── *Test.java ``` diff --git a/docs/phase-3-modern-java-variants.md b/docs/phase-3-modern-java-variants.md new file mode 100644 index 0000000..90817a3 --- /dev/null +++ b/docs/phase-3-modern-java-variants.md @@ -0,0 +1,42 @@ +# Phase 3 — Modern Java implementation variants + +This phase adds side-by-side examples to show how the same pattern can be implemented in three styles: + +1. Classic OO implementation +2. Functional/lambda-first implementation +3. Java 21+ friendly implementation (records, sealed interfaces, switch pattern matching, virtual threads) + +## Classic vs Modern Java examples + +Package: `org.example.modern_java` + +### Strategy +- `StrategyVariants.ClassicCalculator`: interface-based strategy injection. +- `StrategyVariants.FunctionalCalculator`: `IntBinaryOperator` strategy via lambda. +- `StrategyVariants.ModernCalculator`: sealed operation hierarchy (`Add`, `Multiply`) with pattern matching switch. + +### Observer +- `ObserverVariants.ClassicSubject`: concrete observer interface list. +- `ObserverVariants.FunctionalSubject`: `Consumer` subscribers. +- `ObserverVariants.ModernPublisher`: `Flow.Publisher` via `SubmissionPublisher`. + +### Command +- `CommandVariants.ClassicInvoker`: command objects implementing `execute`. +- `CommandVariants.FunctionalInvoker`: queued `Supplier` commands. +- `CommandVariants.ModernInvoker`: sealed command records and switch dispatch. + +## Concurrency patterns pack + +Implemented in `ConcurrencyPatternsPack`: + +1. **Producer-Consumer with virtual threads** + - `producerConsumerWithVirtualThreads(int itemCount)` uses `Executors.newVirtualThreadPerTaskExecutor()`. +2. **Bounded queue/backpressure** + - `BoundedBackpressureQueue` wraps `ArrayBlockingQueue` and provides timed `tryPublish`. +3. **Thread confinement / immutable handoff** + - `ThreadConfinedMailbox` runs consumption on a dedicated single thread. + - `ImmutableMessage` record models immutable handoff data. + +## Validation + +- `ModernJavaVariantsTest` validates all new variants end-to-end. diff --git a/src/main/java/org/example/modern_java/CommandVariants.java b/src/main/java/org/example/modern_java/CommandVariants.java new file mode 100644 index 0000000..2f54bb1 --- /dev/null +++ b/src/main/java/org/example/modern_java/CommandVariants.java @@ -0,0 +1,56 @@ +package org.example.modern_java; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +public final class CommandVariants { + private CommandVariants() { + } + + public interface Command { + String execute(); + } + + public static final class ClassicInvoker { + private final List queue = new ArrayList<>(); + + public void enqueue(final Command command) { + queue.add(command); + } + + public List runAll() { + return queue.stream().map(Command::execute).toList(); + } + } + + public static final class FunctionalInvoker { + private final List> queue = new ArrayList<>(); + + public void enqueue(final Supplier command) { + queue.add(command); + } + + public List runAll() { + return queue.stream().map(Supplier::get).toList(); + } + } + + public sealed interface AppCommand permits CreateUser, SuspendUser { + } + + public record CreateUser(String userId) implements AppCommand { + } + + public record SuspendUser(String userId) implements AppCommand { + } + + public static final class ModernInvoker { + public String run(final AppCommand command) { + return switch (command) { + case CreateUser createUser -> "created:" + createUser.userId(); + case SuspendUser suspendUser -> "suspended:" + suspendUser.userId(); + }; + } + } +} diff --git a/src/main/java/org/example/modern_java/ConcurrencyPatternsPack.java b/src/main/java/org/example/modern_java/ConcurrencyPatternsPack.java new file mode 100644 index 0000000..4436d3b --- /dev/null +++ b/src/main/java/org/example/modern_java/ConcurrencyPatternsPack.java @@ -0,0 +1,108 @@ +package org.example.modern_java; + +import java.time.Duration; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +public final class ConcurrencyPatternsPack { + private ConcurrencyPatternsPack() { + } + + public static List producerConsumerWithVirtualThreads(final int itemCount) { + final BlockingQueue queue = new LinkedBlockingQueue<>(); + final AtomicInteger sum = new AtomicInteger(); + + try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { + final Future producer = executor.submit(() -> { + for (int i = 1; i <= itemCount; i++) { + queue.put(i); + } + queue.put(0); + return null; + }); + + final Future consumer = executor.submit(() -> { + int consumed = 0; + while (true) { + final int value = queue.take(); + if (value == 0) { + break; + } + consumed++; + sum.addAndGet(value); + } + return consumed; + }); + + producer.get(); + return List.of(consumer.get(), sum.get()); + } catch (InterruptedException interruptedException) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Interrupted while processing queue", interruptedException); + } catch (ExecutionException executionException) { + throw new IllegalStateException("Producer-consumer execution failed", executionException); + } + } + + public static final class BoundedBackpressureQueue { + private final ArrayBlockingQueue queue; + + public BoundedBackpressureQueue(final int capacity) { + this.queue = new ArrayBlockingQueue<>(capacity); + } + + public boolean tryPublish(final T value, final Duration timeout) throws InterruptedException { + Objects.requireNonNull(timeout, "timeout must not be null"); + return queue.offer(value, timeout.toMillis(), TimeUnit.MILLISECONDS); + } + + public T take() throws InterruptedException { + return queue.take(); + } + + public int size() { + return queue.size(); + } + } + + public static final class ThreadConfinedMailbox implements AutoCloseable { + private final BlockingQueue queue = new LinkedBlockingQueue<>(); + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); + private final Future worker; + + public ThreadConfinedMailbox(final Consumer consumer) { + final Callable task = () -> { + while (!Thread.currentThread().isInterrupted()) { + final T item = queue.take(); + consumer.accept(item); + } + return null; + }; + this.worker = executorService.submit(task); + } + + public void submitImmutable(final T value) throws InterruptedException { + queue.put(value); + } + + @Override + public void close() { + worker.cancel(true); + executorService.shutdownNow(); + } + } + + public record ImmutableMessage(String topic, String payload) { + } +} diff --git a/src/main/java/org/example/modern_java/ObserverVariants.java b/src/main/java/org/example/modern_java/ObserverVariants.java new file mode 100644 index 0000000..417f13c --- /dev/null +++ b/src/main/java/org/example/modern_java/ObserverVariants.java @@ -0,0 +1,57 @@ +package org.example.modern_java; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Flow; +import java.util.concurrent.SubmissionPublisher; +import java.util.function.Consumer; + +public final class ObserverVariants { + private ObserverVariants() { + } + + public interface Observer { + void onEvent(String event); + } + + public static final class ClassicSubject { + private final List observers = new CopyOnWriteArrayList<>(); + + public void subscribe(final Observer observer) { + observers.add(observer); + } + + public void publish(final String event) { + observers.forEach(observer -> observer.onEvent(event)); + } + } + + public static final class FunctionalSubject { + private final List> observers = new CopyOnWriteArrayList<>(); + + public void subscribe(final Consumer observer) { + observers.add(observer); + } + + public void publish(final String event) { + observers.forEach(observer -> observer.accept(event)); + } + } + + public static final class ModernPublisher implements AutoCloseable { + private final SubmissionPublisher publisher = new SubmissionPublisher<>(); + + public void subscribe(final Flow.Subscriber subscriber) { + publisher.subscribe(subscriber); + } + + public void publish(final String event) { + publisher.submit(event); + } + + @Override + public void close() { + publisher.close(); + } + } +} diff --git a/src/main/java/org/example/modern_java/StrategyVariants.java b/src/main/java/org/example/modern_java/StrategyVariants.java new file mode 100644 index 0000000..1ece1e5 --- /dev/null +++ b/src/main/java/org/example/modern_java/StrategyVariants.java @@ -0,0 +1,61 @@ +package org.example.modern_java; + +import java.util.Map; +import java.util.function.IntBinaryOperator; + +public final class StrategyVariants { + private StrategyVariants() { + } + + public interface Strategy { + int apply(int left, int right); + } + + public static final class ClassicCalculator { + private final Strategy strategy; + + public ClassicCalculator(final Strategy strategy) { + this.strategy = strategy; + } + + public int calculate(final int left, final int right) { + return strategy.apply(left, right); + } + } + + public static final class FunctionalCalculator { + private final IntBinaryOperator operation; + + public FunctionalCalculator(final IntBinaryOperator operation) { + this.operation = operation; + } + + public int calculate(final int left, final int right) { + return operation.applyAsInt(left, right); + } + } + + public sealed interface Operation permits Add, Multiply { + } + + public record Add(int left, int right) implements Operation { + } + + public record Multiply(int left, int right) implements Operation { + } + + public static final class ModernCalculator { + private final Map, IntBinaryOperator> registry = Map.of( + Add.class, Integer::sum, + Multiply.class, (left, right) -> left * right + ); + + public int calculate(final Operation operation) { + return switch (operation) { + case Add add -> registry.get(Add.class).applyAsInt(add.left(), add.right()); + case Multiply multiply -> registry.get(Multiply.class) + .applyAsInt(multiply.left(), multiply.right()); + }; + } + } +} diff --git a/src/test/java/example/ModernJavaVariantsTest.java b/src/test/java/example/ModernJavaVariantsTest.java new file mode 100644 index 0000000..dcceeac --- /dev/null +++ b/src/test/java/example/ModernJavaVariantsTest.java @@ -0,0 +1,120 @@ +package example; + +import org.example.modern_java.CommandVariants; +import org.example.modern_java.ConcurrencyPatternsPack; +import org.example.modern_java.ObserverVariants; +import org.example.modern_java.StrategyVariants; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Flow; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ModernJavaVariantsTest { + + @Test + void strategyVariantsShouldWorkAcrossStyles() { + final StrategyVariants.ClassicCalculator classic = + new StrategyVariants.ClassicCalculator((left, right) -> left + right); + final StrategyVariants.FunctionalCalculator functional = + new StrategyVariants.FunctionalCalculator((left, right) -> left * right); + final StrategyVariants.ModernCalculator modern = new StrategyVariants.ModernCalculator(); + + assertEquals(7, classic.calculate(3, 4)); + assertEquals(12, functional.calculate(3, 4)); + assertEquals(11, modern.calculate(new StrategyVariants.Add(5, 6))); + } + + @Test + void commandVariantsShouldExecute() { + final CommandVariants.ClassicInvoker classic = new CommandVariants.ClassicInvoker(); + classic.enqueue(() -> "classic"); + + final CommandVariants.FunctionalInvoker functional = new CommandVariants.FunctionalInvoker(); + functional.enqueue(() -> "functional"); + + final CommandVariants.ModernInvoker modern = new CommandVariants.ModernInvoker(); + + assertEquals(List.of("classic"), classic.runAll()); + assertEquals(List.of("functional"), functional.runAll()); + assertEquals("created:u1", modern.run(new CommandVariants.CreateUser("u1"))); + } + + @Test + void observerVariantsShouldPublish() throws InterruptedException { + final List classicEvents = new ArrayList<>(); + final ObserverVariants.ClassicSubject classic = new ObserverVariants.ClassicSubject(); + classic.subscribe(classicEvents::add); + classic.publish("classic-event"); + + final List functionalEvents = new ArrayList<>(); + final ObserverVariants.FunctionalSubject functional = new ObserverVariants.FunctionalSubject(); + functional.subscribe(functionalEvents::add); + functional.publish("functional-event"); + + final CountDownLatch latch = new CountDownLatch(1); + final List modernEvents = new ArrayList<>(); + try (ObserverVariants.ModernPublisher modern = new ObserverVariants.ModernPublisher()) { + modern.subscribe(new Flow.Subscriber<>() { + private Flow.Subscription subscription; + + @Override + public void onSubscribe(final Flow.Subscription incomingSubscription) { + this.subscription = incomingSubscription; + this.subscription.request(1); + } + + @Override + public void onNext(final String item) { + modernEvents.add(item); + latch.countDown(); + subscription.cancel(); + } + + @Override + public void onError(final Throwable throwable) { + latch.countDown(); + } + + @Override + public void onComplete() { + } + }); + + modern.publish("modern-event"); + assertTrue(latch.await(2, TimeUnit.SECONDS)); + } + + assertEquals(List.of("classic-event"), classicEvents); + assertEquals(List.of("functional-event"), functionalEvents); + assertEquals(List.of("modern-event"), modernEvents); + } + + @Test + void concurrencyPackShouldSupportModernPatterns() throws InterruptedException { + final List consumedAndSum = ConcurrencyPatternsPack.producerConsumerWithVirtualThreads(5); + assertEquals(List.of(5, 15), consumedAndSum); + + final ConcurrencyPatternsPack.BoundedBackpressureQueue bounded = + new ConcurrencyPatternsPack.BoundedBackpressureQueue<>(1); + assertTrue(bounded.tryPublish(1, Duration.ofMillis(50))); + assertEquals(1, bounded.size()); + assertEquals(1, bounded.take()); + + final List seen = new ArrayList<>(); + try (ConcurrencyPatternsPack.ThreadConfinedMailbox mailbox = + new ConcurrencyPatternsPack.ThreadConfinedMailbox<>(seen::add)) { + mailbox.submitImmutable(new ConcurrencyPatternsPack.ImmutableMessage("order", "created")); + Thread.sleep(100); + } + + assertEquals(1, seen.size()); + assertEquals("order", seen.get(0).topic()); + } +}