Reputation: 179
I try to understand a few unclear issues:
Which approach to composing objects/aggregates presented below is the best (examples below are simplified - normally I use vavr and FP instead of throwing exceptions)? DescriptionValidator
is just a port. In his implementation I'm currently asking other microservice about description
correctness.
a)
public class Description {
private final String text;
public Description(String text) {
this.text = text;
}
}
public class Item {
private final Description description;
public Item(Description description) {
this.description = description;
}
}
public class ItemService {
private final DescriptionValidator descriptionValidator;
private final ItemRepo itemRepo;
public void saveNewItem(String description) {
if(descriptionValidator.isValid(description)) {
itemRepo.save(new Item(new Description(description)));
}
}
}
b)
public class Description {
private final String text;
public Description(String text) {
this.text = text;
}
}
public class Item {
private final Description description;
public Item(String description) {
this.description = new Description(description);
}
}
public class ItemService {
private final DescriptionValidator descriptionValidator;
private final ItemRepo itemRepo;
public void saveNewItem(String description) {
if(descriptionValidator.isValid(description)) {
itemRepo.save(new Item(description));
}
}
}
c)
public class Description {
private final String text;
private Description(String text) {
this.text = text;
}
public static Description validated(String text, DescriptionValidator validator) {
if(validator.isValid(text)) {
return new Description(text);
}
throw new RuntimeException();
}
}
public class Item {
private final Description description;
public Item(String description, DescriptionValidator validator) {
this.description = Description.validated(description, validator);
}
}
public class ItemService {
private final DescriptionValidator descriptionValidator;
private final ItemRepo itemRepo;
public void saveNewItem(String description) {
itemRepo.save(new Item(description, descriptionValidator));
}
}
or d)
public class Description {
private final String text;
private Description(String text) {
this.text = text;
}
public static Description validated(String text, DescriptionValidator validator) {
if(validator.isValid(text)) {
return new Description(text);
}
throw new RuntimeException();
}
}
public class Item {
private final Description description;
public Item(Description description) {
this.description = description;
}
}
public class ItemService {
private final DescriptionValidator descriptionValidator;
private final ItemRepo itemRepo;
public void saveNewItem(String description) {
itemRepo.save(new Item(Description.validated(description, descriptionValidator)));
}
}
Upvotes: 1
Views: 1601
Reputation: 1241
I'll use your second code which contains a much more realistic example. Throughout this description I will refer to the Ubiquitous Language as UL. Here are a few things I have picked up from this code that will matter to the design
An Item has a description
)Let's now look at your invariants from the context listed above.
Items always have a description
. The best way to check for this kind of invariant is in the constructor of the Item itself. public class Item {
...
public Item(string description) {
if (description == null) throw NoDescriptionException();
}
}
Items have a max description of X length because <<Insert reason here>>
. In this case, again the check will go in the Item Constructor. public class Item {
...
private int MaxLength = 200;
public Item(string description) {
if (description == null) throw NoDescriptionException();
if (description.length > MaxLength) throw DescriptionTooLongException();
}
}
public class ProfanityService implements IProfanityService
{
...
public bool ContainsProfanity(string text){...}
}
public class Item {
...
public Item(Description description, IProfanityService profanityService) {
if (description == null) throw NoDescriptionException();
if (description.length > MaxLength) throw DescriptionTooLongException();
if (profanityService.containsProfanity(description)) throw TooProfaneException();
}
}
There is an argument to be made against putting something like the Profanity Checker in a constructor and if someone pushed back on me because of that, I wouldn't fight too hard to keep that design. There are other ways to handle it that would be equally as robust as long as you keep the logic for such a check contained within a context that makes better sense than in the Item's context.
A few notes about my sample code above:
Upvotes: 1
Reputation: 57259
TL; DR: Parse, Don't Validate
We normally prefer to convert unstructured/general-purpose inputs to structured inputs near the boundary. There are two reasons for this - first, that's the part of the code most likely to be able to report a problem correctly, and second that's the part of the code that is actually aware of the expected message format.
For instance, if we are dealing with something like an HTTP form submission, we normally want to verify that the data we find in the HTTP request actually conforms to our messaging schema (which is to say, the stable agreement about messages between this server and its clients).
Translation from the message schema to the in memory representations expected by the domain model is actually a separate concern.
That second concern should be addressed in your application layer - which is to say that it is the responsibility of your application code (the same "layer" that knows about your repositories, and transaction semantics, and so on) to translate from MessageSchema.Description to DomainModel.Description (and handle any edge cases where these two concepts are not actually aligned).
Validation within the domain value itself is a defensive programming tactic to ensure that the data structure within DomainModel.Description satisfies all of the preconditions necessary to ensure that the Description methods return consistent results.
Horses for courses.
Upvotes: 3