views:

132

answers:

5

I'm implementing a class that wraps around an xml document with a very strictly defined schema. I don't control the schema.

One of the properties in the class is for an element value that the schema indicates must match a certain regular expression. In the setter for the property, if a string doesn't match the expression I'm throwing an exception.

My question is, how can I better communicate to users of my class the requirements for this field? Is there an attribute I can use? Xml comments (so it shows up in intellisense)? Should I do something other than thrown an exception? What other options do I have?

A: 

Document it in the XML comments, and throw an exception. Make the message explicit:

"Element <elementname> must match /regex/";

That's about all you can do.

chris
A: 

Either your code's documentation should address requirements, or the documentation for the schema should explain the requirements. You can't do anything for someone who doesn't bother to research the code they're about to use.

Lucas Oman
Of course the schema docs explain it, but the point of the class is to hide the need to use them. And my docs already explain it, but I was looking for something more "in your face" to help enforce it.
Joel Coehoorn
+1  A: 

XmlComments may help if you ship them with your assembly, but I would say that you are best off throwing exceptions if the requirements are not met, and making the exception message as detailed as possible. I would also throw exceptions (again with lots of detail) if the requirement is not met when the user calls and methods/properties the rely on the property.

There isn't really much you can do to keep someone using the code from making the mistake the first time, but you should be as clear as possible when the mistake does occur about how to correct it.

ckramer
upvote. I'm from the school of thought that exceptions should be, well, exceptional. If this code accepts input directly from the end-user, it might not be (ie: I don't want to force my class user to duplicate the regex just to avoid letting the end-user cause exceptions).
Joel Coehoorn
A: 

Thanks for the advice.

One idea I had while thinking about this was creating a new class named something like MatchedString to enforce the constraint.

It'd have a constructor that required a regex string, and after construction the expression would only be exposed to users via a read-only property. Then it would have a value property that users could set that would check against the expression in the setter.

My thought was that I could then also create options for different behaviors to use when the validation failed in an enum, and let the user specify which they want:

  • set to empty string
  • set to empty string and throw exception
  • set bad value anyway
  • set bad value anyway, and throw excpetion
  • just throw exception
  • do nothing

Also, I was thinking that this would allow my class user to do some basic tests without having to duplicate the RegEx object in their own code.

Any thoughts on this?

Joel Coehoorn
I believe this would be an appropriate refactoring in line with the Single Responsibility Principal (Classes should have only one reason to change).
ckramer
A: 

Whether or not I use it, implementing a MatchedString class looked like fun. So here it is:

Public Class MatchedString
    Public Enum InvalidValueBehaviors
        SetToEmpty
        AllowSetToInvalidValue
        DoNothing
    End Enum

    Public Sub New(ByVal Expression As String)
        Me.expression = Expression
        exp = New Regex(Me.expression)
    End Sub

    Public Sub New(ByVal Description As String, ByVal Expression As String)
        Me.expression = Expression
        exp = New Regex(Me.expression)
        _expressiondescription = Description
    End Sub

    Public Sub New(ByVal Expression As String, ByVal ThrowOnInvalidValue As Boolean, ByVal InvalidValueBehavior As InvalidValueBehaviors)
        Me.expression = Expression
        exp = New Regex(Me.expression)
        Me.ThrowOnInvalidValue = ThrowOnInvalidValue
        Me.InvalidValueBehavior = InvalidValueBehavior
    End Sub

    Public Sub New(ByVal Description As String, ByVal Expression As String, ByVal ThrowOnInvalidValue As Boolean, ByVal InvalidValueBehavior As InvalidValueBehaviors)
        Me.expression = Expression
        exp = New Regex(Me.expression)
        _expressiondescription = Description
        Me.ThrowOnInvalidValue = ThrowOnInvalidValue
        Me.InvalidValueBehavior = InvalidValueBehavior
    End Sub

    Private exp As Regex
    Private expression As String

    Public ReadOnly Property MatchExpression() As String
        Get
            Return expression
        End Get
    End Property

    Public ReadOnly Property ExpressionDescription() As String
        Get
            Return _expressiondescription
        End Get
    End Property
    Private _expressiondescription As String

    Public Function CheckIsMatch(ByVal s As String)
        Return exp.IsMatch(s)
    End Function

    Public Property ThrowOnInvalidValue() As Boolean
        Get
            Return _thrownoninvalidvalue
        End Get
        Set(ByVal value As Boolean)
            _thrownoninvalidvalue = value
        End Set
    End Property
    Private _thrownoninvalidvalue = True

    Public Property InvalidValueBehavior() As InvalidValueBehaviors
        Get
            Return _invalidvaluebehavior
        End Get
        Set(ByVal value As InvalidValueBehaviors)
            _invalidvaluebehavior = value
        End Set
    End Property
    Private _invalidvaluebehavior As InvalidValueBehaviors = InvalidValueBehaviors.DoNothing

    Public Property Value() As String
        Get
            Return _value
        End Get
        Set(ByVal value As String)
            If String.IsNullOrEmpty(value) Then
                _value = ""
                Return
            End If

            If CheckIsMatch(value) Then
                _value = value
            Else
                Select Case InvalidValueBehavior
                    Case InvalidValueBehaviors.AllowSetToInvalidValue
                        _value = value
                    Case InvalidValueBehaviors.SetToEmpty
                        _value = ""
                End Select

                If ThrowOnInvalidValue Then
                    Throw New ArgumentOutOfRangeException(String.Format("String: {0} does not match expression: {1}", value, MatchExpression))
                End If
            End If
        End Set
    End Property
    Private _value As String = ""

    Public Overrides Function ToString() As String
        Return _value
    End Function
End Class
Joel Coehoorn