As I understand, most of the business logic is stored in the value objects as constraints, like the price
cannot be less than 0.
But I have a problem where the value object depends on another value object as a constraint.
Says we have 2 value objects BasePrice
and Price
in the Product
entity. Both the BasePrice
and Price
could be set separately. But the Price
cannot be, says, greater than BasePrice + 1000
.
Since both of them can be set separately, if we update the BasePrice
, the Price
might be not valid anymore.
How to solve these dependencies between value objects?
3 Answers 3
Not all constraints can be validated within a value object.
If the constraint is about the relation between two VOs then the constraint has to be enforced by something that holds them both. In DDD this could be a third value object or an entity. In this case it sounds like that would be the Product
.
That object can throw throw an exception if there's an attempt to create an invalid combination of Price
and BasePrice
. The answer by Doc Brown shows a nice way of doing that.
-
You may take the time to read my answer to see how the described constraint can be validated within the value object
Price
.Doc Brown– Doc Brown12/21/2021 14:23:01Commented Dec 21, 2021 at 14:23 -
@DocBrown, yes that looks nice. It's still Product enforcing the invariant by throwing, but it's delegating most of the business logic to Price. You could also create a void function on price that asserts it's valid for the given base price and throws if not.bdsl– bdsl12/21/2021 14:27:12Commented Dec 21, 2021 at 14:27
-
You are right. The solution of @DocBrown is the
Product
enforcing the invariant.Ngọc Nguyễn– Ngọc Nguyễn12/21/2021 14:28:44Commented Dec 21, 2021 at 14:28 -
Both you and @DocBrown are great, but I could only mark one answer correct. Could you add a reference to DocBrown's answer? So I could mark your answer, since it's generally correct, and DocBrown's reference as a detailed implementation.Ngọc Nguyễn– Ngọc Nguyễn12/21/2021 14:31:30Commented Dec 21, 2021 at 14:31
A Price
object which depends on an updateable BasePrice
cannot be a value object, since value objects should be immutable (at least, by the book).
So if Price
and BasePrice
are both immutable, and Price
holds a reference to a BasePrice
, if one needs a price with a different base price, it will be necessary to create a new Price
object with a new BasePrice
passed in the constructor, so the old Price
object stays valid. However, if Price
and BasePrice
should both be properties of a Product
, this is probably not a good idea, since now the Price
object of the product might reference a different base price than the Product
itself.
Hence, when you want Price
and BasePrice
to be independent properties of your Product
, each one should not know anything about the other directly. Therefore, the constraint "Price must be between BasePrice and BasePrice+1000" makes sense only in the context of a Product
object. This constraint needs to be checked whenever a method like Product.SetPrice
or Product.SetBasePrice
is called.
Still, the business logic for checking against the base price can be part of the Price
object. Design the latter with a method IsInValidRange(BasePrice bp)
, and call it like this
class Product
{
BasePrice basePrice;
Price price;
void ChangePrice(Price newPrice)
{
if(!newPrice.IsInValidRange(basePrice))
throw new InvalidPriceException();
price = newPrice;
}
void ChangeBasePrice(BasePrice newBasePrice)
{
if(!price.IsInValidRange(newBasePrice))
throw new InvalidPriceException();
basePrice = newBasePrice;
}
}
I guess your issue lies in the the phrase "business logic stored in the value objects as constraints" - better replace "stored" by "implemented", this yields to the kind of solution I sketched above.
-
Yeah, the old
Price
stays valid but logically, thePrice
should be updated according to theBasePrice
, right? And when the data is read from the database, if we pass the updatedBasePrice
to thePrice
constructor, it is invalid. How do we let the user know about this?Ngọc Nguyễn– Ngọc Nguyễn12/21/2021 13:53:50Commented Dec 21, 2021 at 13:53 -
For example, Amazon has a policy about the price the sellers can set according to the base price. If Amazon sets a new base price, do the sellers have to set a new price too?Ngọc Nguyễn– Ngọc Nguyễn12/21/2021 13:55:09Commented Dec 21, 2021 at 13:55
-
2@NgọcNguyễn: how to handle constraint violations in your application is a completely different question than the one you asked, that's completely unrelated to value objects. But let me add some words on your scenario in my answer.Doc Brown– Doc Brown12/21/2021 13:59:13Commented Dec 21, 2021 at 13:59
-
Maybe I have misunderstood somewhere. As I mentioned in my question, the value object, in my understanding, is responsible to protect the business logic, checking the data if it is valid, as the length of a string, the value of a number, etc. So the
Price
in this situation should make sure its value is in relative range to theBasePrice
. If I misunderstand anything, could you kindly explain it to me, please?Ngọc Nguyễn– Ngọc Nguyễn12/21/2021 14:03:41Commented Dec 21, 2021 at 14:03 -
@NgọcNguyễn: see my edit. A value object can be made responsible for validating a constraint without holding a reference to other objects, just pass the required input through the validation method.Doc Brown– Doc Brown12/21/2021 14:14:40Commented Dec 21, 2021 at 14:14
It sounds like you have a thing
object that has two price values, not two separate objects. Since they have a dependency on each other it doesn't make sense to try and make the separate. From there your options are managing that relation in getter and setter methods of properties. In this case I would probably advise using explicit methods rather than something like a traditional .Net property, because there are side effects to changing these values that can be more clear with a method like thing.updatePricing(5, thing.Price)
than thing.BasePrice=5;
.
-
It's 2 separate objects. The
BasePrice
can't do anything with the discount and is only needed when setting thePrice
. ThePrice
, on the other hand, can do something with the discount. So that makes them separate objects.Ngọc Nguyễn– Ngọc Nguyễn12/21/2021 13:47:57Commented Dec 21, 2021 at 13:47