Today, on a conference call with Hal Helms, we started to talk about domain modeling and which objects in the application would be responsible for what logic. The example we had on hand was that of an account transfer. We decided that it would be good to model the Transfer itself as a domain object that might have the following subset of class properties:
- OriginatingAccount (Account)
- TargetAccount (Account)
- Amount (Money)
One of the benefits of having a Transfer class was that you could subclass it for specialization. For example, if your application would only allow for a given account to have 4 transfers a month, then we could have some sort of StandardTransfer class that extends the Transfer base class and has logic to allow for only 4 transfers per month.
I have gotten into the mentality of thinking about classes as data types and with that, I have accepted the idea that a data type can only exist in a valid state (otherwise, it cannot uphold the contract of the given data type). As such, I asked Hal if this subclass, StandardTransfer, should throw an exception in the constructor (Init() method) if the given OriginatingAccount has already executed four transfers in the current month. To me, it seems that in order for the StandardTransfer to be in a "valid state", it would need to contain an originating account property that could perform the transfer. If the OriginatingAccount could not perform the transfer, then it would seem to me that the StandardTransfer would not be in a valid state.
Hal disagreed with this. He believed that the validity of the class was defined by its data types and not their capabilities. Meaning, as long as the subclass was passed two Account classes and a Money class, then it was in a valid state. What he argued would be "invalid" would be the execution of the Transfer's Execute() method. The Execute() method would throw an exception if called. The thinking here was that the StandardTransfer class would have some sort of IsValidTransfer() method that would check the transfer properties in the context of the 4-transfer-limit business logic. To say it another way, the object was valid, but some of its method executions would not be valid.
I have to say that this rocked my world in a huge way and I'm having trouble reconciling prior Hal Helms teachings with this concept.
I think this really took the legs out from under what I considered a "valid state" object. To me, the validity of an object was modeled as the combination of each data type and its meaning within the application. For example, in order for an "Account" class to be in a valid state, it would need to have an "account number" string property AND that account number property could not be the empty string (for example).
But if we take what we see in the Transfer / StandardTransfer example, and apply it to the smaller example of the Account object, what we see is that an object's validity is a function of its composed data types and NOT what the value of those data types actually are. So, if you can create an object who's data types are valid, but who's composed values are NOT valid in the context of the application, then somebody, somewhere needs to know if the object is valid from a contextual standpoint.
At this point, you might be asking yourself where I am going with this line of reasoning? Really, what I'm having trouble reconciling is Hal's belief that you should never call IsValid() (or Validate()) on an object (a point that was made very clear in my first Real World OO class). His reasoning behind this was that an object should never be able exist in an invalid state, so calling Object.IsValid() would be a non-sequitur. However, if the "valid state" that he is referring to is a function of data type, not data value, then validity, in the way that we generally think about it - in the context of the application - is completely unrelated to the concept of object validity.
As such, either the service class that is creating and populating objects needs to have some sort of IsValid() method call (to which we would pass an object instance for business-logic validity) or, the object itself needs to have an IsValid() method that checks business-logic validity.
So, which one is it?
If we have an IsValid() method on the object, then this really goes directly against what Hal taught me previously?
If we have an IsValid() method on the service class, then we start to create "bloated" service layers and anemic domain objects?
(I use question marks because I am not sure if this logic is sound)
The other option we have is to put business-logic validation in the Controller. But, if we do that, then we lose out on code reuse, forcing us to duplicate validation logic anywhere the Controller needs to create a given object.
And so it is that a seemingly simple conference call has inadvertently rocked my world and turned my mental model of Object Oriented Programming on its head.