5

I am facing a very interesting problem here, and I'd like to see some design ideas from domain driven design perspective.

To make it easier to express what I need I have following hypothetical situation. I have a bounded-context named Customer, which is used to handle create new customer, update customer, searching customer and so on. I have a customer class look like

public class Customer
{
    public int Id { get; set ; }
    public int SocialInsuranceNumber { get; set ; }

    public bool IsValid()
    {
        // my logic to validate social insurance number
    }
}

So I have a straightforward logic in my entity to validate the data. The Canadian Social Insurance Number validation rule indicates all values begins with 0 should not be used, but Canadian government might use it for some reason. So there are possible 2 validation rules here now.

The business has the requirement of one single application - for majority of the situations when user enters a social insurance number we have to make sure the value cannot start with 0 and the value must be a valid SIN, but in some unique situation we must allow a valid SIN starting with 0.

My domain service code looks like

public class CustomerService
{
    public void AddCustomer(Customer customer)
    {
        if (customer.IsValid())
        {
            // save to database
        }
    }
}

If I add a boolean parameter to Customer class's IsValid() function to make to look like IsValid(bool allowLeadingZero), I could make the code work. But I think it looks ugly.

From design perspective how would you suggest a more elegant way to deal with such requirement?

  • When you are loading data into the Customer object do you know if the 0 is allowed or not then? – AvetisG Mar 13 '15 at 17:39
  • Err, since this is just a hypothetical case I haven't thought about it yet. Maybe we just load the data as is, for now. – hardywang Mar 13 '15 at 17:46
  • How about adding a bool IsFiticiousSIN property to the Customer class? Alternatively, you can – rwong Mar 13 '15 at 19:01
  • There are many articles one could find on the internet that talk about the issues gorodinski.com/blog/2012/05/19/… – rwong Mar 13 '15 at 19:02
  • Smells like there are 2 bounded context here... at least at the problem level. Turning it into implementation might be overkill, but the smell is there. – ZioBrando Mar 15 '15 at 16:43
3

If I were you I would make SocialInsuranceNumber a Value Object with validation in its constructor. However, since the logic for validation is complex you might prefer to delegate the validation logic into a Domain Factory instead.

The factory will have a validation strategy injected into it depending on the type of the social security number you are validating.

So SocialInsuranceNumber's constructor will validate for NULL, empty, min length, illegal characters, ...etc, but SocialInsuranceNumberFactory will validate whether the pattern used in valid in case it was Canadian or not using a CanadianSocialInsuranceNumberValidator or DefaultSocialInsuranceNumberValidator.

1
public class Customer 
{
    public int Id { get; set ; }
    public int SocialInsuranceNumber { get; set ; }

    public bool IsValid()
    {
        // my logic to validate social insurance number
    }
}

First, avoid creating invalid objects. It only makes the code harder to maintain. If Customer is an object in your domain model, then it shouldn't be doing the data validation at all -- that's a job for the anti corruption component to deal with.

The usual answer is to create a value type for your state.

public class Customer
{
    public Id<Customer> id
    public SocialInsuranceNumber sin
    ...
}

The constructor for these types should throw an exception if the data isn't valid; therefore, the Customer entity doesn't need to worry about that at all.

The business has the requirement of one single application - for majority of the situations when user enters a social insurance number we have to make sure the value cannot start with 0 and the value must be a valid SIN, but in some unique situation we must allow a valid SIN starting with 0.

"Some unique situation" is a a bit vague; in DDD you would be expected to sit down with your domain experts and get a much better understanding of that.

First problem: do you need to support customers with SINs that use the leading 0? The only acceptable answers are "No, never", and "Yes". No, never is straight forward, so let's look at yes.

First, we create a value type for the looser rules of the SIN.

public class ValidSocialInsuranceNumber { public final String id;

   ValidSocialInsuranceNumber(String id) {
      // throw illegal argument exception of the sin isn't valid
      validate(id);
      this.id = id;
   }

}

public class SocialInsuranceNumber { public final ValidSocialInsuranceNumber validSin;

   SocialInsuranceNumber(ValidSocialInsuranceNumber validSin) {
       // throw an illegal argument exception if the sin
       // has been issued for Fictitious purposes
       forbidFictitious(validSin.id);
       this.validSin = validSin;
   }

}

Since we sometimes need to allow a valid but fictitious SIN, the customer needs to be modeled with the looser constraint.

public class Customer
{
    public Id<Customer> id
    public ValidSocialInsuranceNumber sin
    ...
}

But your anti corruption layer doesn't need to; instead, most paths can use the strict version to validate the user inputs, and then down grade to the other when the check is no longer needed.

addCustomer(String id, String sin) {
    customerId = new ID<Customer>(id);
    customerSIN = new SocialInsuranceNumber(sin);

    customer = new Customer(customerId, customerSIN.validSin)
}

If the sin doesn't validate under the strict rules, report the error back to the human operator, perhaps with a note to let them know that support does have a "unique" path available that they can execute on the operator's behalf.

Then, in your "unique" code path, use the more forgiving logic

addUniqueCustomer(String id, String sin) {
    customerId = new ID<Customer>(id);
    validSin = new ValidSocialInsuranceNumber(sin);

    customer = new Customer(customerId, validSin)
}

That's how I would handle it in the case as described, where real exceptions are less likely than typos.

When that's not the case, then I start looking to using a process; allow the user to be created (possibly in a disabled state), but also raise an event that an unusual SIN has been added, surfacing it for the business to act upon. Using process managers to mitigate contingencies is a common approach in

0

Well, I don't see any problem here, actually.

To consider something to be a validation rule of the object, you must be able to take an object and tell me exactly how do you validate this object to satisfy the rule. In your case there is no condition clearly defined when to apply this rule, so I would say you have no validation rule at all ;)

...in some unique situation...

So, make a flag of such a situation part of object state. And then you will be able to apply validation rule over it.

0

I suggest that you should separate the Customer and SocialInsuranceNumber entities into their own classes, as clearly both contain data and have behaviour:

  • Create a SocialInsuranceNumber class, and move the isValid() method into this class as a static method isValid(int CandidateSIN). It could be a member function, but that means it is possible to create invalid instances, which would probably not be wise. The SocialInsuranceNumber constructor should prohibit this.
  • Add an instance of SocialInsuranceNumber to your customer. Only populate this if the you have a valid SIN.
  • To handle the Canadian case, consider using a more general constructor and validation method like SocialInsuranceNumber(int SIN, String CountryCode) and isValid(int CandidateSIN, String CountryCode). Then you can bury the validation rules inside the SocialInsuranceNumber class.
  • The Customer class could have an isValid() method, but in the future this might need to handle (for instance) deceased or insolvent customers. If you need a hasValidSIN() method, then it only needs to check if the SIN is not null.

Slightly off topic, but the int type is not a good choice for a field that is optional. For instance, overseas customers may not have SINs.

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.