Bambelal
Bambelal

Reputation: 179

Ways to creating Value Objects in DDD - which presented solution is the best?

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

Answers (2)

Louis
Louis

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

  1. Description is a value Object
  2. Item is the Entity that owns Description (so, in UL Parlance: An Item has a description)
  3. An Item contains a Description

Let's now look at your invariants from the context listed above.

  1. Description cannot be NULL: This invariant, in your UL, will translate to 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();
        }
    }
  1. Cannot be too Long - This is an interesting one because you have not given enough information to allow me to glean how this can be represented in your UL. I'll assume the most basic UL representation and say your intent is 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();
        }
    }
  1. Cannot contain Profanities: In this case, the logic for checking for Profanities may be involved and contain things that are most likely out of the boundary of the Item Entity. That logic doesn't fit too well into any concepts but a service. So
    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:

  1. I haven't even created a Description class yet because there's no need for it. Description can stay as a native string until my UL exposes a requirement that says otherwise.
  2. I didn't create a service to handle even the persistance of the entity because that doesn't belong in a service. The dynamics of implementing persistence in DDD is something that is relatively complex and there are many different ways to skin that cat. I usually have a Repository whose sole purpose is to persist the entity. So, I compose the new entity (directly by "newing it up" if it's simple enough or by using a Factory) and pass that entity to a repository ( through a call in the Application Service)
  3. My use of a service only occured when the concept described by the UL didn't fit anywhere else. You want to avoid creating an Anaemic Model. You want to instead create rich Domain Entities that express your UL. Another change I would make to your code to further drive this point home is to generate the Id for the Item, inside the Id itself. I would do this in the constructor of the Item class.

Upvotes: 1

VoiceOfUnreason
VoiceOfUnreason

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

Related Questions