views:

113

answers:

2

As the title states I'm wondering if it's a good idea for my validation class to have access to all properties from my model. Ideally, I would like to do that because some fields require 10+ other fields to verify whether it is valid or not. I could but would rather not have functions with 10+ parameters. Or would that make the model and validator too coupled with one another? Here is a little example of what I mean. This code however does not work because it give an infinite loop!

Class User
    Private m_UserID
    Private m_Validator

    Public Sub Class_Initialize()
    End Sub

    Public Property Let Validator(value)
        Set m_Validator = value

        m_Validator.Initialize(Me)
    End Property

    Public Property Get Validator()
        Validator = m_Validator
    End Property

    Public Property Let UserID(value)
        m_UserID = value
    End property

    Public Property Get UserID()
        UserID = m_Validator.IsUserIDValid()
    End property End Class

Class Validator
    Private m_User

    Public Sub Class_Initialize()
    End Sub

    Public Sub Initialize(value)
        Set m_User = value
    End Sub

    Public Function IsUserIDValid()
        IsUserIDValid = m_User.UserID > 13
    End Function End Class

Dim mike : Set mike = New User 

mike.UserID = 123456  mike.Validator = New Validator

Response.Write mike.UserID

If I'm right and it is a good idea, how can I go a head and fix the infinite loop with the get property UserID?

Thank you.

Solution

<!-- #include file = "../lib/Collection.asp" -->

<style type="text/css">

td { padding: 4px; }

td.error 
{
    background: #F00F00;
}

td.warning 
{
    background: #FC0;
}

</style>

<%

Class UserModel
    Private m_Name
    Private m_Age
    Private m_Height

    Public Property Let Name(value)
        m_Name = value
    End Property

    Public Property Get Name()
        Name = m_Name
    End Property

    Public Property Let Age(value)
        m_Age = value
    End Property

    Public Property Get Age()
        Age = m_Age
    End Property

    Public Property Let Height(value)
        m_Height = value
    End Property

    Public Property Get Height()
        Height = m_Height
    End Property
End Class

Class NameValidation
    Private m_Name

    Public Function Init(name)
        m_Name = name
    End Function

    Public Function Validate()
        Dim validationObject

        If Len(m_Name) < 5 Then
            Set validationObject = New ValidationError
        Else
            Set validationObject = New ValidationSuccess
        End If

        validationObject.CellValue = m_Name

        Set Validate = validationObject
    End Function
End Class

Class AgeValidation
    Private m_Age

    Public Function Init(age)
        m_Age = age
    End Function

    Public Function Validate()
        Dim validationObject

        If m_Age < 18 Then
            Set validationObject = New ValidationError
        ElseIf m_Age = 18 Then
            Set validationObject = New ValidationWarning
        Else
            Set validationObject = New ValidationSuccess
        End If

        validationObject.CellValue = m_Age

        Set Validate = validationObject
    End Function
End Class

Class HeightValidation
    Private m_Height

    Public Function Init(height)
        m_Height = height
    End Function

    Public Function Validate()
        Dim validationObject

        If m_Height > 400 Then
            Set validationObject = New ValidationError
        ElseIf m_Height = 324 Then
            Set validationObject = New ValidationWarning
        Else
            Set validationObject = New ValidationSuccess
        End If

        validationObject.CellValue = m_Height

        Set Validate = validationObject
    End Function
End Class

Class ValidationError
    Private m_CSSClass
    Private m_CellValue

    Public Property Get CSSClass()
        CSSClass = "error"
    End Property

    Public Property Let CellValue(value)
        m_CellValue = value
    End Property

    Public Property Get CellValue()
        CellValue = m_CellValue
    End Property
End Class

Class ValidationWarning
    Private m_CSSClass
    Private m_CellValue

    Public Property Get CSSClass()
        CSSClass = "warning"
    End Property

    Public Property Let CellValue(value)
        m_CellValue = value
    End Property

    Public Property Get CellValue()
        CellValue = m_CellValue
    End Property
End Class

Class ValidationSuccess
    Private m_CSSClass
    Private m_CellValue

    Public Property Get CSSClass()
        CSSClass = ""
    End Property

    Public Property Let CellValue(value)
        m_CellValue = value
    End Property

    Public Property Get CellValue()
        CellValue = m_CellValue
    End Property
End Class

Class ModelValidator
    Public Function ValidateModel(model)
        Dim modelValidation : Set modelValidation = New CollectionClass

        ' Validate name
        Dim name : Set name = New NameValidation
        name.Init model.Name
        modelValidation.Add name

        ' Validate age
        Dim age : Set age = New AgeValidation
        age.Init model.Age
        modelValidation.Add age

        ' Validate height
        Dim height : Set height = New HeightValidation
        height.Init model.Height
        modelValidation.Add height

        Dim validatedProperties : Set validatedProperties = New CollectionClass
        Dim modelVal
        For Each modelVal In modelValidation.Items()
            validatedProperties.Add modelVal.Validate()
        Next

        Set ValidateModel = validatedProperties
    End Function
End Class

Dim modelCollection : Set modelCollection = New CollectionClass

Dim user1 : Set user1 = New UserModel
user1.Name = "Mike"
user1.Age = 12
user1.Height = 32
modelCollection.Add user1

Dim user2 : Set user2 = New UserModel
user2.Name = "Phil"
user2.Age = 18
user2.Height = 432
modelCollection.Add user2

Dim user3 : Set user3 = New UserModel
user3.Name = "Michele"
user3.Age = 32
user3.Height = 324
modelCollection.Add user3

' Validate all models in the collection
Dim modelValue
Dim validatedModels : Set validatedModels = New CollectionClass
For Each modelValue In modelCollection.Items()
    Dim objModelValidator : Set objModelValidator = New ModelValidator
    validatedModels.Add objModelValidator.ValidateModel(modelValue)
Next

%>

<table>
    <tr>
        <td>Name</td>
        <td>Age</td>
        <td>Height</td>
    </tr>
    <%

    Dim r, c
    For Each r In validatedModels.Items()
        %><tr><%
        For Each c In r.Items()
            %><td class="<%= c.CSSClass %>"><%= c.CellValue %></td><%        
        Next
        %></tr><%
    Next

    %>
</table>

Which produces Solution image

While not perfect, it's way better than what I started with. Basically, I decided to use the decorator pattern. My next step is to most likely remove the Init() function from each validate and replace it with a SetModel() function or something. That way each validate can have access to every property in my model.

Thanks all.

+2  A: 

I usually define a validator that validates an entire model; In this case, I would have a UserValidator class that has a method that accepts a User and returns a ValidationResult, which includes a list of validation errors.

This allows you to change the User class' implementation without affecting validation (eg, you don't have to add a new method to the Validator class every time you add a new property, or change a method signature if you want to change how the UserID is validated, etc).

Chris Shaffer
Could you please post an example?
Mike
+1  A: 

I think you are right in making the validator validate the entire model. To break the infinite loop, you can pass the value to the validator

Public Property Get UserID()
     UserID = m_Validator.IsUserIDValid(m_userID)
End property 

// in Validator
Public Function IsUserIDValid(userID)
    IsUserIDValid = userID > 13
End Function

Alternatively, if you prefer encapsulation, you can add Friend functions for accessing the property without validation.

Public Property Get UserID()
     UserID = m_Validator.IsUserIDValid()
End property 

Friend Function GetUserID()
   GetUserID = m_userID
End Function

// in Validator
Public Function IsUserIDValid()
    // "private" access - to get the unvalidated property
    IsUserIDValid = m_user.GetUserID > 13
End Function

A third way to do this is to separate your object from validation. The base class defines all the properites without validation. Then you define a child class that adds validation:

class User
    Private m_userID
    Public Property Get UserID()
         UserID = m_userID
    End property 
End Class

class ValidatedUser inherits User
   Public Overrides Property Get UserID()
       if (m_userID<15)
           // handle invalid case, e.g. throw exception with property that is invalid
       UserID = m_userID
   End Property

   Public Function Validate()
    ' class-level validation
   End Function
End Class

A final variation uses delegation to keep the basic user properties separate from the validated ones. We make User an abstract class, since we have to implementations - one with validation, and one without.

Class MustInherit User
   Public MustInherit Property Get UserID()
End Class

' A simple implementation of User that provides the properties
Class DefaultUser Inherits User
   Private m_UserID
   Public Overrides Property Get UserID()
      UserID = m_UserID
   End Property   
End Class

Class ValidatedUser Inherits User
   private Validator m_validator
   private User m_User

   Public Property Let Validator(value)
        Set m_Validator = value
        m_Validator.Initialize(m_User)
        ' note that validator uses m_User - this breaks the infinite recursion
    End Property

   Public Overrides Property Let UserID(value)
      m_User.UserID = value;
   End Property

   Public Overrides Property Get UserID()
      UserID = m_validator.IsUserValid();
   End Property
End Class   

In the last example ValidatedUser looks similar to your original code, but the key difference is that ValidatedUser itself doesn't have any property values - it delegates all property accessors to the m_User object. The Validator uses the m_user object which provides simple properties without validation, so the infinite recursion goes away.

At present, validation is done when the property is retrieved. I imagine this is done because you want to validate the data before it's used, and to avoid transient validation errors as properties are assigned. In addition to property-level validation, you may want to also define a "whole object" validation method that checks all properties on your object, particularly those involved in multi-property constraints. For example, if you have the constraint A+B+C < 50, then checking A B and C as separate properties will lead to that condition (A+B+C<50) being evaluated 3 times, which is unnecessary, and also confusing since the error will appear on one specific property, when it's really a problem with all 3 properties. Your object-level validator can check this condition just once and flag an error that indicates all 3 properties are not valid.

All of the above bind the Validation to the User class, so that clients can use User without concern for validation. There are benefits and drawbacks with this approach. The benefit is transparency - client's can use User objects and get validation behind the scenes without explicitly asking for it. The downside is that it ties validation very tightly in with your model. An alternative is to completely separate validation from the User object. This not only decouples validation, but also provides for "whole-object" validation. E.g.

' User is now a simple class (like DefaultUser above '
' with just properties, no validation '
Class UserValidator

   Public Function Validate(user)
     ' validate the given user object, return a list of
     ' validation errors, each validation error object
     ' that describes the property or properties
     ' that caused the validation error and why it's an error     
     ' E.g. '
     Dim ve As ValidationError 
     ve = new ValidationError
     ve.obj = user;   ' the object that failed validation
     ve.property = "userID"
     ve.msg = "userId must be < 15"
     ' potentially put several of these in a list and return to caller     
   End
End Class

Any code manipulating User will then have to explicitly call Validate after making changes, but this is usually not a problem, and the level of control is much better than having it done automatically. (In my experience,you almost always have to undo "automatic" actions at some point because they get in the way.)

I wrote more than I intended. I hope this is helpful!

PS: I don't do much VB, so please be lenient of the occasional syntax error. I am an OO programmer, so I know the principles are correct. And I just noticed the "asp-classic" tag - some of the examples use features that may not be available in classic asp, although the separate Validator code - the last example, should be fine on classic asp.

mdma
Woah! This post is very much appreciated, thank you. :)While my final solution isn't 100% what you posted. It's pretty close.
Mike