How to share validation between Forms and Value Objects in Domain Driven Design?
Asked Answered
T

1

8

#1. Validate EmailAddress on the Form

I have a backend form class with an emailAddress property that has validation logic so that I can return an error message back to the user. I validate all form inputs with something like:

$form->fillWith($request->input());

if($form->validate()){
    $form->dispatch($command); // if synchronous, form takes command's messageBag
}

return response($form->getMessageBag()->toJson());

#2. Validate EmailAddress Value Object in the Command Handler

I have a command handler that will take the primitive string email and create a value object. The value object will throw an exception on creation if the email is invalid:

public function handle($command){

   try {
      $emailAddress = new ValueObjects\EmailAddress($command->emailAddress);

      // create more value objects...

      // do something else with the domain...

   } catch (DomainException $e) {
        $this->messageBag->add("errors", $e->getMessage());
   } catch (\Exception $e) {
        $this->messageBag->add("errors", "unexpected error");
   }

   return $this->messageBag;
}

In #1, I want to capture validation early before I dispatch a command. But then in #2 that validation logic is repeated when I build VOs.

Issues I have:

  • If I need to change validation requirements on email addresses, then I have to update both places.
  • If I use VOs on my form then I will have to deconstruct them again when passing to the command. Also, if my form is in a different Bounded Context then I will have VOs leaking domain from the other Bounded Context (maybe this is necessary?).

So my question is, should I create some validator objects that both my form validation and VOs can share/utilize? Or how do I capture repeated validation concerns between forms and value objects?

Touzle answered 19/10, 2015 at 17:25 Comment(4)
This answer might help... read the "How to report back..." section.Troposphere
@Troposphere ok. Sounds like I just go with some form Validator Objects to use for different entry points from the UI. Then create my Value Objects without these and just don't worry about repeated logic. If email requirements change then I need to update 2 places instead of 1, but I'm a full stack guy as you say.Touzle
If you want to form this into an answer then I can accept it. You helped me gain the insight I needed to get a comfortable approach. Thank you!Touzle
Well, I don't have the time right now, but I'm glad I could help! By the way, if the validation logic is so complex to express that you really don't want any duplication, then you may expose a validator web service that the UI can call. For instance, if you had password complexity policies, but various policies depending on account types, then duplicating all that would not be very practical.Troposphere
J
6

Encapsulate the validation logic into a reusable class. These classes are usually called specifications, validators or rules and are part of the domain.

There are multiple ways of doing this, here is an approach that I use:

  1. Define an interface Specification that provides a bool IsSatisifed() method.
  2. Implement this interface for a specific value object, e.g. EmailWellformedSpec.
  3. Enforce the business rule within the domain by using the spec as precondition (i.e. violation is always a programming error).
  4. Use the spec for input input validation in the service layer (i.e. violation is a user error).

If you want to combine multiple specs to a larger one, the Specification Pattern is a good approach. Note that you need to pass in the data through the constructor if you use that pattern, but this is not a problem because the specification classes are usually simple.

Jeopardy answered 20/10, 2015 at 7:35 Comment(6)
I'm a little lost here @theDmi. So you are saying that I can extract the duplicate validation parts that are on my backend form class and are in my value object and put them into a single specification? The specification would be used perhaps directly on the form and also would be used inside the value object? So I can make any change to EmailWellFormedSpec implementation without changing its interface and both my form and VO stay DRY this way (even though the validation will still be ran twice, but that's not a SOLID issue and is just insignificant micro-performance decrease).Touzle
@Touzle Yes, you got it. The only thing I wouldn't do is use the specification inside the value objects, because that makes them awkward to use (you'd have the spec on the constructor then). I'd use the spec in the domain where the VO is used, e.g. in a method of an entity.Jeopardy
Oh you don't like the spec being on the VO's constructor? Seems like the spec is part of the invariants that an EmailAddress VO would be comprised of, right? Also I was thinking of having entity methods require the fully formed VO (instead of a.primitive string or the EmailWellFormedSpec etc.). Otherwise I'm going have a much bigger set of arguments on my entity methods. Think about Address for example. That's 5 fields for example. But also maybe I see what you are suggesting in that the VOs are supposed to be hidden behind the aggregate root maybe?Touzle
My suggestion was not to pass primitives to entity methods. Pass value objects, and make it a precondition of the method that the passed VO is valid by validating it against the specification.Jeopardy
I'm sorry I'm on my phone and my thumbs are so limited! Let me make one more comment tomorrow for last clarification and then I'll +1 you for that. Thanks for responding so well here! I need to review specification pattern again before I return back as well.Touzle
@Jeopardy I don't quite get it. You suggest to pass a VO along with Specification it has to satisfy and do a check inside an entity method? What if I have multiple methods operating on this VO, would you do the same check in each of them? How is this VO different from a primitive if it's just a wrapped value without any validation? Would you please share some code snippet of the above?Adamec

© 2022 - 2024 — McMap. All rights reserved.