The last post ended on a bit of cliffhanger. How do we leverage the compiler to validate before making changes to the domain? We’d like to make it easy to follow this pattern:
public class ItineraryImpl implements Itinerary, UninitializedItinerary {
...
}
public interface Itinerary {
List getLegs();
boolean isExpected(HandlingEvent event);
}
public interface UninitializedItinerary {
void setLegs(List legs);
Itinerary validate(ValidationResults r);
}
public class Cargo {
public void assignToRoute(Itinerary itinerary) {
...
The next step is to support composing validation for multiple objects. We can do this with a simple local transaction class, used like this for example:
UninitializedItinerary itinerary = itineraryFactory.create();
itinerary.setLegs(...);
txn.with(itinerary).add(i->cargo.assignToRoute(i));
if (txn.isValid()) {
txn.commit();
} else {
reject(txn.getValidationResults());
}
With the validation approach described in the previous post, support for these transactions is simple and easy.
Within the domain, we use only domain interfaces. We use the transaction class to convert from uninitialized interfaces to domain interfaces. Especially with Java 8 lambda expressions, it's easy to defer actions until after validation. For example, the "cargo.assignToRoute(i)" call above does not run until and unless all validation for the transaction has succeeded.
Using his approach, it's hard to accidentally use an object before it's been initialized. For example, an unadorned:
cargo.assignToRoute(itinerary);
doesn't compile. Nor does an attempt to modify the private state of an already initialized object:
cargo.itinerary().setLegs(null);
"Enrichment" or "defaulting" has exactly the same challenge as validation. In fact, any calculation that is both validated and then applied to the domain has the same challenge. We want neither the entity nor clients of the domain to care about defaulting logic. The solution is the same: wire up defaulting services in the object factory and let the transaction wiring ensure that defaulting, as well as validation, is applied at the right time.
These transactions are like local database transactions. Instead of making changes immediately and performing a "rollback" if those changes are not actually valid, DDD transactions validate first and only then proceed to make changes.
This is how to do transactions for Event Sourcing or Prevalent Systems. Enrich and validate whole objects, and use the type system to ensure that comes first.
- Build all new objects, setting fields directly
- Validate, and if everything is valid then…
- Call the domain methods
public class ItineraryImpl implements Itinerary, UninitializedItinerary {
...
}
public interface Itinerary {
List
}
public interface UninitializedItinerary {
void setLegs(List
Itinerary validate(ValidationResults r);
}
public class Cargo {
public void assignToRoute(Itinerary itinerary) {
...
The next step is to support composing validation for multiple objects. We can do this with a simple local transaction class, used like this for example:
UninitializedItinerary itinerary = itineraryFactory.create();
itinerary.setLegs(...);
txn.with(itinerary).add(i->cargo.assignToRoute(i));
if (txn.isValid()) {
txn.commit();
} else {
reject(txn.getValidationResults());
}
With the validation approach described in the previous post, support for these transactions is simple and easy.
Within the domain, we use only domain interfaces. We use the transaction class to convert from uninitialized interfaces to domain interfaces. Especially with Java 8 lambda expressions, it's easy to defer actions until after validation. For example, the "cargo.assignToRoute(i)" call above does not run until and unless all validation for the transaction has succeeded.
Using his approach, it's hard to accidentally use an object before it's been initialized. For example, an unadorned:
cargo.assignToRoute(itinerary);
doesn't compile. Nor does an attempt to modify the private state of an already initialized object:
cargo.itinerary().setLegs(null);
"Enrichment" or "defaulting" has exactly the same challenge as validation. In fact, any calculation that is both validated and then applied to the domain has the same challenge. We want neither the entity nor clients of the domain to care about defaulting logic. The solution is the same: wire up defaulting services in the object factory and let the transaction wiring ensure that defaulting, as well as validation, is applied at the right time.
These transactions are like local database transactions. Instead of making changes immediately and performing a "rollback" if those changes are not actually valid, DDD transactions validate first and only then proceed to make changes.
This is how to do transactions for Event Sourcing or Prevalent Systems. Enrich and validate whole objects, and use the type system to ensure that comes first.