다음 단계를 이어 나가기 전에 솔루션 전반에 걸쳐 사용하게 될 Base Class 들에 대한 설명이 필요할 것 같습니다.
먼저 솔루션 구조는 아래와 같습니다.
Base Library 는 다른 프로젝트에서 참조하게 될 것입니다.
클래스 다이어그램에서의 TabAggregate 는 다음 과정에서 작성할 것입니다.
Aggregate.cs
- Aggregate 는 DDD(Domain Driven Design) 에서 애기하는 비즈니스 애플리케이션의 주축이 되는 구성 요소 입니다. 이 Aggregate는 다음 과정들에서 설명될 event stream의 주체가 되는 역할을 합니다. 이 Aggregate를 상속하여 비즈니스 로직이 구성되어질 것입니다.
public class Aggregate { /// <SUMMARY> /// 해당 Aggregate에 로드된 events number /// </SUMMARY> public int EventsLoaded { get; private set; } /// <SUMMARY> /// 해당 Aggreagte의 Id /// </SUMMARY> public Guid Id { get; internal set; } /// <SUMMARY> /// 제공된 이벤트를 Aggregate에 적용 /// </SUMMARY> /// <PARAM name="events"></PARAM> public void ApplyEvents(IEnumerable events) { foreach (var e in events) { GetType().GetMethod("ApplyOneEvent") .MakeGenericMethod(e.GetType()) .Invoke(this, new object[] { e }); } } public void ApplyOneEvent<TEVENT>(TEvent ev) { var applier = this as IApplyEvent<TEVENT>; if (applier == null) { throw new InvalidOperationException(string.Format( "Aggregate {0} does not know how to apply event {1}", GetType().Name, ev.GetType().Name)); } applier.Apply(ev); EventsLoaded++; } }
IApplyEvent<TEvent>
- 이 인터페이스를 통해, Aggregate 에서 이벤트를 발생시키게 됩니다.
/// <SUMMARY> /// Aggregate에서 이벤트를 적용하는 인터페이스 /// </SUMMARY> /// <TYPEPARAM name="TEvent"></TYPEPARAM> public interface IApplyEvent<TEVENT> { void Apply(TEvent e); }
IEventStore
- Event Store 는 발생되는 모든 이벤트를 기록하고, Aggregate 별로 발생된 이벤트를 불러오는 역할을 하는 이벤트 저장소입니다.
public interface IEventStore { IEnumerable LoadEventsFor<TAGGREGATE>(Guid id); void SaveEventsFor<TAGGREGATE>(Guid id, int eventsLoaded, ArrayList newEvents); }
IHandleCommand<TCommand>
- 사용자의 행위(즉, 커맨드)를 핸들링하는 인터페이스입니다. Aggregate 가 핸들링 할 수 있는 커맨드들을 이 인터페이스를 이용해 추가하게 될 것입니다.
public interface IHandleCommand<TCOMMAND> { IEnumerable Handle(TCommand c); }
ISubscribeTo<TEvent>
- Query Facade 쪽에서 쓰게 될 인터페이스 입니다. 커맨드를 통해 이벤트가 발생했을 경우, 해당 이벤트가 발생되면 Query Facade 쪽으로의 반영을 담당하게 될 인터페이스입니다.
public interface ISubscribeTo<TEVENT> { void Handle(TEvent e); }
BDDTest
- Unit Test 를 위해 필요한 base class 입니다. 도메인 로직에 대한 테스트를 자동화 하기 위해 만들어진 유틸성 클래스입니다.
/// <SUMMARY> /// Aggregate 테스트를 위한 베이스 클래스 /// </SUMMARY> public class BDDTest<TAGGREGATE> where TAggregate : Aggregate, new() { private TAggregate _sut; [TestInitialize] public void BDDTestInitialize() { _sut = new TAggregate(); } protected void Test(IEnumerable given, Func<TAggregate, object> when, Action<object> then) { then(when(ApplyEvents(_sut, given))); } protected IEnumerable Given(params object[] events) { return events; } protected Func<TAggregate, object> When<TCOMMAND>(TCommand command) { return agg => { try { return DispatchCommand(command).Cast<object>().ToArray(); } catch (Exception e) { return e; } }; } protected Action<object> Then(params object[] expectedEvents) { return got => { var gotEvents = got as object[]; if (gotEvents != null) { if (gotEvents.Length == expectedEvents.Length) { for (var i = 0; i < gotEvents.Length; i++) { if (gotEvents[i].GetType() == expectedEvents[i].GetType()) Assert.AreEqual(Serialize(expectedEvents[i]), Serialize(gotEvents[i])); else Assert.Fail(string.Format( "Incorrect event in results; expected a {0} but got a {1}", expectedEvents[i].GetType().Name, gotEvents[i].GetType().Name)); } } else if (gotEvents.Length < expectedEvents.Length) { Assert.Fail(string.Format("Expected event(s) missing: {0}", string.Join(", ", EventDiff(expectedEvents, gotEvents)))); } else { Assert.Fail(string.Format("Unexpected event(s) emitted: {0}", string.Join(", ", EventDiff(gotEvents, expectedEvents)))); } } else if (got is CommandHandlerNotDefiendException) { Assert.Fail((got as Exception).Message); } else { Assert.Fail("Expected events, but got exception {0}", got.GetType().Name); } }; } private string[] EventDiff(object[] a, object[] b) { var diff = a.Select(e => e.GetType().Name).ToList(); foreach (var remove in b.Select(e=>e.GetType().Name)) { diff.Remove(remove); } return diff.ToArray(); } protected Action<object> ThenFailWith<TEXCEPTION>() { return got => { if (got is TException) { } else if (got is CommandHandlerNotDefiendException) { Assert.Fail((got as Exception).Message); } else if (got is Exception) { Assert.Fail(string.Format("Expected exception {0}, but got exception {1}", typeof(TException).Name, got.GetType().Name)); } else { Assert.Fail(string.Format("Expected exception {0}, but got event result", typeof(TException).Name)); } }; } private IEnumerable DispatchCommand<TCOMMAND>(TCommand command) { var handler = _sut as IHandleCommand<TCOMMAND>; if(handler==null) { throw new CommandHandlerNotDefiendException(string.Format( "Aggregate {0} does not yet handle command {1}", _sut.GetType().Name, command.GetType().Name)); } return handler.Handle(command); } private TAggregate ApplyEvents(TAggregate agg, IEnumerable events) { agg.ApplyEvents(events); return agg; } private string Serialize(object obj) { var ser = new XmlSerializer(obj.GetType()); var ms = new MemoryStream(); ser.Serialize(ms, obj); ms.Seek(0, SeekOrigin.Begin); return new StreamReader(ms).ReadToEnd(); } private class CommandHandlerNotDefiendException : Exception { public CommandHandlerNotDefiendException(string msg) : base(msg) { } } }
'Framework > CQRS' 카테고리의 다른 글
Event Sourcing (0) | 2015.12.15 |
---|---|
CQRS + Event Sourcing – A Step by Step Overview (0) | 2015.12.15 |
Cafe CQRS (Read Models) - part 4 (0) | 2015.12.10 |
Cafe CQRS (Domain Logic) - part 3 (0) | 2015.12.01 |
Cafe CQRS - part 1 (0) | 2015.12.01 |