Thursday 15 April 2010

Domain Driven Design - complex validation of commands across different aggregates -



Domain Driven Design - complex validation of commands across different aggregates -

i've began ddd , trying grasp ways different things it. i'm trying design using asynchronous events (no event-sourcing yet) cqrs. i'm stuck validation of commands. i've read question: validation in domain driven design , however, none of answers seem cover complex validation across different aggregate roots.

let's have these aggregate roots:

client - contains list of enabled services, each service can have value-object list of discounts , validity. discountorder - order enable more discounts on of services of given client, contains order items discount configuration. billcycle - each period when bills generated described own billcycle.

here's usecase:

discount order can submitted. each new discount period in discount order should not overlap of billcycles. no 2 discounts of same type can active @ same time on 1 service.

basically, using hibernate in crud style, similar (java code, question language-agnostic):

public class discountprocessor { ... @transactional public void processorder(long orderid) { discorder order = orderdao.get(orderid); billcycle[] cycles = billcycledao.getall(); (orderitem item : order.getitems()) { //validate billcycle overlapping (billcycle cycle : cycles) { if (periodsoverlap(cycle.getperiod(), item.getperiod())) { throw new periodsoverlapwithbillcycle(...); } } //validate discount overlapping (discount d : item.getforservice().getdiscounts()) { if (d.gettype() == item.gettype() && periodsoverlap(d.getperiod(), item.getperiod())) { throw new periodsoverlapwithotheritems(...); } } //maybe other validations in future or stuff ... } creatediscountsfororder(order); } }

now here thoughts on implementation:

basically, order can in 3 states: "draft", "validated" , "invalid". "draft" state can contain kind of invalid data, "validated" state should contain valid data, "invalid" should contain invalid data. therefore, there should method tries switch state of order, let's phone call order.validate(...). method perform validations required shift of state (draft -> validated or draft -> invalid) , if successful - alter state , transmit ordervalidated or orderinvalidated events.

now, i'm struggling with, signature of said order.validate(...) method. validate order, requires several other aggregates, namely billcycle , client. can see these solutions:

put aggregates straight validate method, order.validatewith(client, cycles) or order.validate(new ordervalidationdata(client, cycles)). however, seems bit hackish. extract required info client , cycle kind of intermediate validation info object. order.validate(new ordervalidationdata(client.getdiscountinfos(), getlistofperiods(cycles)). do validation in separate service method can whatever wants whatever aggregates wants (basically similar crud illustration above). however, seems far ddd, method order.validate() become dummy state setter, , calling method create possible bring order unintuitively corrupted state (status = "valid" contains invalid info because nobody bothered phone call validation service).

what proper way it, , whole thought process wrong?

thanks in advance.

what introducing delegate object manipulate order, client, billcycle?

class orderingservice { @injected private clientrepository clientrepository; @injected private billingrepository billrepository; specification<order> validspec() { homecoming new validorderspec(clientrepository, billrepository); } } class validorderspec implements specification<order> { @override public boolean issatisfied(order order) { client client = clientrepository.findby(order.getclientid()); billcycle[] billcycles = billrepository.findall(); // validate here } } class order { void validate(validorderspecification<order> spec) { if (spec.issatisfiedby(this) { validated(); } else { invalidated(); } } }

the pros , cons of 3 solutions, perspective:

order.validatewith(client, cycles)

it easy test validation order.

#file: orderunittest @test public void should_change_to_valid_when_xxxx() { client client = new clientfixture()...build() billcycle[] cycles = new billcyclefixture()...build() order order = new orderfixture()...build(); subject.validatewith(client, cycles); assertthat(order.getstatus(), is(valid)); }

so far good, there seems duplicate test code discountorderprocess.

#file: discountprocessor @test public void should_change_to_valid_when_xxxx() { client client = new clientfixture()...build() billcycle[] cycles = new billcyclefixture()...build() order order = new orderfixture()...build() discountprocessor subject = ... given(clientrepository).findby(client.getid()).thenreturn(client); given(cyclerepository).findall().thenreturn(cycles); given(orderrepository).findby(order.getid()).thenreturn(order); subject.processorder(order.getid()); assertthat(order.getstatus(), is(valid)); } #or in mock style @test public void should_change_to_valid_when_xxxx() { client client = mock(client.class) billcycle[] cycles = array(mock(billcycle.class)) order order = mock(order.class) discountprocessor subject = ... given(clientrepository).findby(client.getid()).thenreturn(client); given(cyclerepository).findall().thenreturn(cycles); given(orderrepository).findby(order.getid()).thenreturn(order); given(client)..... given(cycle1).... subject.processorder(order.getid()); verify(order).validated(); } order.validate(new ordervalidationdata(client.getdiscountinfos(), getlistofperiods(cycles))

same above one, still need prepare info both orderunittest , discountorderprocessunittest. think 1 improve order not tightly coupled client , billcycle.

order.validate()

similar thought if maintain validation in domain layer. not entity's responsibility, consider domain service or specification object.

#file: orderunittest @test public void should_change_to_valid_when_xxxx() { client client = new clientfixture()...build() billcycle[] cycles = new billcyclefixture()...build() order order = new orderfixture()...build(); specification<order> spec = new validorderspec(clientrepository, cyclerepository); given(clientrepository).findby(client.getid()).thenreturn(client); given(cyclerepository).findall().thenreturn(cycles); subject.validate(spec); assertthat(order.getstatus(), is(valid)); } #file: discountprocessor @test public void should_change_to_valid_when_xxxx() { order order = new orderfixture()...build() specification<order> spec = mock(validorderspec.class); discountprocessor subject = ... given(orderingservice).validspec().thenreturn(spec); given(spec).issatisfiedby(order).thenreturn(true); given(orderrepository).findby(order.getid()).thenreturn(order); subject.processorder(order.getid()); assertthat(order.getstatus(), is(valid)); }

validation design language-agnostic domain-driven-design cqrs

No comments:

Post a Comment