An AGGREGATE is a cluster of associated objects that we treat as a unit for the purpose of data changes. Each AGGREGATE has a root and a boundary. The boundary defines what is inside the AGGREGATE. Domain-Driven Design, Eric Evans
This pattern is at the root of decision making while breathing life into the abstraction of business concerns, yet how to apply these principles in the corners of an application can throw me into a quandary.
Aggregates referencing other aggregates is one such quandary, especially when it comes time to employ the repository(s) associated with each aggregate root.
Using NHibernate can seduce you away from abiding by the aggregate rule with its cascading benefits. I am okay with that, perhaps requiring lazy-loading across aggregate boundaries, since the benefits are great and I am not sure it muddies the aggregate water too much.
But I have a situation where I have the following AGGREGATES:
public class MaterialsTest
{
public Sample Sample {get;set;}
}
public class Sample
{
public string SampleNumber{get;set;}
}
Note that MaterialsTest references Sample. It turns out that MaterialsTest will have several implementations, creating an n...1 relationship to Sample.
As I mentioned, both are aggregates with their respective repositories and NHibernate could let me just cascade my persistence to Sample via mapping. However, there is another rule that popped up that requires the 'SampleNumber' to be set/updated for EACH sample that is saved. That is a Sample specification, though, not a MaterialsTest specification so I want to be sure to keep it in the right camp without munging up my MaterialsTestRepository with references to SampleRepository...I agree it could be argued that the reference is okay but I wanted to keep the rule separate from the repository concerns.
Currently, I am only doing In Memory representations of my persistence, so while I could leap in to NHibernate and employ an IInterceptor.OnFlushDirty call to sniff changes on the properties that determine the value for 'SampleNumber', I thought I'd see if Castle Project's IInterceptor could hep' a brutha out and it turns out I have a cleaner division between my aggregate roots by using interceptors on my repositories.
First my test...I had to learn how to test interceptors along the way. Note that 'SandCone' and 'NuclearDensity' is my MaterialsTest implementations here:
[TestFixture]
[TestsOn(typeof (SampleRepositoryInterceptor))]
public class SampleRepositoryInterceptorTest
{
private IFieldDensityRepository mockRepos;
private MockRepository mocks;
private ProxyGenerator proxyGenerator;
private ISampleRepository sampleRepository;
private SampleRepositoryInterceptor sut;
private IFieldDensityRepository target;
[SetUp]
public void BeforeTest()
{
mocks = new MockRepository();
proxyGenerator = new ProxyGenerator();
sampleRepository = mocks.CreateMock<ISampleRepository>();
mockRepos = mocks.DynamicMock<IFieldDensityRepository>();
sut = CreateSUT();
target =
proxyGenerator.CreateInterfaceProxyWithTarget<IFieldDensityRepository>(mockRepos, sut);
}
[TearDown]
public void AfterTest()
{
mocks.ReplayAll();
mocks.VerifyAll();
}
public SampleRepositoryInterceptor CreateSUT()
{
return new SampleRepositoryInterceptor(sampleRepository);
}
[Test]
public void INterceptSaveMethod_ShouldSaveEnumerableSamples()
{
SandCone sc1 = new SandConeTestDataBuilder().AsNew().Build();
SandCone sc2 = new SandConeTestDataBuilder().AsNew().Build();
Sample[] samplesToSave = new Sample[] {sc1.Sample, sc2.Sample};
using (mocks.Record())
{
Expect.Call(sampleRepository.Save(samplesToSave)).Return(samplesToSave);
}
mocks.ReplayAll();
target.Save(new FieldDensity.FieldDensity[] {sc1, sc2});
}
[Test]
public void INterceptSaveMethod_ShouldSaveSingleSample()
{
SandCone sc1 = new SandConeTestDataBuilder().AsNew().Build();
using (mocks.Record())
{
Expect.Call(sampleRepository.Save(sc1.Sample)).Return(sc1.Sample);
}
mocks.ReplayAll();
target.Save(sc1);
}
}
Now the code that makes it pass:
public class SampleRepositoryInterceptor : IInterceptor
{
private readonly ISampleRepository sampleRepository;
public bool Intercepted;
public SampleRepositoryInterceptor(ISampleRepository sampleRepository)
{
this.sampleRepository = sampleRepository;
}
#region IInterceptor Members
public void Intercept(IInvocation invocation)
{
if (Equals(invocation.Method.Name, "Save"))
{
if (invocation.Arguments[0] is IEnumerable)
{
List<Sample> samples = new List<Sample>();
IEnumerator enumerator = ((IEnumerable) invocation.Arguments[0]).GetEnumerator();
while (enumerator.MoveNext())
{
if(enumerator.Current is IReferenceSample)
samples.Add(((IReferenceSample) enumerator.Current).Sample);
}
sampleRepository.Save(samples.ToArray());
}
else
{
if(invocation.Arguments[0] is IReferenceSample)
{
sampleRepository.Save(((IReferenceSample)invocation.Arguments[0]).Sample);
}
}
}
invocation.Proceed();
}
#endregion
}
Finally, I can decorate my InMemoryFieldDensityRepository with [Interceptor(typeof(SampleRepositoryInterceptor))] and register the interceptor in my container (using Binsor)
Component("sample_repository_interceptor",SampleRepositoryInterceptor,SampleRepositoryInterceptor,LifestyleType.Transient)
Now my client code can just call the repository method for the MaterialsTestRepository implementation (InMemoryFieldDensityRepository above) and my interceptor will enforce my aggregate boundary rule.
fieldDensityRepository.Save(myMaterialsTest);
This gives me freedom to slip in the appropriate SampleNumber rule I mentioned above just before items are delivered to my SampleRepository.
So I learned how to test interceptors, implement them to help me enforce DDD constraints, and simplify my management in the meantime...not bad!
When I implement my NHibernate repositories, I'll be getting even more granular in generating this SampleNumber using NHibernate's built in interception to avoid unnecessary Updates on Samples. Here is another benefit of this interception technique...I get to avoid changing client code to deal with an implementation change.
Posted
02-06-2008 11:16 PM
by
Michael Nichols