From 2537fca68827d2f62b4476021f3dce8baa82179a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=20G=C3=B3mez?= Date: Wed, 30 Jan 2019 22:16:37 +0100 Subject: [PATCH 1/8] Add a simple implementation of an event bus using reactor --- build.gradle | 6 +++- src/main/java/tv/codely/Starter.java | 17 ++++++++++ .../SendPushToSubscribersOnVideoCreated.java | 22 +++++++++++++ .../module/push/application/find/.gitkeep | 0 .../notification/module/push/domain/.gitkeep | 0 .../module/push/infrastructure/.gitkeep | 0 .../application/create/VideoCreator.java | 5 +++ .../module/video/domain/VideoCreated.java | 27 ++++++++++++++++ .../module/video/infrastructure/.gitkeep | 0 .../tv/codely/java_bootstrap/Greeter.java | 9 ------ .../application/DomainEventSubscriber.java | 9 ++++++ .../tv/codely/shared/domain/DomainEvent.java | 5 +++ .../tv/codely/shared/domain/EventBus.java | 5 +++ .../infrastructure/bus/ReactorEventBus.java | 31 +++++++++++++++++++ src/test/java/tv/codely/StarterShould.java | 13 ++++++++ .../codely/java_bootstrap/GreeterShould.java | 15 --------- 16 files changed, 139 insertions(+), 25 deletions(-) create mode 100644 src/main/java/tv/codely/Starter.java create mode 100644 src/main/java/tv/codely/context/notification/module/push/application/create/SendPushToSubscribersOnVideoCreated.java create mode 100644 src/main/java/tv/codely/context/notification/module/push/application/find/.gitkeep create mode 100644 src/main/java/tv/codely/context/notification/module/push/domain/.gitkeep create mode 100644 src/main/java/tv/codely/context/notification/module/push/infrastructure/.gitkeep create mode 100644 src/main/java/tv/codely/context/video/module/video/application/create/VideoCreator.java create mode 100644 src/main/java/tv/codely/context/video/module/video/domain/VideoCreated.java create mode 100644 src/main/java/tv/codely/context/video/module/video/infrastructure/.gitkeep delete mode 100644 src/main/java/tv/codely/java_bootstrap/Greeter.java create mode 100644 src/main/java/tv/codely/shared/application/DomainEventSubscriber.java create mode 100644 src/main/java/tv/codely/shared/domain/DomainEvent.java create mode 100644 src/main/java/tv/codely/shared/domain/EventBus.java create mode 100644 src/main/java/tv/codely/shared/infrastructure/bus/ReactorEventBus.java create mode 100644 src/test/java/tv/codely/StarterShould.java delete mode 100644 src/test/java/tv/codely/java_bootstrap/GreeterShould.java diff --git a/build.gradle b/build.gradle index a530d9f..826bc0d 100644 --- a/build.gradle +++ b/build.gradle @@ -8,6 +8,10 @@ repositories { } dependencies { + // Prod + implementation('io.projectreactor:reactor-bus:2.0.8.RELEASE') + + // Test testCompile('org.junit.jupiter:junit-jupiter-api:5.3.2') testRuntime('org.junit.jupiter:junit-jupiter-engine:5.3.2') } @@ -22,4 +26,4 @@ test { reports { html.enabled = true } -} \ No newline at end of file +} diff --git a/src/main/java/tv/codely/Starter.java b/src/main/java/tv/codely/Starter.java new file mode 100644 index 0000000..92576a4 --- /dev/null +++ b/src/main/java/tv/codely/Starter.java @@ -0,0 +1,17 @@ +package tv.codely; + +import tv.codely.context.notification.module.push.application.create.SendPushToSubscribersOnVideoCreated; +import tv.codely.context.video.module.video.domain.VideoCreated; +import tv.codely.shared.infrastructure.bus.ReactorEventBus; + +import java.util.Arrays; + +public class Starter { + public static void main(String[] args) { + var sendPushToSubscribersOnVideoCreated = new SendPushToSubscribersOnVideoCreated(); + + var eventBus = new ReactorEventBus(Arrays.asList(sendPushToSubscribersOnVideoCreated)); + + eventBus.notify(new VideoCreated("Llegamos a 1M de subscribers!", "CodelyTV es una gran plataforma, CREMITA!")); + } +} diff --git a/src/main/java/tv/codely/context/notification/module/push/application/create/SendPushToSubscribersOnVideoCreated.java b/src/main/java/tv/codely/context/notification/module/push/application/create/SendPushToSubscribersOnVideoCreated.java new file mode 100644 index 0000000..dba8396 --- /dev/null +++ b/src/main/java/tv/codely/context/notification/module/push/application/create/SendPushToSubscribersOnVideoCreated.java @@ -0,0 +1,22 @@ +package tv.codely.context.notification.module.push.application.create; + +import tv.codely.context.video.module.video.domain.VideoCreated; +import tv.codely.shared.application.DomainEventSubscriber; + +public class SendPushToSubscribersOnVideoCreated implements DomainEventSubscriber { + @Override + public String subscribedTo() { + return VideoCreated.NAME; + } + + @Override + public void react(VideoCreated event) { + System.out.println( + String.format( + "Hey! There is a new video with title <%s> and description <%s>", + event.name(), + event.description() + ) + ); + } +} diff --git a/src/main/java/tv/codely/context/notification/module/push/application/find/.gitkeep b/src/main/java/tv/codely/context/notification/module/push/application/find/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/main/java/tv/codely/context/notification/module/push/domain/.gitkeep b/src/main/java/tv/codely/context/notification/module/push/domain/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/main/java/tv/codely/context/notification/module/push/infrastructure/.gitkeep b/src/main/java/tv/codely/context/notification/module/push/infrastructure/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/main/java/tv/codely/context/video/module/video/application/create/VideoCreator.java b/src/main/java/tv/codely/context/video/module/video/application/create/VideoCreator.java new file mode 100644 index 0000000..2bccfda --- /dev/null +++ b/src/main/java/tv/codely/context/video/module/video/application/create/VideoCreator.java @@ -0,0 +1,5 @@ +package tv.codely.context.video.module.video.application.create; + +public class VideoCreator { + +} diff --git a/src/main/java/tv/codely/context/video/module/video/domain/VideoCreated.java b/src/main/java/tv/codely/context/video/module/video/domain/VideoCreated.java new file mode 100644 index 0000000..f27b056 --- /dev/null +++ b/src/main/java/tv/codely/context/video/module/video/domain/VideoCreated.java @@ -0,0 +1,27 @@ +package tv.codely.context.video.module.video.domain; + +import tv.codely.shared.domain.DomainEvent; + +public class VideoCreated implements DomainEvent { + public static final String NAME = "video.created"; + + private final String name; + private final String description; + + public VideoCreated(String name, String description) { + this.name = name; + this.description = description; + } + + public String domainEventName() { + return NAME; + } + + public String name() { + return name; + } + + public String description() { + return description; + } +} diff --git a/src/main/java/tv/codely/context/video/module/video/infrastructure/.gitkeep b/src/main/java/tv/codely/context/video/module/video/infrastructure/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/main/java/tv/codely/java_bootstrap/Greeter.java b/src/main/java/tv/codely/java_bootstrap/Greeter.java deleted file mode 100644 index f56dc9f..0000000 --- a/src/main/java/tv/codely/java_bootstrap/Greeter.java +++ /dev/null @@ -1,9 +0,0 @@ -package tv.codely.java_bootstrap; - -public class Greeter { - - public String greet(String name) { - return "Hello " + name; - } - -} diff --git a/src/main/java/tv/codely/shared/application/DomainEventSubscriber.java b/src/main/java/tv/codely/shared/application/DomainEventSubscriber.java new file mode 100644 index 0000000..55577a1 --- /dev/null +++ b/src/main/java/tv/codely/shared/application/DomainEventSubscriber.java @@ -0,0 +1,9 @@ +package tv.codely.shared.application; + +import tv.codely.shared.domain.DomainEvent; + +public interface DomainEventSubscriber { + String subscribedTo(); + + void react(Event event); +} diff --git a/src/main/java/tv/codely/shared/domain/DomainEvent.java b/src/main/java/tv/codely/shared/domain/DomainEvent.java new file mode 100644 index 0000000..02d60fa --- /dev/null +++ b/src/main/java/tv/codely/shared/domain/DomainEvent.java @@ -0,0 +1,5 @@ +package tv.codely.shared.domain; + +public interface DomainEvent { + String domainEventName(); +} diff --git a/src/main/java/tv/codely/shared/domain/EventBus.java b/src/main/java/tv/codely/shared/domain/EventBus.java new file mode 100644 index 0000000..9ce86dd --- /dev/null +++ b/src/main/java/tv/codely/shared/domain/EventBus.java @@ -0,0 +1,5 @@ +package tv.codely.shared.domain; + +public interface EventBus { + void notify(DomainEvent event); +} diff --git a/src/main/java/tv/codely/shared/infrastructure/bus/ReactorEventBus.java b/src/main/java/tv/codely/shared/infrastructure/bus/ReactorEventBus.java new file mode 100644 index 0000000..333a0e7 --- /dev/null +++ b/src/main/java/tv/codely/shared/infrastructure/bus/ReactorEventBus.java @@ -0,0 +1,31 @@ +package tv.codely.shared.infrastructure.bus; + +import reactor.bus.Event; +import reactor.bus.EventBus; +import tv.codely.shared.application.DomainEventSubscriber; +import tv.codely.shared.domain.DomainEvent; + +import java.util.List; + +import static reactor.bus.selector.Selectors.$; + +public class ReactorEventBus implements tv.codely.shared.domain.EventBus { + private final EventBus bus; + + public ReactorEventBus(List subscribers) { + this.bus = EventBus.create(); + + for (DomainEventSubscriber subscriber : subscribers) + { + this.bus.on( + $(subscriber.subscribedTo()), + reactorEvent -> subscriber.react((DomainEvent) reactorEvent.getData()) + ); + } + } + + @Override + public void notify(DomainEvent event) { + this.bus.notify(event.domainEventName(), Event.wrap(event)); + } +} diff --git a/src/test/java/tv/codely/StarterShould.java b/src/test/java/tv/codely/StarterShould.java new file mode 100644 index 0000000..866d2ed --- /dev/null +++ b/src/test/java/tv/codely/StarterShould.java @@ -0,0 +1,13 @@ +package tv.codely; + +import org.junit.jupiter.api.Test; + +class StarterShould { + + @Test + void greet_with_a_hello_message_to_the_name_it_receives() { + Starter greeter = new Starter(); + + } + +} diff --git a/src/test/java/tv/codely/java_bootstrap/GreeterShould.java b/src/test/java/tv/codely/java_bootstrap/GreeterShould.java deleted file mode 100644 index 8399e15..0000000 --- a/src/test/java/tv/codely/java_bootstrap/GreeterShould.java +++ /dev/null @@ -1,15 +0,0 @@ -package tv.codely.java_bootstrap; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.jupiter.api.Test; - -public class GreeterShould { - - @Test - void greet_with_a_hello_message_to_the_name_it_receives() { - Greeter greeter = new Greeter(); - assertEquals("Hello Jhon", greeter.greet("Jhon")); - } - -} From 30fc8048425ce83a7c11bc077b3f82f87a630b18 Mon Sep 17 00:00:00 2001 From: Javier Ferrer Date: Thu, 31 Jan 2019 02:22:38 +0100 Subject: [PATCH 2/8] Add Application Gradle plugin in order to have the following commands available: * `./gradlew run`: Run the main entrypoint (`Starter`) without having to reference it * `./gradlew startScripts`: Creates OS specific scripts to run the project as a JVM application * More info: https://docs.gradle.org/current/userguide/application_plugin.html --- README.md | 2 ++ build.gradle | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/README.md b/README.md index 66a75e6..e42d99f 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,8 @@ Take a look, play and have fun with it! `./gradlew assemble --warning-mode all` 2. Run the tests and plugins verification tasks: `./gradlew check --warning-mode all` + 3. Execute the main application entrypoint: + `./gradlew run --warning-mode all` 4. Start developing! ## 🤔 How to update dependencies diff --git a/build.gradle b/build.gradle index 826bc0d..f0851e6 100644 --- a/build.gradle +++ b/build.gradle @@ -1,4 +1,5 @@ apply plugin: 'java' +apply plugin: 'application' sourceCompatibility = 1.11 targetCompatibility = 1.11 @@ -27,3 +28,7 @@ test { html.enabled = true } } + +application { + mainClassName = "tv.codely.Starter" +} From 23ef6efdef1ebc32701edbf10bdb7d7a70c86240 Mon Sep 17 00:00:00 2001 From: Javier Ferrer Date: Thu, 31 Jan 2019 02:53:59 +0100 Subject: [PATCH 3/8] Refactor the EventBus example polishing out some minor details: * Use immutable `Set` instead of `List` as the subscribers collection while instantiating the `ReactorEventBus`: We don't need to guarantee the subscribers order (in fact, we should avoid depending on it in order to avoid creating dependencies between subscribers based on execution order) * Rename `VideoCreated` semantics by `VideoPublished` in order to bring more domain related language to the code instead of a CRUDy language * Rename `DomainEvent#domainEventName` to `DomainEvent#fullQualifiedEventName`. This way we are being explicit about using a FQEN naming convention (which has been also introduced in this commit: `vendor.bounded_context.subdomain.[event|command|query].version.resource.event_occured`) * Modify the `DomainEventSubscriber#subscribedTo` method return type from `String` to `Class`. This way we gain a robuster contract avoiding possible mistakes returning any random string instead of the actual event we want to subscribe to. * This also has allowed us to not having to expose as public the constant with the event name and access it from the event bus implementation. That is, now we're having a really modullar implementation Open/Closed Principle compliant. * Set the FQEN constant as private and only expose as public the `DomainEvent#fullQualifiedEventName` method. This way we ensure by the `DomainEvent` interface that all subclasses will have the needed information in order to deal with them while serializing and so on (previously we were depending on the constant which isn't declared in the interface). I've left this `DomainEvent#fullQualifiedEventName` in order to use it when we publish the events to some distributed message broker. * Renamed the `EventBus#notify` method to `EventBus#publish`, and `DomainEventSubscriber#react` to `DomainEventSubscriber#consume`. This way we're respecting the same semantics we use while talking about Domain Events (subscribing, publishing, and consuming) without being polluted by the semantics of Java Reactor (`react`) * Refactor the `ReactorEventBus` class in order to use `Set#forEach` method and lambda functions extracted into their own methods in order to make it easier to read --- src/main/java/tv/codely/Starter.java | 21 +++++++---- ...endPushToSubscribersOnVideoPublished.java} | 12 +++---- .../module/video/domain/VideoCreated.java | 27 -------------- .../module/video/domain/VideoPublished.java | 27 ++++++++++++++ .../application/DomainEventSubscriber.java | 6 ++-- .../tv/codely/shared/domain/DomainEvent.java | 2 +- .../tv/codely/shared/domain/EventBus.java | 2 +- .../infrastructure/bus/ReactorEventBus.java | 36 ++++++++++++------- 8 files changed, 77 insertions(+), 56 deletions(-) rename src/main/java/tv/codely/context/notification/module/push/application/create/{SendPushToSubscribersOnVideoCreated.java => SendPushToSubscribersOnVideoPublished.java} (52%) delete mode 100644 src/main/java/tv/codely/context/video/module/video/domain/VideoCreated.java create mode 100644 src/main/java/tv/codely/context/video/module/video/domain/VideoPublished.java diff --git a/src/main/java/tv/codely/Starter.java b/src/main/java/tv/codely/Starter.java index 92576a4..7e6d9ed 100644 --- a/src/main/java/tv/codely/Starter.java +++ b/src/main/java/tv/codely/Starter.java @@ -1,17 +1,26 @@ package tv.codely; -import tv.codely.context.notification.module.push.application.create.SendPushToSubscribersOnVideoCreated; -import tv.codely.context.video.module.video.domain.VideoCreated; +import tv.codely.context.notification.module.push.application.create.SendPushToSubscribersOnVideoPublished; +import tv.codely.context.video.module.video.domain.VideoPublished; +import tv.codely.shared.application.DomainEventSubscriber; +import tv.codely.shared.domain.EventBus; import tv.codely.shared.infrastructure.bus.ReactorEventBus; -import java.util.Arrays; +import java.util.Set; public class Starter { public static void main(String[] args) { - var sendPushToSubscribersOnVideoCreated = new SendPushToSubscribersOnVideoCreated(); + final Set subscribers = Set.of( + new SendPushToSubscribersOnVideoPublished() + ); - var eventBus = new ReactorEventBus(Arrays.asList(sendPushToSubscribersOnVideoCreated)); + final EventBus eventBus = new ReactorEventBus(subscribers); - eventBus.notify(new VideoCreated("Llegamos a 1M de subscribers!", "CodelyTV es una gran plataforma, CREMITA!")); + final var videoPublished = new VideoPublished( + "\uD83C\uDF89 New youtube.com/CodelyTV video title", + "This should be the video description \uD83D\uDE42" + ); + + eventBus.publish(videoPublished); } } diff --git a/src/main/java/tv/codely/context/notification/module/push/application/create/SendPushToSubscribersOnVideoCreated.java b/src/main/java/tv/codely/context/notification/module/push/application/create/SendPushToSubscribersOnVideoPublished.java similarity index 52% rename from src/main/java/tv/codely/context/notification/module/push/application/create/SendPushToSubscribersOnVideoCreated.java rename to src/main/java/tv/codely/context/notification/module/push/application/create/SendPushToSubscribersOnVideoPublished.java index dba8396..ce70b4c 100644 --- a/src/main/java/tv/codely/context/notification/module/push/application/create/SendPushToSubscribersOnVideoCreated.java +++ b/src/main/java/tv/codely/context/notification/module/push/application/create/SendPushToSubscribersOnVideoPublished.java @@ -1,20 +1,20 @@ package tv.codely.context.notification.module.push.application.create; -import tv.codely.context.video.module.video.domain.VideoCreated; +import tv.codely.context.video.module.video.domain.VideoPublished; import tv.codely.shared.application.DomainEventSubscriber; -public class SendPushToSubscribersOnVideoCreated implements DomainEventSubscriber { +public class SendPushToSubscribersOnVideoPublished implements DomainEventSubscriber { @Override - public String subscribedTo() { - return VideoCreated.NAME; + public Class subscribedTo() { + return VideoPublished.class; } @Override - public void react(VideoCreated event) { + public void consume(VideoPublished event) { System.out.println( String.format( "Hey! There is a new video with title <%s> and description <%s>", - event.name(), + event.title(), event.description() ) ); diff --git a/src/main/java/tv/codely/context/video/module/video/domain/VideoCreated.java b/src/main/java/tv/codely/context/video/module/video/domain/VideoCreated.java deleted file mode 100644 index f27b056..0000000 --- a/src/main/java/tv/codely/context/video/module/video/domain/VideoCreated.java +++ /dev/null @@ -1,27 +0,0 @@ -package tv.codely.context.video.module.video.domain; - -import tv.codely.shared.domain.DomainEvent; - -public class VideoCreated implements DomainEvent { - public static final String NAME = "video.created"; - - private final String name; - private final String description; - - public VideoCreated(String name, String description) { - this.name = name; - this.description = description; - } - - public String domainEventName() { - return NAME; - } - - public String name() { - return name; - } - - public String description() { - return description; - } -} diff --git a/src/main/java/tv/codely/context/video/module/video/domain/VideoPublished.java b/src/main/java/tv/codely/context/video/module/video/domain/VideoPublished.java new file mode 100644 index 0000000..3038368 --- /dev/null +++ b/src/main/java/tv/codely/context/video/module/video/domain/VideoPublished.java @@ -0,0 +1,27 @@ +package tv.codely.context.video.module.video.domain; + +import tv.codely.shared.domain.DomainEvent; + +public final class VideoPublished implements DomainEvent { + private static final String FULL_QUALIFIED_EVENT_NAME = "codelytv.video.video.event.1.video.published"; + + private final String title; + private final String description; + + public VideoPublished(String title, String description) { + this.title = title; + this.description = description; + } + + public String fullQualifiedEventName() { + return FULL_QUALIFIED_EVENT_NAME; + } + + public String title() { + return title; + } + + public String description() { + return description; + } +} diff --git a/src/main/java/tv/codely/shared/application/DomainEventSubscriber.java b/src/main/java/tv/codely/shared/application/DomainEventSubscriber.java index 55577a1..099cdaa 100644 --- a/src/main/java/tv/codely/shared/application/DomainEventSubscriber.java +++ b/src/main/java/tv/codely/shared/application/DomainEventSubscriber.java @@ -2,8 +2,8 @@ import tv.codely.shared.domain.DomainEvent; -public interface DomainEventSubscriber { - String subscribedTo(); +public interface DomainEventSubscriber { + Class subscribedTo(); - void react(Event event); + void consume(EventType event); } diff --git a/src/main/java/tv/codely/shared/domain/DomainEvent.java b/src/main/java/tv/codely/shared/domain/DomainEvent.java index 02d60fa..563dd6e 100644 --- a/src/main/java/tv/codely/shared/domain/DomainEvent.java +++ b/src/main/java/tv/codely/shared/domain/DomainEvent.java @@ -1,5 +1,5 @@ package tv.codely.shared.domain; public interface DomainEvent { - String domainEventName(); + String fullQualifiedEventName(); } diff --git a/src/main/java/tv/codely/shared/domain/EventBus.java b/src/main/java/tv/codely/shared/domain/EventBus.java index 9ce86dd..8920253 100644 --- a/src/main/java/tv/codely/shared/domain/EventBus.java +++ b/src/main/java/tv/codely/shared/domain/EventBus.java @@ -1,5 +1,5 @@ package tv.codely.shared.domain; public interface EventBus { - void notify(DomainEvent event); + void publish(DomainEvent event); } diff --git a/src/main/java/tv/codely/shared/infrastructure/bus/ReactorEventBus.java b/src/main/java/tv/codely/shared/infrastructure/bus/ReactorEventBus.java index 333a0e7..8df8b4a 100644 --- a/src/main/java/tv/codely/shared/infrastructure/bus/ReactorEventBus.java +++ b/src/main/java/tv/codely/shared/infrastructure/bus/ReactorEventBus.java @@ -2,30 +2,42 @@ import reactor.bus.Event; import reactor.bus.EventBus; +import reactor.bus.selector.Selector; +import reactor.fn.Consumer; import tv.codely.shared.application.DomainEventSubscriber; import tv.codely.shared.domain.DomainEvent; -import java.util.List; +import java.util.Set; import static reactor.bus.selector.Selectors.$; public class ReactorEventBus implements tv.codely.shared.domain.EventBus { private final EventBus bus; - public ReactorEventBus(List subscribers) { - this.bus = EventBus.create(); + public ReactorEventBus(final Set subscribers) { + bus = EventBus.create(); - for (DomainEventSubscriber subscriber : subscribers) - { - this.bus.on( - $(subscriber.subscribedTo()), - reactorEvent -> subscriber.react((DomainEvent) reactorEvent.getData()) - ); - } + subscribers.forEach(this::registerOnEventBus); } @Override - public void notify(DomainEvent event) { - this.bus.notify(event.domainEventName(), Event.wrap(event)); + public void publish(final DomainEvent event) { + Class eventIdentifier = event.getClass(); + Event wrappedEvent = Event.wrap(event); + + bus.notify(eventIdentifier, wrappedEvent); + } + + private void registerOnEventBus(final DomainEventSubscriber subscriber) { + final Selector eventIdentifier = $(subscriber.subscribedTo()); + + bus.on(eventIdentifier, eventConsumer(subscriber)); + } + + private Consumer eventConsumer(final DomainEventSubscriber subscriber) { + return (Event reactorEvent) -> { + DomainEvent unwrappedEvent = (DomainEvent) reactorEvent.getData(); + subscriber.consume(unwrappedEvent); + }; } } From ffb720c14b620ff45963ead0b14e1c5a9ca492ef Mon Sep 17 00:00:00 2001 From: Javier Ferrer Date: Thu, 31 Jan 2019 03:40:44 +0100 Subject: [PATCH 4/8] Extract the use case business logic to its own Application Service class and execute it from the main entry point MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Create the `AggregateRoot` abstract class in order to deal with recorded domain events being able to pull them out afterwards * Refactor the `EventBus#publish` contract in order to receive a `List` of `DomainEvent`s instead of a single one. I've tried with the varargs approach in order to be able to publish one single event or a list of them, but it wouldn't match the actual contract of the `AggregateRoot#pullDomainEvents(): List` return type. So taking into account we would have to convert it to an `Array` (cost O(n)) just because of that, I think it make sense to just use `List` on the `EventBus` side and simplify the signatures at least for now 🤟 --- src/main/java/tv/codely/Starter.java | 12 ++++------ .../application/create/VideoCreator.java | 5 ---- .../application/create/VideoPublisher.java | 19 +++++++++++++++ .../video/module/video/domain/Video.java | 23 +++++++++++++++++++ .../codely/shared/domain/AggregateRoot.java | 19 +++++++++++++++ .../tv/codely/shared/domain/EventBus.java | 4 +++- .../infrastructure/bus/ReactorEventBus.java | 8 ++++++- 7 files changed, 76 insertions(+), 14 deletions(-) delete mode 100644 src/main/java/tv/codely/context/video/module/video/application/create/VideoCreator.java create mode 100644 src/main/java/tv/codely/context/video/module/video/application/create/VideoPublisher.java create mode 100644 src/main/java/tv/codely/context/video/module/video/domain/Video.java create mode 100644 src/main/java/tv/codely/shared/domain/AggregateRoot.java diff --git a/src/main/java/tv/codely/Starter.java b/src/main/java/tv/codely/Starter.java index 7e6d9ed..ee61adf 100644 --- a/src/main/java/tv/codely/Starter.java +++ b/src/main/java/tv/codely/Starter.java @@ -1,7 +1,7 @@ package tv.codely; import tv.codely.context.notification.module.push.application.create.SendPushToSubscribersOnVideoPublished; -import tv.codely.context.video.module.video.domain.VideoPublished; +import tv.codely.context.video.module.video.application.create.VideoPublisher; import tv.codely.shared.application.DomainEventSubscriber; import tv.codely.shared.domain.EventBus; import tv.codely.shared.infrastructure.bus.ReactorEventBus; @@ -13,14 +13,12 @@ public static void main(String[] args) { final Set subscribers = Set.of( new SendPushToSubscribersOnVideoPublished() ); - final EventBus eventBus = new ReactorEventBus(subscribers); + final var videoPublisher = new VideoPublisher(eventBus); - final var videoPublished = new VideoPublished( - "\uD83C\uDF89 New youtube.com/CodelyTV video title", - "This should be the video description \uD83D\uDE42" - ); + final var videoTitle = "\uD83C\uDF89 New YouTube.com/CodelyTV video title"; + final var videoDescription = "This should be the video description \uD83D\uDE42"; - eventBus.publish(videoPublished); + videoPublisher.publish(videoTitle, videoDescription); } } diff --git a/src/main/java/tv/codely/context/video/module/video/application/create/VideoCreator.java b/src/main/java/tv/codely/context/video/module/video/application/create/VideoCreator.java deleted file mode 100644 index 2bccfda..0000000 --- a/src/main/java/tv/codely/context/video/module/video/application/create/VideoCreator.java +++ /dev/null @@ -1,5 +0,0 @@ -package tv.codely.context.video.module.video.application.create; - -public class VideoCreator { - -} diff --git a/src/main/java/tv/codely/context/video/module/video/application/create/VideoPublisher.java b/src/main/java/tv/codely/context/video/module/video/application/create/VideoPublisher.java new file mode 100644 index 0000000..de9fb4a --- /dev/null +++ b/src/main/java/tv/codely/context/video/module/video/application/create/VideoPublisher.java @@ -0,0 +1,19 @@ +package tv.codely.context.video.module.video.application.create; + +import tv.codely.context.video.module.video.domain.Video; +import tv.codely.shared.domain.EventBus; + +public final class VideoPublisher { + private final EventBus eventBus; + + public VideoPublisher(EventBus eventBus) { + this.eventBus = eventBus; + } + + public void publish(String title, String description) { + final var video = Video.publish(title, description); + + eventBus.publish(video.pullDomainEvents()); + } + +} diff --git a/src/main/java/tv/codely/context/video/module/video/domain/Video.java b/src/main/java/tv/codely/context/video/module/video/domain/Video.java new file mode 100644 index 0000000..f106ab6 --- /dev/null +++ b/src/main/java/tv/codely/context/video/module/video/domain/Video.java @@ -0,0 +1,23 @@ +package tv.codely.context.video.module.video.domain; + +import tv.codely.shared.domain.AggregateRoot; + +public final class Video extends AggregateRoot { + private final String title; + private final String description; + + private Video(String title, String description) { + this.title = title; + this.description = description; + } + + public static Video publish(String title, String description) { + var video = new Video(title, description); + + var videoCreated = new VideoPublished(title, description); + + video.record(videoCreated); + + return video; + } +} diff --git a/src/main/java/tv/codely/shared/domain/AggregateRoot.java b/src/main/java/tv/codely/shared/domain/AggregateRoot.java new file mode 100644 index 0000000..82ecbed --- /dev/null +++ b/src/main/java/tv/codely/shared/domain/AggregateRoot.java @@ -0,0 +1,19 @@ +package tv.codely.shared.domain; + +import java.util.LinkedList; +import java.util.List; + +public abstract class AggregateRoot { + private List recordedDomainEvents = new LinkedList<>(); + + final public List pullDomainEvents() { + final var recordedDomainEvents = this.recordedDomainEvents; + this.recordedDomainEvents = new LinkedList<>(); + + return recordedDomainEvents; + } + + final protected void record(DomainEvent event) { + recordedDomainEvents.add(event); + } +} diff --git a/src/main/java/tv/codely/shared/domain/EventBus.java b/src/main/java/tv/codely/shared/domain/EventBus.java index 8920253..31211bc 100644 --- a/src/main/java/tv/codely/shared/domain/EventBus.java +++ b/src/main/java/tv/codely/shared/domain/EventBus.java @@ -1,5 +1,7 @@ package tv.codely.shared.domain; +import java.util.List; + public interface EventBus { - void publish(DomainEvent event); + void publish(final List events); } diff --git a/src/main/java/tv/codely/shared/infrastructure/bus/ReactorEventBus.java b/src/main/java/tv/codely/shared/infrastructure/bus/ReactorEventBus.java index 8df8b4a..800bccf 100644 --- a/src/main/java/tv/codely/shared/infrastructure/bus/ReactorEventBus.java +++ b/src/main/java/tv/codely/shared/infrastructure/bus/ReactorEventBus.java @@ -7,6 +7,8 @@ import tv.codely.shared.application.DomainEventSubscriber; import tv.codely.shared.domain.DomainEvent; +import java.util.Arrays; +import java.util.List; import java.util.Set; import static reactor.bus.selector.Selectors.$; @@ -21,7 +23,11 @@ public ReactorEventBus(final Set subscribers) { } @Override - public void publish(final DomainEvent event) { + public void publish(final List events) { + events.forEach(this::publish); + } + + private void publish(final DomainEvent event) { Class eventIdentifier = event.getClass(); Event wrappedEvent = Event.wrap(event); From 1073e31ce078d29b36444d917d1c55a3cc348f96 Mon Sep 17 00:00:00 2001 From: Javier Ferrer Date: Thu, 31 Jan 2019 03:40:57 +0100 Subject: [PATCH 5/8] Ignore Gradle `out/` folder --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 11756d0..dc57c96 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Gradle .gradle build/ +out/ # Ignore Gradle GUI config gradle-app.setting From 763b86a4255b7f0174115ebc0b772ac7af91f765 Mon Sep 17 00:00:00 2001 From: Javier Ferrer Date: Thu, 31 Jan 2019 03:43:06 +0100 Subject: [PATCH 6/8] Rename publish application service package --- src/main/java/tv/codely/Starter.java | 2 +- .../video/application/{create => publish}/VideoPublisher.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/main/java/tv/codely/context/video/module/video/application/{create => publish}/VideoPublisher.java (87%) diff --git a/src/main/java/tv/codely/Starter.java b/src/main/java/tv/codely/Starter.java index ee61adf..829da82 100644 --- a/src/main/java/tv/codely/Starter.java +++ b/src/main/java/tv/codely/Starter.java @@ -1,7 +1,7 @@ package tv.codely; import tv.codely.context.notification.module.push.application.create.SendPushToSubscribersOnVideoPublished; -import tv.codely.context.video.module.video.application.create.VideoPublisher; +import tv.codely.context.video.module.video.application.publish.VideoPublisher; import tv.codely.shared.application.DomainEventSubscriber; import tv.codely.shared.domain.EventBus; import tv.codely.shared.infrastructure.bus.ReactorEventBus; diff --git a/src/main/java/tv/codely/context/video/module/video/application/create/VideoPublisher.java b/src/main/java/tv/codely/context/video/module/video/application/publish/VideoPublisher.java similarity index 87% rename from src/main/java/tv/codely/context/video/module/video/application/create/VideoPublisher.java rename to src/main/java/tv/codely/context/video/module/video/application/publish/VideoPublisher.java index de9fb4a..dddcefb 100644 --- a/src/main/java/tv/codely/context/video/module/video/application/create/VideoPublisher.java +++ b/src/main/java/tv/codely/context/video/module/video/application/publish/VideoPublisher.java @@ -1,4 +1,4 @@ -package tv.codely.context.video.module.video.application.create; +package tv.codely.context.video.module.video.application.publish; import tv.codely.context.video.module.video.domain.Video; import tv.codely.shared.domain.EventBus; From 41530f8fc0e94c94170159807dca53d36fcfcae4 Mon Sep 17 00:00:00 2001 From: Javier Ferrer Date: Thu, 31 Jan 2019 04:11:58 +0100 Subject: [PATCH 7/8] Add `VideoTitle` and `VideoDescription` Value Objects examples: * I've go for doing the conversion between primitive types to value objects inside the Application Service. We're used to to do so in the Command/Query handler, and when we don't have a command/query bus we tend to do so in the entrypoint controller. However, I think this way we avoid some boilerplate on the different entrypoints a use case might have, and we avoid coupling our infrastructure to all the internal structure of our domain. What do you think? * Move `tv.codely.Starter` to `tv.codely.context.video.module.video.infrastructure.VideoPublisherCliController` in order to serve as entrypoint controller through CLI --- build.gradle | 2 +- .../application/publish/VideoPublisher.java | 8 ++++-- .../video/module/video/domain/Video.java | 10 +++---- .../module/video/domain/VideoDescription.java | 13 +++++++++ .../video/module/video/domain/VideoTitle.java | 13 +++++++++ .../VideoPublisherCliController.java} | 4 +-- src/test/java/tv/codely/StarterShould.java | 13 --------- .../publish/VideoPublisherShould.java | 27 +++++++++++++++++++ 8 files changed, 67 insertions(+), 23 deletions(-) create mode 100644 src/main/java/tv/codely/context/video/module/video/domain/VideoDescription.java create mode 100644 src/main/java/tv/codely/context/video/module/video/domain/VideoTitle.java rename src/main/java/tv/codely/{Starter.java => context/video/module/video/infrastructure/VideoPublisherCliController.java} (89%) delete mode 100644 src/test/java/tv/codely/StarterShould.java create mode 100644 src/test/java/tv/codely/context/video/module/video/application/publish/VideoPublisherShould.java diff --git a/build.gradle b/build.gradle index f0851e6..9fb3348 100644 --- a/build.gradle +++ b/build.gradle @@ -30,5 +30,5 @@ test { } application { - mainClassName = "tv.codely.Starter" + mainClassName = "tv.codely.context.video.module.video.infrastructure.VideoPublisherCliController" } diff --git a/src/main/java/tv/codely/context/video/module/video/application/publish/VideoPublisher.java b/src/main/java/tv/codely/context/video/module/video/application/publish/VideoPublisher.java index dddcefb..8bf8eac 100644 --- a/src/main/java/tv/codely/context/video/module/video/application/publish/VideoPublisher.java +++ b/src/main/java/tv/codely/context/video/module/video/application/publish/VideoPublisher.java @@ -1,6 +1,8 @@ package tv.codely.context.video.module.video.application.publish; import tv.codely.context.video.module.video.domain.Video; +import tv.codely.context.video.module.video.domain.VideoDescription; +import tv.codely.context.video.module.video.domain.VideoTitle; import tv.codely.shared.domain.EventBus; public final class VideoPublisher { @@ -10,10 +12,12 @@ public VideoPublisher(EventBus eventBus) { this.eventBus = eventBus; } - public void publish(String title, String description) { + public void publish(String rawTitle, String rawDescription) { + final var title = new VideoTitle(rawTitle); + final var description = new VideoDescription(rawDescription); + final var video = Video.publish(title, description); eventBus.publish(video.pullDomainEvents()); } - } diff --git a/src/main/java/tv/codely/context/video/module/video/domain/Video.java b/src/main/java/tv/codely/context/video/module/video/domain/Video.java index f106ab6..cb7c4e9 100644 --- a/src/main/java/tv/codely/context/video/module/video/domain/Video.java +++ b/src/main/java/tv/codely/context/video/module/video/domain/Video.java @@ -3,18 +3,18 @@ import tv.codely.shared.domain.AggregateRoot; public final class Video extends AggregateRoot { - private final String title; - private final String description; + private final VideoTitle title; + private final VideoDescription description; - private Video(String title, String description) { + private Video(VideoTitle title, VideoDescription description) { this.title = title; this.description = description; } - public static Video publish(String title, String description) { + public static Video publish(VideoTitle title, VideoDescription description) { var video = new Video(title, description); - var videoCreated = new VideoPublished(title, description); + var videoCreated = new VideoPublished(title.value(), description.value()); video.record(videoCreated); diff --git a/src/main/java/tv/codely/context/video/module/video/domain/VideoDescription.java b/src/main/java/tv/codely/context/video/module/video/domain/VideoDescription.java new file mode 100644 index 0000000..92a5ea3 --- /dev/null +++ b/src/main/java/tv/codely/context/video/module/video/domain/VideoDescription.java @@ -0,0 +1,13 @@ +package tv.codely.context.video.module.video.domain; + +public final class VideoDescription { + private final String value; + + public VideoDescription(final String value) { + this.value = value; + } + + public String value() { + return value; + } +} diff --git a/src/main/java/tv/codely/context/video/module/video/domain/VideoTitle.java b/src/main/java/tv/codely/context/video/module/video/domain/VideoTitle.java new file mode 100644 index 0000000..040bcbe --- /dev/null +++ b/src/main/java/tv/codely/context/video/module/video/domain/VideoTitle.java @@ -0,0 +1,13 @@ +package tv.codely.context.video.module.video.domain; + +public final class VideoTitle { + private final String value; + + public VideoTitle(String value) { + this.value = value; + } + + public String value() { + return value; + } +} diff --git a/src/main/java/tv/codely/Starter.java b/src/main/java/tv/codely/context/video/module/video/infrastructure/VideoPublisherCliController.java similarity index 89% rename from src/main/java/tv/codely/Starter.java rename to src/main/java/tv/codely/context/video/module/video/infrastructure/VideoPublisherCliController.java index 829da82..768fb44 100644 --- a/src/main/java/tv/codely/Starter.java +++ b/src/main/java/tv/codely/context/video/module/video/infrastructure/VideoPublisherCliController.java @@ -1,4 +1,4 @@ -package tv.codely; +package tv.codely.context.video.module.video.infrastructure; import tv.codely.context.notification.module.push.application.create.SendPushToSubscribersOnVideoPublished; import tv.codely.context.video.module.video.application.publish.VideoPublisher; @@ -8,7 +8,7 @@ import java.util.Set; -public class Starter { +public class VideoPublisherCliController { public static void main(String[] args) { final Set subscribers = Set.of( new SendPushToSubscribersOnVideoPublished() diff --git a/src/test/java/tv/codely/StarterShould.java b/src/test/java/tv/codely/StarterShould.java deleted file mode 100644 index 866d2ed..0000000 --- a/src/test/java/tv/codely/StarterShould.java +++ /dev/null @@ -1,13 +0,0 @@ -package tv.codely; - -import org.junit.jupiter.api.Test; - -class StarterShould { - - @Test - void greet_with_a_hello_message_to_the_name_it_receives() { - Starter greeter = new Starter(); - - } - -} diff --git a/src/test/java/tv/codely/context/video/module/video/application/publish/VideoPublisherShould.java b/src/test/java/tv/codely/context/video/module/video/application/publish/VideoPublisherShould.java new file mode 100644 index 0000000..129b332 --- /dev/null +++ b/src/test/java/tv/codely/context/video/module/video/application/publish/VideoPublisherShould.java @@ -0,0 +1,27 @@ +package tv.codely.context.video.module.video.application.publish; + +import org.junit.jupiter.api.Test; +import tv.codely.context.notification.module.push.application.create.SendPushToSubscribersOnVideoPublished; +import tv.codely.shared.application.DomainEventSubscriber; +import tv.codely.shared.domain.EventBus; +import tv.codely.shared.infrastructure.bus.ReactorEventBus; + +import java.util.Set; + +final class VideoPublisherShould { + @Test + void publish_the_video_published_domain_event() { + final Set subscribers = Set.of( + new SendPushToSubscribersOnVideoPublished() + ); + final EventBus eventBus = new ReactorEventBus(subscribers); + final var videoPublisher = new VideoPublisher(eventBus); + + final var videoTitle = "\uD83C\uDF89 New YouTube.com/CodelyTV video title"; + final var videoDescription = "This should be the video description \uD83D\uDE42"; + + videoPublisher.publish(videoTitle, videoDescription); + + } + +} From 72f600cd41ba58609c0d3346d5b159c140434f38 Mon Sep 17 00:00:00 2001 From: Javier Ferrer Date: Thu, 31 Jan 2019 04:22:45 +0100 Subject: [PATCH 8/8] Add the unit test for the `VideoPublisher` --- build.gradle | 8 +++-- .../module/video/domain/VideoDescription.java | 15 +++++++++ .../module/video/domain/VideoPublished.java | 18 +++++++++++ .../video/module/video/domain/VideoTitle.java | 15 +++++++++ .../publish/VideoPublisherShould.java | 31 ++++++++++--------- 5 files changed, 69 insertions(+), 18 deletions(-) diff --git a/build.gradle b/build.gradle index 9fb3348..1a08332 100644 --- a/build.gradle +++ b/build.gradle @@ -6,15 +6,17 @@ targetCompatibility = 1.11 repositories { mavenCentral() + jcenter() } dependencies { // Prod - implementation('io.projectreactor:reactor-bus:2.0.8.RELEASE') + implementation 'io.projectreactor:reactor-bus:2.0.8.RELEASE' // Test - testCompile('org.junit.jupiter:junit-jupiter-api:5.3.2') - testRuntime('org.junit.jupiter:junit-jupiter-engine:5.3.2') + testCompile "org.mockito:mockito-core:2.+" + testCompile 'org.junit.jupiter:junit-jupiter-api:5.+' + testRuntime 'org.junit.jupiter:junit-jupiter-engine:5.+' } test { diff --git a/src/main/java/tv/codely/context/video/module/video/domain/VideoDescription.java b/src/main/java/tv/codely/context/video/module/video/domain/VideoDescription.java index 92a5ea3..696bda8 100644 --- a/src/main/java/tv/codely/context/video/module/video/domain/VideoDescription.java +++ b/src/main/java/tv/codely/context/video/module/video/domain/VideoDescription.java @@ -10,4 +10,19 @@ public VideoDescription(final String value) { public String value() { return value; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + VideoDescription that = (VideoDescription) o; + + return value.equals(that.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } } diff --git a/src/main/java/tv/codely/context/video/module/video/domain/VideoPublished.java b/src/main/java/tv/codely/context/video/module/video/domain/VideoPublished.java index 3038368..f527419 100644 --- a/src/main/java/tv/codely/context/video/module/video/domain/VideoPublished.java +++ b/src/main/java/tv/codely/context/video/module/video/domain/VideoPublished.java @@ -24,4 +24,22 @@ public String title() { public String description() { return description; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + VideoPublished that = (VideoPublished) o; + + if (!title.equals(that.title)) return false; + return description.equals(that.description); + } + + @Override + public int hashCode() { + int result = title.hashCode(); + result = 31 * result + description.hashCode(); + return result; + } } diff --git a/src/main/java/tv/codely/context/video/module/video/domain/VideoTitle.java b/src/main/java/tv/codely/context/video/module/video/domain/VideoTitle.java index 040bcbe..92b916e 100644 --- a/src/main/java/tv/codely/context/video/module/video/domain/VideoTitle.java +++ b/src/main/java/tv/codely/context/video/module/video/domain/VideoTitle.java @@ -10,4 +10,19 @@ public VideoTitle(String value) { public String value() { return value; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + VideoTitle that = (VideoTitle) o; + + return value.equals(that.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } } diff --git a/src/test/java/tv/codely/context/video/module/video/application/publish/VideoPublisherShould.java b/src/test/java/tv/codely/context/video/module/video/application/publish/VideoPublisherShould.java index 129b332..ef09711 100644 --- a/src/test/java/tv/codely/context/video/module/video/application/publish/VideoPublisherShould.java +++ b/src/test/java/tv/codely/context/video/module/video/application/publish/VideoPublisherShould.java @@ -1,27 +1,28 @@ package tv.codely.context.video.module.video.application.publish; import org.junit.jupiter.api.Test; -import tv.codely.context.notification.module.push.application.create.SendPushToSubscribersOnVideoPublished; -import tv.codely.shared.application.DomainEventSubscriber; +import tv.codely.context.video.module.video.domain.VideoPublished; import tv.codely.shared.domain.EventBus; -import tv.codely.shared.infrastructure.bus.ReactorEventBus; -import java.util.Set; +import java.util.List; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; final class VideoPublisherShould { - @Test - void publish_the_video_published_domain_event() { - final Set subscribers = Set.of( - new SendPushToSubscribersOnVideoPublished() - ); - final EventBus eventBus = new ReactorEventBus(subscribers); - final var videoPublisher = new VideoPublisher(eventBus); + @Test + void publish_the_video_published_domain_event() { + final EventBus eventBus = mock(EventBus.class); + final var videoPublisher = new VideoPublisher(eventBus); + + final var videoTitle = "\uD83C\uDF89 New YouTube.com/CodelyTV video title"; + final var videoDescription = "This should be the video description \uD83D\uDE42"; - final var videoTitle = "\uD83C\uDF89 New YouTube.com/CodelyTV video title"; - final var videoDescription = "This should be the video description \uD83D\uDE42"; + videoPublisher.publish(videoTitle, videoDescription); - videoPublisher.publish(videoTitle, videoDescription); + final var expectedVideoCreated = new VideoPublished(videoTitle, videoDescription); - } + verify(eventBus).publish(List.of(expectedVideoCreated)); + } }