The Value object should contain only primitive values (integers, strings, Boolean flags, other Value objects, etc.).
It would often be better if the Value object itself defended its invariants . In the example quantity that you supply, he can easily do this by checking the input value without relying on external dependencies. However, I understand that you are writing
This is a very simplified example. My real object, I have serious logic in it that can also use other dependencies.
So, while I'm going to present a solution based on an example of quantity, keep in mind that it looks too complicated because the verification logic is so simple here.
Since you also write
I used the PHP example for illustration only. Responses in other languages are welcome.
I am going to answer in F #.
If you have external validation dependencies, but want to save the quantity as a value object, you need to separate the verification logic from the Value object.
One way to do this is to define an interface to test:
type IQuantityValidator = abstract Validate : decimal -> unit
In this case, I created a Validate template Validate the OP example, which throws exceptions when validation fails. This means that if the Validate method does not throw an exception, all this is good. For this reason, the method returns unit .
(If I had not decided to map this interface to the OP, I would rather use the Specification ; instead, I would instead declare the Validate method as decimal -> bool .)
The IQuantityValidator interface allows you to enter Composite :
type CompositeQuantityValidator(validators : IQuantityValidator list) = interface IQuantityValidator with member this.Validate value = validators |> List.iter (fun validator -> validator.Validate value)
This Composite simply IQuantityValidator through other instances of the IQuantityValidator and calls their Validate method. This allows you to create arbitrarily complex validator graphs.
A single sheet validator can be:
type IntegerValidator() = interface IQuantityValidator with member this.Validate value = if value % 1m <> 0m then raise( ArgumentOutOfRangeException( "value", "Quantity must be an integer."))
Another may be:
type GreaterThanValidator(boundary) = interface IQuantityValidator with member this.Validate value = if value <= boundary then raise( ArgumentOutOfRangeException( "value", "Quantity must be greater than zero."))
Note that the GreaterThanValidator class accepts the dependency through its constructor. In this case, the boundary is just a decimal , so it is a Primitive dependency , but it can also be a polymorphic dependence (AKA a Service).
Now you can create your own validator from these building blocks:
let myValidator = CompositeQuantityValidator([IntegerValidator(); GreaterThanValidator(0m)])
When you call myValidator , for example. 9m or 42m , it returns without errors, but if you call it, for example. 9.8m , 0m or -1m it throws the corresponding exception.
If you want to create something more complex than decimal , you can enter Factory and compose Factory with the appropriate validator.
Since the quantity here is very simple, we can simply define it as an alias of type on decimal :
type Quantity = decimal
A Factory might look like this:
type QuantityFactory(validator : IQuantityValidator) = member this.Create value : Quantity = validator.Validate value value
Now you can create an instance of QuantityFactory with your selection validator:
let factory = QuantityFactory(myValidator)
which allows you to enter decimal values as input and receive (check) Quantity values as output.
These challenges succeed:
let x = factory.Create 9m let y = factory.Create 42m
while they raise the corresponding exceptions:
let a = factory.Create 9.8m let b = factory.Create 0m let c = factory.Create -1m
Now all this is very difficult , given the simple nature of the example area, but as the problem area becomes more complex, the complex is better than complex .