1

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?

2

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.

5
  • 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
    Dec 21 '21 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
    Dec 21 '21 at 14:27
  • You are right. The solution of @DocBrown is the Product enforcing the invariant. Dec 21 '21 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. Dec 21 '21 at 14:31
  • @NgọcNguyễn Added. Thanks.
    – bdsl
    Dec 21 '21 at 15:12
5

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.

5
  • Yeah, the old Price stays valid but logically, the Price should be updated according to the BasePrice, right? And when the data is read from the database, if we pass the updated BasePrice to the Price constructor, it is invalid. How do we let the user know about this? Dec 21 '21 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? Dec 21 '21 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
    Dec 21 '21 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 the BasePrice. If I misunderstand anything, could you kindly explain it to me, please? Dec 21 '21 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
    Dec 21 '21 at 14:14
0

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;.

1
  • It's 2 separate objects. The BasePrice can't do anything with the discount and is only needed when setting the Price. The Price, on the other hand, can do something with the discount. So that makes them separate objects. Dec 21 '21 at 13:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.