During my worked at project X, I've looked for several validation patterns.
The use case is: you have a view with several fields, you need to validate them all, collect all the error messages and focus on the 1st erroneous file.
Since the View represents of a view (projection) over the Domain (Model) the right thing to do is to put the validation in the Model.
The problem arises from the fact the in an ActiveController-PassiveView (aka Model-View-Presenter)
neither the Model nor the View have access to each other:
PassiveView <-- Presenter --> Model.
The latest frameworks use annotations/validators & reflection to map validations ViewField <->
ModelField. (it gets hairier if you need to validate accross several fields, or several objects).
Project X was a .Net project, and .Net 2.0 has a nice feature: implicit type inference for delegates.
Let me explain: if you define an delegate
public delegate R Func<R, A1>(A1 arg1);
and a method
void DoSomething<R,A1>(Func<R,A1> converter) {
... }
then you can pass *any* method which matches the signature of the delegate to the
DoSomething(...) method.
public R MyConverter<R,A1>(A1 arg);
...
DoSomething(MyConverter) // this is valid
Basically, starting with .Net 2.0, closures/functions are 1st class citizens in C#, like any other 'normal' objects. (closures are objects, afterall), with some remarks:
- .Net/C# properties are not methods and cannot be used as such
- in java you have to write a *lot* of anonymous classes, making more difficult to see any benefit. (that's also why an Enumerating component is not appealing in java, in C# the compiler writes the anon. classes for you)
Having said that, we can do a 'manual' binding in Presenter:
ValidationService.bind(PassiveView.GetField(), Model.ValidateField, PassiveView.SetErrorOnField)
the function signature of bind would be
bind<T>(T candidate, Func<bool, T> validate, Action signalError) {
if (! validate(field)) signalError();
}
We can do some more variations on the thema, where the ValidateField will also give us a message saying what was wrong with the candidate.
Variation within variation: message in several languages, should the model be responsible for that, maybe should provide the id to an message which will be translate...
One smell is that, after we have validated the field, by the Model, we pass again the value back to
the model in order to update/create some objects/fields.
Another discussion we had is that a constructor/factory using exceptions for validating arguments is expensive and it fails too fast, since we want to collect all error messages.
So we could replace that with a 2-step process:
maybeValidArgs = factory.Validate(args);
factory.create(maybeValidArgs);
but this is too 'manual'.
Some static typing functional languages have a construct to explicitely define a nullable value. In a c#/java would be:
interface Option<T> {
boolean HasValue();
T GetValue();
}
class Some<T> {
boolean HasValue() -> always true;
T GetValue() -> always the value;
}
class None<T> {
boolean HasValue() -> always false;
T GetValue() -> always throws an exception;
}
Note: In java we have type erasure, so for None<?> we can use a Singleton object.
Now for validation use case we might:
interface Option<T> {
boolean HasValue();
T GetValue();
IEnumerable<String> Errors();// Danger in using Strings !!! String is a 'primitive/sealed' type, which you cannot extend.
}
class Some<T> {
boolean HasValue() -> always true;
T GetValue() -> always the value;
IEnumerable<String> Errors() -> always Empty;
}
class None<T> {
boolean HasValue() -> always false;
T GetValue() -> always throw;
IEnumerable<String> Errors() -> always the errors;
}
Basically HasValue() has the same semantic with IsEmpty(Errors()).
We could change the pull - with a push:
instead of IEnumerable<String> Errors() (pull errors)
we push the errors in a handler
DoWithErrors(Action<IEnumerable<String>> errorHandler) -> Some will call the the action, None will do not
so we could use it like this:
Option<T> maybe = factory.TryCreate(args);
if (maybe.HasValue()) { ... do something with it }
else {
maybe.doWithErrors(View.DisplayErrors);
}
It is not difficult to associate an extra validation action for each arg/candidate field.
We just add optional args to TryCreate(args, params IAction[] actions) which we'll define as
mapping arg1 -> action1, arg2 -> action2. (if we have more args then actions, we don't call any action for them)
eg.
Option<Address> maybe = addressFactory.TryCreate(street_candidate, number_candidate, city_name_candidate, view.SignalErrorStreetName, view.SignalErrorValidateNumberCandidate, view.SignalValidateCityName)
...probably this will evolve to a 'framework', but at least a safe-type one, not string/reflection-based... And probably the annotation/reflection based are more AOP/crosscutting then this will be.
Monday, March 31, 2008
Subscribe to:
Post Comments (Atom)
No comments:
Post a Comment