Created
July 19, 2010 09:54
-
-
Save NeilRobbins/481220 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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