Last active
January 27, 2021 00:00
-
-
Save bion/31311a05f9b433a48afaf6cc75b63b02 to your computer and use it in GitHub Desktop.
Domain model design sketch -- ObjectMapper approach
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Entity | |
class Applicant extends Model { | |
@Id | |
Long id; | |
@DbJsonB | |
CustomObjectNode object; | |
// etc. | |
} | |
enum ApplicantDataNodeTypes { | |
ADDRESS, | |
ARRAY, | |
DATE, | |
FILE, | |
MONEY, | |
NUMBER, | |
CUSTOM_OBJECT, | |
PHONE, | |
SIGNATURE, | |
STRING, | |
} | |
interface ApplicantDataNode { | |
ApplicantDataNodeTypes getNodeType(); | |
// get the wrapped node, throws if wrong type | |
AddressNode getAddress(); | |
ArrayNode getArray(); | |
DateNode getDate(); | |
FileNode getFile(); | |
MoneyNode getMoney(); | |
NumberNode getNumber(); | |
CustomObjectNode getObject(); | |
PhoneNode getPhone(); | |
SignatureNode getSignature(); | |
StringNode getString(); | |
} | |
class CustomObjectNode { | |
Map<String, ApplicantDataNode> contents; | |
// Get the object or answer primitive containing the field(s) | |
// to be updated | |
// e.g. applicant.first_name -> applicant | |
// e.g. applicant.address.zip -> address | |
// e.g. applicant.dependents[1].employers[2].address.zip -> address | |
ApplicantDataNode getContainerForUpdate(UpdatePath updatePath) { | |
// do some recursion through objects and their child fields | |
} | |
} | |
class Update { | |
UpdatePath path; | |
// basically a "one of" | |
Value value; | |
} | |
class UpdatePath { | |
// { applicant, pet's names, 1 } | |
// e.g. "applicant.dependents[1].employers[2].address.zip" | |
// becomes | |
// { applicant, dependents, 1, employers, 2, address, zip } | |
List<String> path; | |
String getPropertyName() { | |
return Iterables.last(path); | |
} | |
} | |
class UpdateResult { | |
boolean success; | |
Map<String, String> validationMessages; | |
UpdateResult(success, validationMessages) { | |
// etc. | |
} | |
boolean isSuccess() { | |
return success; | |
} | |
} | |
class CustomObjectNode { | |
Map<String, ApplicantDataNode> properties; | |
Map<String, ApplicantDataNodeTypes> propertyMap; | |
Map<String, NodeValidation> propertyValidations; | |
// returns a validation message if it fails | |
// throws is value is of the wrong type | |
public Optional<String> update(String propertyName, Value value) { | |
ApplicantDataNodeTypes propType = typeFor(propertyName); | |
switch (propType) { | |
case PropTypes.NUMBER: { | |
updateNumberProp(propertyName, value.getNumber()); | |
break; | |
} | |
// etc | |
} | |
} | |
private ApplicantDataNodeTypes typeFor(String propertyName) { | |
return propertyMap.get(popertyName); | |
} | |
} | |
class CustomObjectUpdateStrategy extends UpdateStrategy { | |
CustomObjectValidator validator; | |
@Inject | |
CustomObjectUpdateStrategy(CustomObjectValidator validator) { | |
this.validator = validator; | |
} | |
UpdateResult update(ApplicantDataNode wrapperNode, Update update) { | |
CustomObjectNode object = wrapperNode.getCustomObject(); | |
String fieldName = update.getPath().getFieldName(); | |
object.update(fieldName, update.getValue()); | |
} | |
} | |
class AddressUpdateStrategy extends UpdateStrategy { | |
AddressValidator validator; | |
@Inject | |
AddressUpdateStrategy(AddressValidator validator) { | |
this.validator = validator; | |
} | |
UpdateResult update(ApplicantDataNode wrapperNode, Update update) { | |
AddressNode address = wrapperNode.getAddress(); | |
String fieldName = update.getPath().getFieldName(); | |
switch (fieldName) { | |
case "zip": { | |
// we know the value is a string because that's what goes in zip. | |
// if it's not a string then getString() throws. | |
address.setZip(update.getValue().getString()); | |
break; | |
} | |
// etc | |
} | |
// validator can call type-checked methods for accessing JSON-backed | |
// and computed properties, and doesn't need to check | |
// that the value its receiving for validation is actually an address | |
// | |
// returnas a map of field names to validation messages | |
Map<String, String> validationMessages = validator.validate(address); | |
return new UpdateResult(validationMessages.isEmpty(), validationMessages); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Applicant applicant = new Applicant() | |
.email.equalTo("rob@foo.bar") | |
.findOne(); | |
ObjectNode applicantData = applicant.getObject(); | |
ApplicantDataNode container = applicantData | |
.getContainerForUpdate(update.getPath()); | |
// each node type has a corresponding update strategy | |
UpdateStrategy strategy = updateStrategyFactory | |
.strategyFor(container.getNodeType()); | |
// type-specific update strategy allows for type-specific validation messages | |
UpdateResult result = strategy.update(container, update); | |
if (result.isSuccess()) { | |
applicant.save(); | |
return ok(...); | |
} else { | |
// or something like this, messages will need to be mapped to form input elements | |
return badRequest(formState, result.errorMessages); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment