@Michael:
Okay, I had to laugh at that.
Admittedly, it's a work in progress. However, I will gladly post what I have, if there's a decent place to post it. (I've complained about the lack of a unified online Peer Review Forum before. See here.)
-------------
In the interests of
fostering discussion, here's what I have (edited down). Bear in mind that's
VB.NET, and that I haven't gotten around to localizing it into resource files
yet. (I figure I'll do that when I get the green light from folks who have
determined that it's a worthwhile endeavor.)
Key principles:
1. It had to be SIMPLE.
2. It had to be EASY TO USE.
3. It had to WORK.
4. It had to be FAST.
5. It had to be EASY TO UNDERSTAND.
6. It had to be EASY TO MAINTAIN.
The goal of the “library” “framework”
or whatever you want to call it is to provide a standard means of validating
method parameters that encourages their consistent use throughout the
application. The basic premise: parameter validation code tends to be verbose,
discouraging me from doing it. By making it more terse, and by taking advantage
of IntelliSense, I might be more inclined to do it at the start of every
method, or to at least think about
it.
I used NUnit’s Assert.That() statement as a model for the
interface, since it was familiar, and the learning curve would be small. Under
the hood, however, the mechanics are radically different, since NUnit relies
heavily on reflection (not an option for a library where the methods may be
called multiple times on every layer of the call stack).
For a very basic example,
we'll look at the ConnectionValidator class. In order to validate a
parameter, you invoke the heavily overloaded
That
method of the Validate class. (For connections, in most cases, the parameter name
is always “connection”, so the Validate class provides an overload that
provides that default name for you.)
Option Strict
On
Imports System.Data
Namespace Validation
Public NotInheritable
Class Validate
Public Shared Function That(ByVal value As IDbConnection) _
As ConnectionValidator
Return
New ConnectionValidator(value, "connection")
End Function
Public Shared Function That(ByVal value As IDbConnection, _
ByVal
parameterName As
String) _
As ConnectionValidator
Return
New ConnectionValidator(value, parameterName)
End Function
End Class
End Namespace
Basically, Validate is just a factory; it spits out
parameter validator objects, whose type is determined by the parameters that
you pass. Once you get back an appropriately typed parameter validator, you’re
free to invoke the methods that allow you to test the parameter for its
suitability.
The ConnectionValidator class below provides the methods to
test a database connection’s suitability. (Its base class is shown immediately
below it.)
Option Strict
On
Imports System.Data
Namespace Validation
Public Class
ConnectionValidator
Inherits ParameterValidatorBase
Friend Sub New(ByVal connection As IDbConnection, ByVal name As String)
MyBase.New(connection, name)
End Sub
Public Sub IsClosed()
If Not Connection Is Nothing Then
If
(Connection.State
And ConnectionState.Closed) = 0 Then
Throw
New ArgumentException("Operation requires a closed connection.",
Name)
End
If
End If
End Sub
Public Sub IsNotNull()
If Connection Is Nothing Then
Throw
New ArgumentNullException(Name)
End If
End Sub
Public Sub IsOpen()
If Not Connection Is Nothing Then
If
(Connection.State
And ConnectionState.Open) = 0 Then
Throw
New ArgumentException("Operation requires an open connection.",
Name)
End
If
End
If
End Sub
Public Sub IsValid()
IsNotNull()
IsOpen()
End Sub
Private ReadOnly Property Connection() As IDbConnection
Get
Return
DirectCast(InnerValue,
IDbConnection)
End Get
End Property
End Class
Public MustInherit
Class ParameterValidatorBase
Protected
Sub New(ByVal value As Object, ByVal name As String)
m_value
= value
m_name
= name
End Sub
Protected
ReadOnly Property
InnerValue() As
Object
Get
Return
m_value
End Get
End Property
Public ReadOnly Property Name() As String
Get
Return
m_name
End Get
End Property
Private m_value As Object
Private
m_name As String
End Class
End Namespace
The library contains
strongly-typed validator classes for each of the intrinsic data types (String,
Date, integer, long, short, boolean, and so on), transactions, and connections.
It also contains a few additional utility types that I found useful, such as
array validators (to check for bounds conditions, empty arrays, and null
arrays) and collection validators.
In any event, a sample chunk
of validation code looks like this:
Public Sub
New(ByVal siteId As Integer, _
ByVal startDate As Date, _
ByVal endDate As Date, _
ByVal offenderId As Integer, _
ByVal logonName As String, _
ByVal connection As SqlConnection)
Validate.That(siteId, "siteId").IsPositive()
Validate.That(startDate, "startDate").IsGreaterThanOrEqualTo(Constants.DefaultDate)
Validate.That(endDate, "endDate").IsGreaterThanOrEqualTo(startDate)
Validate.That(offenderId, "offenderId").IsGreaterThan(-1)
Validate.That(offenderId, "offenderid").NotEqualTo(0)
Validate.That(logonName,_
"logonName").IsNotNull()
Validate.That(connection).IsValid()
...
End Sub
Which, in my opinion, is far
more readable than the following:
If siteId <= 1 Then
Throw New ArgumentOutOfRangeException("siteId")
End If
If startDate < Constants.DefaultDate Then
Throw New ArgumentOutOfRangeException("startDate")
End If
If endDate < startDate
Then
Throw New ArgumentOutOfRangeException("enddate")
End If
If offenderId < -1
Then
Throw New ArgumentOutOfRangeException("offenderId")
End If
If offenderId = 0 Then
Throw New ArgumentException("offenderId may not be zero.")
End If
If logonName Is Nothing Then
Throw New ArgumentNullException("logonName")
End If
If connection Is Nothing Then
Throw New ArgumentNullException("connection")
End If
If (connection.State And ConnectionState.Open) = 0 Then
Throw New ArgumentException("Operation requires an open connection.")
End If
Now, there are several
issues which I have yet to resolve (but am actively working on):
1. If you want to perform several tests
on the same object, you have to create several different parameter validator
objects. The current design is wasteful this way. I’d like to find a way around
that.
2. It’s not very extensible. The core
type support is great; but what about new types? Having to manually add a new
method for every new type I want to support is problematic, and completely contrary to object oriented
principles. I need to do something interface based, and I haven’t quite wrapped
my brain around it yet. (I do know that I want to avoid the penalties inherent
in reflection at all costs. This thing is used way too much to incur those kinds of costs.)
3. It’s not localized. But we’ve already
mentioned that.
So there you have it. Tell
me what you think.