Skip to content

Instantly share code, notes, and snippets.

@NeilRobbins
Created July 19, 2010 09:54
Show Gist options
  • Save NeilRobbins/481220 to your computer and use it in GitHub Desktop.
Save NeilRobbins/481220 to your computer and use it in GitHub Desktop.
// Worth noting that restrictions prevented me from accessing libraries like the Apache Commons Guid and others
// Also, not production code - needs some TLC & refactoring love
// If I get time will be moved to a proper home in GitHub
package semeosis.eventsourcing.infrastructure;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import semeosis.eventsourcing.infrastructure.events.IEvent;
import semeosis.eventsourcing.infrastructure.exceptions.RegisteredEventHandlingMethodDoesNotExistException;
public abstract class AggregateRoot {
private Set _changes;
private EventHandlerRegistry _eventHandlerRegistry;
public AggregateRoot() {
_changes = new HashSet();
_eventHandlerRegistry = new EventHandlerRegistry();
}
protected void register(Class event) {
register(event, getEventHandlerMethodNameFollowingConvention(event));
}
private String getEventHandlerMethodNameFollowingConvention(Class event) {
return "apply" + getEventNameWithoutNamespaceQualifiers(event);
}
private String getEventNameWithoutNamespaceQualifiers(Class event) {
String eventName = event.getName();
String eventNameWithoutNamespaceQualifiers =
eventName.substring(eventName.lastIndexOf(".") + 1, eventName.length());
return eventNameWithoutNamespaceQualifiers;
}
private void register(Class event, String handlerMethodName) {
register(event, getDeclaredMethod(handlerMethodName, new Class[] {event}));
}
private void register(Class event, Method method) {
_eventHandlerRegistry.RegisterEventHandler(new EventHandler(event, method));
}
private Method getDeclaredMethod(String name, Class[] parameterTypes) {
try {
return this.getClass().getDeclaredMethod(name, parameterTypes);
}
catch (NoSuchMethodException noSuchMethodException) {
throw new RegisteredEventHandlingMethodDoesNotExistException(name, noSuchMethodException);
}
}
protected void apply(IEvent event) {
apply(event,true);
}
private void apply(IEvent event, boolean remember) {
if (remember) _changes.add(event);
Method[] eventHandlers = _eventHandlerRegistry.HandlersForEvent(event.getClass());
for (int position = 0; position < eventHandlers.length; position++) {
try {
eventHandlers[position].setAccessible(true);
eventHandlers[position].invoke(this, new Object [] {event});
} catch (IllegalAccessException illegalAccessException) {
throw new UnsupportedOperationException();
} catch (InvocationTargetException invocationTargetException) {
throw new UnsupportedOperationException();
}
}
}
public IEvent[] getChanges() {
IEvent[] response = new IEvent[_changes.size()];
int position = 0;
for (Iterator changes = _changes.iterator(); changes.hasNext();) {
response[position] = (IEvent)changes.next();
}
return response;
}
public void LoadFromHistory(IEvent[] events) {
for (int position = 0; position < events.length; position ++) {
apply(events[position], false);
}
}
}
package semeosis.eventsourcing.infrastructure;
public interface Command {
long AggregateId();
}
package semeosis.eventsourcing.infrastructure;
import java.lang.reflect.Method;
public class EventHandler {
Class _event;
Method _method;
public EventHandler(Class event, Method method) {
_event = event;
_method = method;
}
public Class getEvent() {
return _event;
}
public Method getMethod() {
return _method;
}
}
package semeosis.eventsourcing.infrastructure;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Set;
import java.util.Iterator;
public class EventHandlerRegistry {
private Set _eventHandlers;
public EventHandlerRegistry() {
_eventHandlers = new HashSet();
}
public void RegisterEventHandler(EventHandler eventHandler) {
_eventHandlers.add(eventHandler);
}
public Method[] HandlersForEvent(Class event) {
Set handlersForTheEvent = getTheHandlersForAnEvent(event);
Method[] response = getATypedArrayOfHandlersForAnEventFromAnUntypedSetOfHandlers(handlersForTheEvent);
return response;
}
private Method[] getATypedArrayOfHandlersForAnEventFromAnUntypedSetOfHandlers(Set handlersForTheEvent) {
Method[] response = new Method[handlersForTheEvent.size()];
Iterator handlers = handlersForTheEvent.iterator();
int position = 0;
while (handlers.hasNext()) {
response[position] = ((EventHandler)handlers.next()).getMethod();
position ++;
}
return response;
}
private Set getTheHandlersForAnEvent(Class event) {
Set handlersForTheEvent = new HashSet();
EventSpecification specification = new EventSpecification(event);
for (Iterator handlers = _eventHandlers.iterator(); handlers.hasNext();) {
EventHandler handler = (EventHandler)handlers.next();
if (specification.IsMetBy(handler)) handlersForTheEvent.add(handler);
}
return handlersForTheEvent;
}
private class EventSpecification {
private Class _event;
public EventSpecification(Class event) {
_event = event;
}
public boolean IsMetBy(EventHandler eventHandler) {
return eventHandler.getEvent() == _event;
}
}
}
package semeosis.eventsourcing.infrastructure;
import semeosis.eventsourcing.infrastructure.events.IEvent;
public interface EventLoader {
IEvent[] Load(long aggregateId);
}
package semeosis.eventsourcing.infrastructure;
import semeosis.eventsourcing.infrastructure.events.IEvent;
public interface EventPersister {
void Persist(IEvent[] events);
}
package semeosis.eventsourcing.infrastructure.events;
public interface IEvent {
// TODO: Id's should be changed so that they all use a UUID.
long getAggregateId();
}
package semeosis.eventsourcing.infrastructure.exceptions;
public class EventCouldNotBeAppliedException extends RuntimeException {
String _message;
public EventCouldNotBeAppliedException(String message) {
_message = message;
}
public EventCouldNotBeAppliedException(String message, Throwable cause) {
super(cause);
_message = message;
}
private static final long serialVersionUID = -1401658484018712281L;
}
package semeosis.eventsourcing.infrastructure.exceptions;
public class RegisteredEventHandlingMethodDoesNotExistException extends
RuntimeException {
private static final long serialVersionUID = 4868379006678572849L;
private String _methodName;
public RegisteredEventHandlingMethodDoesNotExistException(String methodName) {
_methodName = methodName;
}
public RegisteredEventHandlingMethodDoesNotExistException(String methodName, Throwable cause) {
super(cause);
_methodName = methodName;
}
public String getMethodName() {
return _methodName;
}
}
package semeosis.eventsourcing.specifications.infrastructure;
import semeosis.eventsourcing.testhelpers.FakeStateChangedEventObjectMother;
import semeosis.eventsourcing.testhelpers.MockAggregateRoot;
import junit.framework.TestCase;
public class When_an_aggregate_is_loaded_from_its_history extends TestCase {
private MockAggregateRoot _aggregateRoot;
private long _aggregateId = 100;
protected void setUp() throws Exception {
super.setUp();
Given();
When();
}
private void Given() {
_aggregateRoot = new MockAggregateRoot(_aggregateId);
}
private void When() {
_aggregateRoot.LoadFromHistory(FakeStateChangedEventObjectMother.Events(_aggregateId));
}
public void test_then_its_state_should_be_rebuilt_to_the_latest_version() {
assertTrue(_aggregateRoot.CurrentStateIsEqualToExpectedState(
FakeStateChangedEventObjectMother.StateExpectedAfterApplyingTheEvents()));
}
public void test_then_the_events_applied_during_this_load_process_should_not_be_recorded_as_changes() {
assertEquals(_aggregateRoot.getChanges().length,0);
}
public void test_then_the_events_should_be_applied_in_the_order_in_which_they_were_created_with_the_earliest_first() {
assertTrue(_aggregateRoot.CurrentStateIsEqualToExpectedState(
FakeStateChangedEventObjectMother.StateExpectedAfterApplyingTheEvents()));
}
}
package semeosis.eventsourcing.specifications.infrastructure;
import junit.framework.TestCase;
import semeosis.eventsourcing.infrastructure.events.IEvent;
import semeosis.eventsourcing.testhelpers.FakeEvent;
import semeosis.eventsourcing.testhelpers.MockAggregateRoot;
import semeosis.eventsourcing.testhelpers.ThisAggregateRoot;
public class When_an_event_is_applied_to_an_AggregateRoot extends TestCase {
private MockAggregateRoot _mockAggregateRoot;
private long _aggregateId;
private IEvent _expectedEvent = new FakeEvent(_aggregateId);
protected void setUp() throws Exception {
super.setUp();
Given();
When();
}
private void Given()
{
_mockAggregateRoot = new MockAggregateRoot(_aggregateId);
}
private void When()
{
_mockAggregateRoot.ProcessFakeCommandLeadingToAFakeEventBeingRaised();
}
public void test_then_any_handlers_registered_should_be_invoked() {
assertTrue(_mockAggregateRoot.HadItsRegisteredHandlerCalled());
}
public void test_then_the_event_should_be_retrievable_by_calling_GetChanges_on_the_AggregateRoot() {
assertTrue(new ThisAggregateRoot(_mockAggregateRoot).ContainsThisEvent(_expectedEvent));
}
}
package semeosis.eventsourcing.specifications.infrastructure;
import java.lang.reflect.Method;
import semeosis.eventsourcing.infrastructure.EventHandler;
import semeosis.eventsourcing.infrastructure.EventHandlerRegistry;
import semeosis.eventsourcing.infrastructure.exceptions.RegisteredEventHandlingMethodDoesNotExistException;
import semeosis.eventsourcing.testhelpers.FakeEvent;
import semeosis.eventsourcing.testhelpers.FakeStateChangedEvent;
import semeosis.eventsourcing.testhelpers.TheseMethods;
import junit.framework.TestCase;
public class When_the_handlers_for_an_event_are_requested extends TestCase {
EventHandlerRegistry _registry = new EventHandlerRegistry();
Method[] _actualMethodsReturned;
Class _eventClass = FakeEvent.class;
Class _differentEventClass = FakeStateChangedEvent.class;
EventHandler[] _eventHandlers;
protected void setUp() throws Exception {
super.setUp();
Given();
When();
}
private void Given() {
_eventHandlers = new EventHandler[] {
new EventHandler(_eventClass, getFakeMethod("fakeMethod1"))
, new EventHandler(_eventClass, getFakeMethod("fakeMethod2"))
, new EventHandler(_eventClass, getFakeMethod("fakeMethod3"))
, new EventHandler(_eventClass, getFakeMethod("fakeMethod4"))
};
for(int position = 0; position < _eventHandlers.length; position++) {
_registry.RegisterEventHandler(_eventHandlers[position]);
}
_registry.RegisterEventHandler(new EventHandler(_differentEventClass, getFakeMethod("fakeMethod5")));
}
private void When() {
_actualMethodsReturned = _registry.HandlersForEvent(_eventClass);
}
public void test_then_the_handlers_which_have_been_registered_for_that_event_should_be_returned() {
TheseMethods theseMethods = new TheseMethods(_actualMethodsReturned);
for(int position = 0; position < _eventHandlers.length; position++) {
assertTrue(theseMethods.IncludeThisMethod(_eventHandlers[position].getMethod()));
}
}
private Method getFakeMethod(String methodName){
try {
return this.getClass().getDeclaredMethod(methodName, null);
} catch (NoSuchMethodException noSuchMethodException) {
throw new RegisteredEventHandlingMethodDoesNotExistException(methodName);
}
}
private void fakeMethod1(){}
private void fakeMethod2(){}
private void fakeMethod3(){}
private void fakeMethod4(){}
private void fakeMethod5(){}
}
package semeosis.eventsourcing.testhelpers;
import semeosis.eventsourcing.infrastructure.events.IEvent;
public class FakeEvent implements IEvent {
// TODO: Id's should be changed so that they all use a UUID.
long _id;
public FakeEvent(long id) {
_id = id;
}
public long getAggregateId() {
return _id;
}
public int hashCode() {
final int PRIME = 31;
int result = 1;
result = PRIME * result + (int) (_id ^ (_id >>> 32));
return result;
}
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final FakeEvent other = (FakeEvent) obj;
if (_id != other._id)
return false;
return true;
}
}
package semeosis.eventsourcing.testhelpers;
import semeosis.eventsourcing.infrastructure.events.IEvent;
public class FakeStateChangedEvent implements IEvent {
// TODO: Id's should be changed so that they all use a UUID.
long _id;
String _updatedState;
public FakeStateChangedEvent(long id, String updatedState) {
_id = id;
_updatedState = updatedState;
}
public String getUpdatedState() {
return _updatedState;
}
public long getAggregateId() {
return _id;
}
public int hashCode() {
final int PRIME = 31;
int result = 1;
result = PRIME * result + (int) (_id ^ (_id >>> 32));
return result;
}
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final FakeEvent other = (FakeEvent) obj;
if (_id != other._id)
return false;
return true;
}
}
package semeosis.eventsourcing.testhelpers;
public class FakeStateChangedEventObjectMother {
public static FakeStateChangedEvent[] Events(long aggregateId) {
return new FakeStateChangedEvent[] {
new FakeStateChangedEvent(aggregateId, "A")
, new FakeStateChangedEvent(aggregateId, "AB")
, new FakeStateChangedEvent(aggregateId, "ABC")
, new FakeStateChangedEvent(aggregateId, "ABCD")
, new FakeStateChangedEvent(aggregateId, "ABCDE")
, new FakeStateChangedEvent(aggregateId, "ABCDEF")
, new FakeStateChangedEvent(aggregateId, "ABCDEFG")
};
}
public static String StateExpectedAfterApplyingTheEvents() {
return "ABCDEFG";
}
}
package semeosis.eventsourcing.testhelpers;
import semeosis.eventsourcing.infrastructure.AggregateRoot;
public class MockAggregateRoot extends AggregateRoot {
boolean _applyFakeEventCalled;
long _aggregateId;
String _fakeState;
public MockAggregateRoot(long aggregateId) {
_aggregateId = aggregateId;
register(FakeEvent.class);
register(FakeStateChangedEvent.class);
}
public void ProcessFakeStateChangingCommand(String additionalText) {
String newState = _fakeState + additionalText;
apply(new FakeStateChangedEvent(_aggregateId, newState));
}
public void ProcessFakeCommandLeadingToAFakeEventBeingRaised() {
FakeEvent event = new FakeEvent(_aggregateId);
apply(event);
}
private void applyFakeEvent(FakeEvent event) {
_applyFakeEventCalled = true;
}
private void applyFakeStateChangedEvent(FakeStateChangedEvent event) {
_fakeState = event.getUpdatedState();
}
public boolean CurrentStateIsEqualToExpectedState(String expectedState) {
return _fakeState.equals(expectedState);
}
public boolean HadItsRegisteredHandlerCalled() {
return _applyFakeEventCalled;
}
}
package semeosis.eventsourcing.testhelpers;
import java.lang.reflect.Method;
public class TheseMethods {
Method[] _methods;
public TheseMethods(Method[] methods) {
_methods = methods;
}
public boolean IncludeThisMethod(Method method) {
for (int position = 0; position < _methods.length; position++) {
if (_methods[position].equals(method))
return true;
}
return false;
}
}
package semeosis.eventsourcing.testhelpers;
import semeosis.eventsourcing.infrastructure.AggregateRoot;
import semeosis.eventsourcing.infrastructure.events.IEvent;
public class ThisAggregateRoot {
AggregateRoot _aggregateRoot;
public ThisAggregateRoot(AggregateRoot aggregateRoot) {
_aggregateRoot = aggregateRoot;
}
public boolean ContainsThisEvent(IEvent expectedEvent) {
IEvent[] changes = _aggregateRoot.getChanges();
for (int position = 0; position < changes.length; position++ ) {
IEvent actualEvent = (IEvent)changes[position];
if (actualEvent.equals(expectedEvent))
return true;
}
return false;
}
public boolean ContainsAnEventOfType(Class expectedType) {
IEvent[] changes = _aggregateRoot.getChanges();
for (int position = 0; position < changes.length; position++ ) {
IEvent actualEvent = (IEvent)changes[position];
if (actualEvent.getClass().equals(expectedType))
return true;
}
return false;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment