Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Step07/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,11 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
runtimeOnly 'org.springframework.boot:spring-boot-devtools'
compileOnly 'org.projectlombok:lombok:1.18.12'
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because this code is a workshop, and because not everyone is familiar with lombok, I wanted to use vanilla java code and not carry extra dependencies doing black magic. I admit though that lombok is a good library.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used it when I started to modify the code, but it has been removed since.

annotationProcessor 'org.projectlombok:lombok:1.18.12'

testCompileOnly 'org.projectlombok:lombok:1.18.12'
testAnnotationProcessor 'org.projectlombok:lombok:1.18.12'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ public final Iterable<? extends Event> getUncommittedChanges() {

public final void loadFromHistory(Iterable<? extends Event> history) {
for (Event e : history) {
if(version + 1 == e.version) {
version = e.version;
if(version + 1 == e.getVersion()) {
version = e.getVersion();
}
applyChange(e, false);
}
Expand All @@ -48,7 +48,7 @@ private void applyChange(Event event, boolean isNew) {

if (isNew) {
version++;
event.version = version;
event.setVersion(version);
changes.add(event);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
package net.agilepartner.workshops.cqrs.core;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;

import java.util.UUID;

@Getter
@Setter(AccessLevel.PROTECTED)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ouuuuhhh black magic

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Black magic has been removed.

public abstract class Command implements Message {
public UUID id;
public UUID aggregateId;

private static final long serialVersionUID = -840035726759327475L;

private UUID id;
private UUID aggregateId;


}
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
package net.agilepartner.workshops.cqrs.core;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;

import java.util.UUID;

@Getter
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More black magic

@Setter
public abstract class Event implements Message {
public UUID aggregateId;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand it seems counter-intuitive to have public fields, but most likely the events will be serialized, and there is no real need for encapsulation here, since events (and command for that matter) are mainly data containers, and have no logic.
Therefore, I go for simplicity and don't need getters and setters that just add syntactic noise here... using the black magic of lombok

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, it's counter-intuitive. And in case field are not encapsulated, the temptation is use something like lombok.

public int version;

private static final long serialVersionUID = 8922791526755347386L;

private UUID aggregateId;
private int version;

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
import java.util.UUID;

public interface Repository<T extends AggregateRoot> {
public T getById(UUID aggregateId);
public void save(T aggregate);
T getById(UUID aggregateId);
void save(T aggregate);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package net.agilepartner.workshops.cqrs.core.infrastructure;

import net.agilepartner.workshops.cqrs.core.Event;

public class UnsupportedEventException extends RuntimeException {

public UnsupportedEventException(Class< ? extends Event> eventType) {
super("Unsupported event " + eventType.getSimpleName());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public void save(UUID aggregateId, Iterable<? extends Event> newEvents, int expe

if (events.containsKey(aggregateId)) {
existingEvents = events.get(aggregateId);
currentVersion = existingEvents.get(existingEvents.size() - 1).version;
currentVersion = existingEvents.get(existingEvents.size() - 1).getVersion();
} else {
events.put(aggregateId, existingEvents);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,22 @@

import java.util.UUID;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import net.agilepartner.workshops.cqrs.core.Command;

@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class CheckInventoryItemIn extends Command {
private static final long serialVersionUID = 1L;
public int quantity;

private int quantity;

public static CheckInventoryItemIn create(UUID aggregateId, int quantity) {
CheckInventoryItemIn cmd = new CheckInventoryItemIn();
cmd.id = UUID.randomUUID();
cmd.aggregateId = aggregateId;
cmd.setId(UUID.randomUUID());
cmd.setAggregateId(aggregateId);
cmd.quantity = quantity;

return cmd;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ public CheckInventoryItemInHandler(Repository<InventoryItem> repository) {

@Override
public void handle(CheckInventoryItemIn command) throws InventoryItemDeactivatedException {
InventoryItem item = repository.getById(command.aggregateId);
item.checkIn(command.quantity);
InventoryItem item = repository.getById(command.getAggregateId());
item.checkIn(command.getQuantity());
repository.save(item);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,22 @@

import java.util.UUID;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import net.agilepartner.workshops.cqrs.core.Command;

@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class CheckInventoryItemOut extends Command {
private static final long serialVersionUID = 2660471147867347530L;
public int quantity;

private int quantity;

public static CheckInventoryItemOut create(UUID aggregateId, int quantity) {
CheckInventoryItemOut cmd = new CheckInventoryItemOut();
cmd.id = UUID.randomUUID();
cmd.aggregateId = aggregateId;
cmd.setId(UUID.randomUUID());
cmd.setAggregateId(aggregateId);
cmd.quantity = quantity;

return cmd;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ public CheckInventoryItemOutHandler(Repository<InventoryItem> repository) {

@Override
public void handle(CheckInventoryItemOut command) throws NotEnoughStockException, InventoryItemDeactivatedException {
InventoryItem item = repository.getById(command.aggregateId);
item.checkOut(command.quantity);
InventoryItem item = repository.getById(command.getAggregateId());
item.checkOut(command.getQuantity());
repository.save(item);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,24 @@

import java.util.UUID;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import net.agilepartner.workshops.cqrs.core.Command;

@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class CreateInventoryItem extends Command {

private static final long serialVersionUID = 1L;
public String name;
public int initialQuantity;

private String name;
private int initialQuantity;

public static CreateInventoryItem create(String name, int initialQuantity) {
CreateInventoryItem cmd = new CreateInventoryItem();
cmd.id = UUID.randomUUID();
cmd.aggregateId = UUID.randomUUID();
cmd.setId(UUID.randomUUID());
cmd.setAggregateId(UUID.randomUUID());
cmd.name = name;
cmd.initialQuantity = initialQuantity;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public CreateInventoryItemHandler(Repository<InventoryItem> repository) {

@Override
public void handle(CreateInventoryItem command) {
InventoryItem item = InventoryItem.create(command.aggregateId, command.name, command.initialQuantity);
InventoryItem item = InventoryItem.create(command.getAggregateId(), command.getName(), command.getInitialQuantity());
repository.save(item);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@

import java.util.UUID;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import net.agilepartner.workshops.cqrs.core.Command;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class DeactivateInventoryItem extends Command {

private static final long serialVersionUID = 1L;

public static DeactivateInventoryItem create(UUID aggregateId) {
DeactivateInventoryItem cmd = new DeactivateInventoryItem();
cmd.id = UUID.randomUUID();
cmd.aggregateId = aggregateId;
cmd.setId(UUID.randomUUID());
cmd.setAggregateId(aggregateId);

return cmd;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public DeactivateInventoryItemHandler(Repository<InventoryItem> repository) {

@Override
public void handle(DeactivateInventoryItem command) {
InventoryItem item = repository.getById(command.aggregateId);
InventoryItem item = repository.getById(command.getAggregateId());
item.deactivate();;
repository.save(item);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,18 +63,18 @@ private void checkActivated() throws InventoryItemDeactivatedException {
protected <T extends Event> void apply(T event) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, this is exactly what we want to avoid by having the "ugly" reflection code in the AggregateRoot base class.

I want to avoid the if-then-else like the plague and I'd much rather stay with nice, clean, and small apply methods that respect the Single Responsibility Principle instead.

I agree that reflection is not the most efficient and clean way to implement that. There are cleaner ways to implement the AggregateRoot base class to avoid reflection, using a more functional approach for example. But this code is for a workshop and I personally prefer focussing on a clean domain than the plumbing code.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this point, I'm totally agree it is not ideal. I'm just looking for a better solution.
I propose to implement it as bellow:

public abstract class AggregateRoot {
    protected Map<Class<? extends Event>, Consumer<? extends Event>> eventsConsumer = new HashMap<>();

    protected AggregateRoot(UUID id) {
        registerEventsConsumer();
        this.id = id;
    }

    protected abstract void registerEventsConsumer();
}

class InventoryItem extends AggregateRoot {
@Override
    protected void registerEventsConsumer() {
        eventsConsumer.put(InventoryItemCreated.class, (Consumer<InventoryItemCreated>) this::apply);
        eventsConsumer.put(InventoryItemRenamed.class, (Consumer<InventoryItemRenamed>) this::apply);
        eventsConsumer.put(InventoryItemCheckedIn.class, (Consumer<InventoryItemCheckedIn>) this::apply);
        eventsConsumer.put(InventoryItemCheckedOut.class, (Consumer<InventoryItemCheckedOut>) this::apply);
        eventsConsumer.put(InventoryItemDeactivated.class, (Consumer<InventoryItemDeactivated>) this::apply);
    }

private void apply(InventoryItemCreated event) {
        this.name = event.getName();
        this.stock = event.getQuantity();
        this.active = true;
    }
}

Single Responsibility Principle is respected, and there is no more ugly code.

if (event instanceof InventoryItemCreated) {
InventoryItemCreated evt = (InventoryItemCreated) event;
this.name = evt.name;
this.stock = evt.quantity;
this.name = evt.getName();
this.stock = evt.getQuantity();
this.active = true;
} else if (event instanceof InventoryItemRenamed) {
InventoryItemRenamed evt = (InventoryItemRenamed) event;
this.name = evt.name;
this.name = evt.getName();
} else if (event instanceof InventoryItemCheckedIn) {
InventoryItemCheckedIn evt = (InventoryItemCheckedIn) event;
this.stock += evt.quantity;
this.stock += evt.getQuantity();
} else if (event instanceof InventoryItemCheckedOut) {
InventoryItemCheckedOut evt = (InventoryItemCheckedOut) event;
this.stock -= evt.quantity;
this.stock -= evt.getQuantity();
} else if (event instanceof InventoryItemDeactivated) {
this.active = false;
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,21 @@

import java.util.UUID;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import net.agilepartner.workshops.cqrs.core.Event;

@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class InventoryItemCheckedIn extends Event {
private static final long serialVersionUID = -8744398303363497614L;
public int quantity;

private int quantity;

public static InventoryItemCheckedIn create(UUID aggregateId, int quantity) {
InventoryItemCheckedIn evt = new InventoryItemCheckedIn();
evt.aggregateId = aggregateId;
evt.setAggregateId(aggregateId);
evt.quantity = quantity;
return evt;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,20 @@

import java.util.UUID;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import net.agilepartner.workshops.cqrs.core.Event;

@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class InventoryItemCheckedOut extends Event {
private static final long serialVersionUID = 1L;
public int quantity;
private int quantity;

public static InventoryItemCheckedOut create(UUID aggregateId, int quantity) {
InventoryItemCheckedOut evt = new InventoryItemCheckedOut();
evt.aggregateId = aggregateId;
evt.setAggregateId(aggregateId);
evt.quantity = quantity;
return evt;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,22 @@

import java.util.UUID;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import net.agilepartner.workshops.cqrs.core.Event;

@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class InventoryItemCreated extends Event {
private static final long serialVersionUID = -5604800934233512172L;
public String name;
public int quantity;

private String name;
private int quantity;

public static InventoryItemCreated create(UUID aggregateId, String name, int quantity) {
InventoryItemCreated evt = new InventoryItemCreated();
evt.aggregateId = aggregateId;
evt.setAggregateId(aggregateId);
evt.name = name;
evt.quantity = quantity;
return evt;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@

import java.util.UUID;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import net.agilepartner.workshops.cqrs.core.Event;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class InventoryItemDeactivated extends Event {

private static final long serialVersionUID = 1L;

public static InventoryItemDeactivated create(UUID aggregateId) {
InventoryItemDeactivated evt = new InventoryItemDeactivated();
evt.aggregateId = aggregateId;
evt.setAggregateId(aggregateId);
return evt;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,21 @@

import java.util.UUID;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import net.agilepartner.workshops.cqrs.core.Event;

@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class InventoryItemRenamed extends Event {
private static final long serialVersionUID = 1L;
public String name;

private String name;

public static InventoryItemRenamed create(UUID aggregateId, String name) {
InventoryItemRenamed evt = new InventoryItemRenamed();
evt.aggregateId = aggregateId;
evt.setAggregateId(aggregateId);
evt.name = name;
return evt;
}
Expand Down
Loading