본문 바로가기

Framework/CQRS

Cafe CQRS - part 2

다음 단계를 이어 나가기 전에 솔루션 전반에 걸쳐 사용하게 될 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