Domanda

Hi I am making a Value Object.

 public class Age
    {
        public Age(int age)
        {
            Value = age
        }

        public int Value { get; private set; }
    }

I want to check that the value being passed inside the constructor is valid. It seems very reasonable and practical to me that we should put the validation inside the constructor and if the parameter is not valid then we should throw an exception.

Then I saw some stackoverflow post which advised against it and they made a very good point. They said that suppose: You have business requirement that age must be greater than 18. So you add a check in your constructor and if the passed age is less than 18 you throw an exception. But after some time requirement changes and now valid age is 21. So you change the validation logic to throw an exception if the age is less than 21.

Now the problem arises, how would you construct the object which were saved with the age 18 - 20?

So now I'm confused as to where I should my validation logic because for a ValueObject I think it should be inside the value object somewhere.

È stato utile?

Soluzione

So now I'm confused as to where I should my validation logic because for a ValueObject I think it should be inside the value object somewhere.

There are a few different things going on, which is a source of some of the confusion

When you write code like

public Age(int age)
{
    if age < 0 throw new ArgumentException(age);
    Value = age
}

The guard clause there isn't about validation, really, but is instead there to ensure that each instance of Age actually satisfies its contract (in this case, the promise that the integer value it returns is always greater than zero).

It's a form of defensive programming, to ensure that if errors are introduced in your program, that they are caught near the source and before they can corrupt the system.


When we are dealing with general purpose values (like "int") and we need to ensure that they satisfy more constraints, then the tool we want to use is parsing, not validating.

if age >= 0 {
   return new Age(age)
} else {
   // ???
}

The test you are making may look the same (and in some cases, you will want to remove the duplication), but the tests are really serving two different purposes, that should not be confused with one another.


You have business requirement that age must be greater than 18.

This part here is a modeling problem. What we are describing here is a policy requirement, and as you noticed, policies can change over time.

So what we should be doing is modeling the policy (and the fact that we might change policies) explicitly.

That can be especially important in cases like "We've changed the eligibility requirements, we we are going to grandfather in everybody that was eligible under the previous rules".


"Input validation" -- for example, checking data submitted from a web form, isn't typically a domain concern. It's not usually the concern of the domain to look for typos (that would normally happen somewhere else).

But in some domains, it does make sense to distinguish between unverified and verified information, and to include different rules/procedures for converting from one to the other (think "email address").

Altri suggerimenti

I prefer a static factory method for new object creation.

In it's most basic form, that looks like this:

public class Age
{
    public int Value { get; private set; }

    private Age(int age)
    {
        Value = age
    }

    public static Age Of(int value)
    {
        // perform validation, throw exception if value doesn't meet the requirements

        return new Age(value);
    }
}

Notice the private constructor.

The problem with exceptions is that they should be used for exceptional situations. And while that is actually the case for a negative age, other scenario's, such as a minimum age of 18, don't justify the use of exceptions. In these cases I use a generic Result<T> class:

public static Result<Age> Of(int value)
{
  // perform validation, return Result.Failure("Custom error message") if value doesn't meet the requirements.

  return new Age(value);
}

To implement the business requirement that age must be greater than 18, you could return a failed result explaining why, but you can also model it more explicitly:

public class Age
{
    public int Value { get; private set; }
    public bool IsAdult => Value > 18;

    private Age(int age)
    {
        Value = age
    }

    public static Age Of(int value)
    {
        // perform validation, only check for exceptional input, throw an exception for negative values or values above the valid upper limit

        return new Age(value);
    }
}

Now if the rule changes from 18 to 21, everything can still be loaded, but the IsAdult property will no longer be true for ages between 18 and 21.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
scroll top