AboElzooz
AboElzooz

Reputation: 319

Do rules on value objects applied by my domain specification

A simple question lets say that i have an email value object in my domain, and in my domain the email can be (null, valid email), so in this case if i want to pass an email object to my contact_info value object which option that are more valid ? //from a ddd point of view

Option 1:

class email{
    public function construct($email)
    {
        if($email !== null)
        {
            // Assert that the email is valid
        }

        $this->email = $email
    }
}

class contact_info{
    public function __construct(Email $email){

    }
}

Option 2:

class email{
    public function construct($email)
    {
        // Assert that the email is valid
        $this->email = $email
    }
}

class contact_info{
    public function __construct(Email $email = null){
        if($email !== null){
            $this->email = $email;
        }
    }
}

Clearly option 1 is way better and its abstraction have a less error margin. but im new to ddd so im not sure :)

Upvotes: 1

Views: 296

Answers (2)

Wes
Wes

Reputation: 4296

Your first option is wrong because the class has two possible states and one is invalid, since null can't fullfill the Email interface. Just imagine using an instance of that class, you will have to check if the email is valid, even when the email is not optional. That makes type safety kinda pointless:

class Consumer{
    // email is not optional at all
    function __construct(Email $email){
        if(!$email->isActuallyAnEmail()) // WTH?
            throw new Exception;
    }
}

What you are trying to use there is the NullObject pattern. But NullObjects work better if they are used in place of behavioral-only interfaces; in your case, instead, you are modeling around scalar data, which is the email string: the NullObject pattern can be used as long it will appear to be working and doing things perfectly fine to caller code. If the interface is supposed to return scalar data somewhere (in your case the email string), a NullObject implementing said interface can only make that data up. So that's actually not a NullObject but what I call Fake/Example/Placeholder objects. Roughly:

// behavioral-only, a perfectly fine NullObject
class NullLogger implements Logger{
    function log($message){ return true; }
}

// this sucks. you are just converting null to errors
// or moving errors to another place
// this NullObject implementation is invalid
// as it doesn't actually fullfill the Email interface
class NullEmail implements Email{
    function __toString(){ throw new Exception; }
}

// not behavioral-only, an ExampleObject in this case
class ExampleEmail implements Email{
    function __toString(){ return "[email protected]"; }
}

And that, clearly, has an entirely different meaning. It's not an optional value anymore. The data is there but it's made up; it's an example value, a default value or just a placeholder.

So, the option 2 is the correct one (unless you are fine with having an [email protected] attached to your "contact info" object).

If you want to know more about null handling, check also the Option pattern / Maybe monad which is a bit more complex to understand; you can look at it but it is not strictly required.

Upvotes: 4

Rafał Łużyński
Rafał Łużyński

Reputation: 7312

You have two ValueObjects here: email and contact info, and both can have different invariants.

Your Email VO should be invalid if it has null value (Email(null) should raise an exception). Yet Your ContactInfo is valid even if email is not set.

In my opinion to constructor you should only pass values that are needed for object to be valid, if email is not needed then don't pass it there.

Of course default arguments makes things easier, but it often hides true meaning of setting a value.

In this example it's hard to see, but look at this:


class Person:
    def __init__(self, id, type=USER):
        self._id = id
        self._type = type

VS

class Person:
    def __init__(self, id):
        self._id = id
        self._type = USER

    def give_admin_permissions(self):
        self._type = ADMIN

We could pass ADMIN type directly to constructor, but is it really saying what we did? It's not too verbose.

Yet in Your example it's fine.

Upvotes: -1

Related Questions