Flexible rules adapting to the state of an object?

Flexible rules adapting to the state of an object?

Old forum URL: forums.lhotka.net/forums/t/212.aspx


CrispinH posted on Saturday, May 27, 2006

There are instances where business rules need to vary according to circumstance.  This would occur as a contact moved from being a prospect to a customer - you may only need a name, organisation name and phone number initially, but once the contact becomes a customer, then you would also need an account number, invoice address, shipping address, etc.

Now "object models aren't the same as relational models" (p50 VB 2005 book).  I'd arrived at this conclusion before reading the book, but Rocky articulated it for me.   Thus we have to think of objects as separate entities from the underlying tables. 

Method 1:  Create a prospect object and a customer object that operate on subsets of a single SQL Server table.  Each object would have it's own validation rules and could be saved when those rules were met.

Method 2: Create a single (stateful) object that can handle the change of state from prospect to customer and dynamically adapt its rules accordingly.

The question I have - which is likely to be easiest to maintain?  Has anyone had any experience of this situation?  Any good methodologies?

If it's method 2 that's optimal, then the rules collection needs to be aware of the state of the object.  Does this mean that multiple rules collections need to be created, or is it best to have state dependent rules?

There's another thought that occurred to me: sometimes it would be good to be able to save an object event though its state is invalid (ie broken rules count > 0) for those occasions when the user is asked for a bit of information they haven't got to hand.  This means that IsSavable is now the combination of IsDirty and IsSufficientlyValidToSave.  A the risk of stretching my example too far, if someone on high decides that prospect records have to include name, phone and email to be 'valid', there still may be a case of being able to save the record with name AND (phone OR email).  So the states for this object would enumerate as: invalid prospect, invalid prospect but saveable, valid prospect and saveable, invalid customer but almost certainly saveable, valid customer.

Incidentlaly I'm using the customer/prospect example as just that, an example.  I've got other stateful objects in mind that are too complicated to explain in a hurry.

Crispin

 

 

Mark replied on Saturday, May 27, 2006

I'd recommend Method #1.  Before I "saw the light", I tried using Method #2.  I ended up having all sorts of maintenance issues.  There were so many conditional checks to see what state the object should be in - making any sort of change required quite a bit of effort.

Just remember - you don't need a 1:1 relationship between your objects and your DB.  Define your objects based on your use cases, reuse when possible, but don't try to force one object to do something that it's not responsile for.

hurcane replied on Monday, May 29, 2006

If you go with different objects, you're likely to run into a situation where another object needs to collaborate with either of the two objects. Extending the given example, you might have a quote object that is used to provide prices to prospects or customers. You could make a third object, quote customer, for this, or you could create a quote customer interface and implement it on the prospect and customer objects.

Another alternative to consider is having a CustomerBase object that defines the behavior (and data) shared between the customer and prospect. Customer and Prospect would both inherit from this class.

It's hard to say what is the best practice because the devil is in the details. I think even experts in OOD (which I'm not) make choices that they realize later weren't the best choice. That's when you start refactoring.

malloc1024 replied on Sunday, May 28, 2006

You may also want to look into the State pattern.  This will allow the object to change its behavior when its internal states change.

rhoeting replied on Monday, May 29, 2006

First of all, this is a great question.  In fact, I often argue that CSLA enables us to raise the discussion to a more abstract and meaningful level.

As we design apps, we need to make many of these little decisions.  Both methods seem reasonable.  I've often found that if I resort to the OO mantra "Objects are defined by behavior, not data" making such decisions become much easier.  You've described some behavior differences that suggest two objects rather than one. You also may want to perform a little speculative alalysis, trying determine if the "customer" and "prospect" will need to evolve along separate paths.  If so, it strengthens the argument decoupling the two objects, if not, perhaps combining them is better.

If you do decouple them, you may want to create a new factory method on your customer object, something like "Customer.NewCustomerFromProspect(Prospect)" that allows a seemless transition from prospect to customer.

Also, you need to be careful how you think of CSLA business rules (which govern IsValid).  They only reflect one aspect of object behavior.   That is, they are "rules that govern savability."   You also have rules that govern other aspects of object behavior.  For example, you may have a rule on your prospect object that governs whether or not it can be promoted to Customer.  In this case, you'd simply create a brand new Readonly Boolean Property called "CanBecomeCustomer."  So let's say the users say, prospects must have last name to be promoted.  The body of the rule would be simply:

Public Readonly Boolean Property CanBecomeCustomer as
    Return LastName.Length > 0 
End Property

It says nothing about savability of a prospect, but it does say something about it promotability to customer.  Of course, you'd have to consume that rule in other places in the app to make is useful, such as binding it to a "Promote To Customer" button enabled flag, or perhaps in a workflow type object that helps the two objects collaborate.

Hope this helps

Rob



Igor replied on Monday, May 29, 2006

Rob,

 

I think that I understand what you mean when you say: "... CSLA business rules (which govern IsValid).  ...  they are “rules that govern savability.”"

 

But:

 

CSLA gives us a way of finding what rules are currently broken in the BO. How to use this information is up to us. We know that CSLA has a built-in support for savability (the IsSavable property that tells us if any rule is broken). But my savability requirements may be different, in which case I can simply override the IsSavable property.

 

On the other hand, I may have other interesting/important sets of rules: say a set of rules that determine if a quote can be converted to an invoice. It is reasonable to use in this case a property called IsConvertable, which is true if and only if none of the conversion rules are broken. I can have on my form a Save button bound to the IsSavable property and a Convert button bound to the IsConvertable property.

 

In my applications (CSLA 1.5) I do have multiple buttons on the same form. The buttons are enabled/disabled according to the object's current state and on the role membership of the current user. If a button is disabled the user sees why in an ErrorProvider message.

 

Igor

 

xal replied on Monday, May 29, 2006

Why not create the rule based on the type? The code inside the rule would look like:

If Me.ContactType = ContactTypes.Customer Then
    If mInvoiceAddress.Trim().Equals(String.Empty) Then
        e.Description="Invoice address is required for customers"
        Return False
    End If
End If
Return True


Andrés

CrispinH replied on Tuesday, May 30, 2006

Thanks for the input so far - there have been some good ideas.

Picking up on Mark's point about using separate objects, this then begs the question as to whether to use inheritance (mentioned by Hurcane) or interfaces to maintain consistency between the prospect and customer objects.  Since it's possible that the behaviour of the customer object may differ from that of the prospect, using an interface might be a better solution.  Thinking aloud, an interface would be better in the situation (say) where a customer is also a vendor - the object would then implement IProspect, ICustomer and IVendor.  With inheritance you'd run into a muddle - just consider that you might have ProspectiveVendor objects as well as Vendor objects...

With regard to using the state pattern (suggested by malloc104) - the problem I have is lack of familiarity with patterns in general.  However I did a quick foray into the state pattern and although it seemed very powerful, my feeling was it was more than was required in this instance - though I'm happy to be proved wrong.

What seems to be emerging is that if an object changes state sufficiently then you should consider two objects rather than one.  This poses the question: what constitutes sufficiently?  The answer to that is to count the behavioral changes of the object in 'state 2': if it's more than three or four, then it's new object time.

Crispin

SoftwareArchitect replied on Tuesday, May 30, 2006

I faced a similar problem and since that solution hasn't been mentioned....here it is to consider.

In my case, I had an address object.  Sometimes it was a ContactAddress requiring a phone number and sometimes it was a PlayerAddress requiring a postal code.  So, the objects all contained the same properties but needed to implement different rules based on...well....really their parent.

I first (and I later refactored) created an abstract base class and began creating specific classes that inherited from that class on top of it.  It worked well and I would use that in certain circumstances; however, I quickly realized that with each address, I was creating quite a few differing address type objects and the maintenance of those would have been too great.

So, I created an AddressValidationArgument class that contained ALL of the various requirements for all addresses in the system.  I pass that in to the NewAddress factory method and also into the GetAddress factory method.  These in turn get passed into the constructor and the proper validation rules added for each.

The entire discussion in in the old forum
http://www.searchcsla.com/Details.aspx?msn_id=26557

Hope that helps. 
Mike

stefan replied on Tuesday, May 30, 2006

Mike,

I just read the complete discussion from the old forum and I remembered having read it
when it was on top. I also had the impression of having seen some more code examples
showing the AddressValidationArgs class and the passing of behaviour-configurating-data
to the constructor. Must have been in another thread...

Could you post some code schema in the sense of a short how-to,
showing the implementation of what you did?

That surely would make this thread an all-time-goody in our new forum ;-)

Thanks anyway for bringing this topic back...

Stefan

SoftwareArchitect replied on Thursday, June 01, 2006

Basic pseudo code - lots of stuff omitted but this should get you the general gist. 

The following example contains 3 classes.  The parent class (Customer), the child address class (Address) and the validation class for addresses (AddressValidationArgs).  This shows that for a customer, the postal code is required.   Assume that for some other parent, the phone number is required.

It will probably be easier if you just cut and paste this into a code window so that the layout is readable.


Class Customer
    private _name as String
    private _address As Address = Address.NewAddress(new AddressValidationArgs(True, False))

    '... etc.
End Class


Class Address

   Private _street1 As String
   Private _city As String
   Private _state As String
   Private _phone as String
   Private _postalCode as String

   Private _validationargs As New AddressValidationArgs()

    Protected Overrides Sub AddBusinessRules()

        ValidationRules.AddRule(AddressOf CSLARules.LengthMax, "Street1")
        ValidationRules.AddRule(AddressOf CSLARules.LengthMax, "PostalCode")
       ... etc

        ' Since AddBusinessRules is called from the base constructor, and since it
        '  calls into the most derived method, the _validationargs variable will NOT
        '  yet have been instantiated when called the FIRST time. 
        If Not _validationargs Is Nothing Then

            ' Validation rules that change based on who is using this object (player, coach, guardian, etc.)
            If _validationargs.RequirePostalCode Then ValidationRules.AddRule(AddressOf Validation.CommonRules.StringRequired, "PostalCode")

              '... process other validation args here

        End If
    End Sub


    'default address - creates a default address validation args class
    Friend Shared Function NewAddress() As Address

        Return NewAddress(New AddressValidationArgs())

    End Function


    Friend Shared Function NewAddress(ByVal args As AddressValidationArgs) As Address

        Return New Address(args)

    End Function


 Friend Shared Function GetAddress(ByVal dr As SafeDataReader) As Address

        Return GetAddress(dr, New AddressValidationArgs())

    End Function

    Friend Shared Function GetAddress(ByVal dr As SafeDataReader, ByVal args As AddressValidationArgs) As Address

        Return New Address(dr, args)

    End Function

'  Constructors
 Private Sub New(ByVal dr As SafeDataReader, ByVal args As AddressValidationArgs)

        _validationargs = args
        AddBusinessRules()  'now that _validationargs is set, re-add business rules (MUST DO HERE)

        MarkAsChild()
        Fetch(dr)

    End Sub


    Private Sub New(ByVal args As AddressValidationArgs)

        _validationargs = args
        AddBusinessRules()     'now that _validationargs is set, re-add business rules (MUST DO HERE)

        MarkAsChild()
        ValidationRules.CheckRules()

    End Sub

    '...data access here -- removed --
  
End Class



Public Class AddressValidationArgs

    Private _requirePostalCode As Boolean = False    
    Private _requirePhone1 As Boolean = False          

    Public Property RequirePostalCode() As Boolean
        Get
            Return _requirePostalCode
        End Get
        Set(ByVal value As Boolean)
            _requirePostalCode = value
        End Set
    End Property

    Public Property RequirePhone() As Boolean
        Get
            Return _requirePhone1
        End Get
        Set(ByVal value As Boolean)
            _requirePhone1 = value
        End Set
    End Property

    Public Sub New(ByVal requirePostalCode As Boolean, _
                   ByVal requirePhone As Boolean)

        _requirePostalCode = requirePostalCode
        _requirePhone1 = requirePhone

    End Sub

   'Default Constructor - nothing is required
    Public Sub New()
    End Sub


End Class

stefan replied on Thursday, June 01, 2006

Thanks a lot Mike for that piece of code!

Today I found where I read about some sort of "Case dependant validation" a few days ago. It was a blogpost on a very interesting blog on the web...

http://www.jpboodhoo.com/blog/ValidationInTheDomainLayerTakeOne.aspx

I think that Blog is worth mentioning (and a bookmark)!

Thanks again,

Stefan

malloc1024 replied on Friday, June 02, 2006

Using a ValidationArgument is a good way to go if you are not going to add more rules to a class.  However, it does violate the open-closed principle.  Every time you want to add a rule you would have to change code instead of just adding code.  Further, it also produces ugly if-else or case statements in the AddBusinessRules method.  Using the state or strategy pattern will eliminate these problems and give you more flexibility and better code

Copyright (c) Marimer LLC