ストーリー
先日、会社の新しいプロジェクトで FluentValidation が使われているのを見ました。ご存知の通り、FluentValidation は厳密に型指定された検証ルールを構築するための非常に強力な .NET フレームワークで、プログラマーから面倒な検証問題を解決してくれ、非常に便利です。しかし、それでも私はとても不愉快なことに遭遇しました。以下のコードをご覧ください:
public class UserInformationValidator : AbstractValidator<UserInformation>
{
public UserInformationValidator()
{
RuleFor(o => o.UserName).Length(2, 20).WithMessage("名前の長さが間違っています");
RuleFor(o => o.Sex).Must(o=>o=="男"||o=="女").WithMessage("性別が間違っています");
RuleFor(o => o.Age).ExclusiveBetween(0, 200).WithMessage("年齢が間違っています");
RuleFor(o => o.Email).EmailAddress().WithMessage("メールアドレスが間違っています");
}
}
static void Main(string[] args)
{
UserInformation userInformation = new UserInformation();
userInformation.UserName = "";
userInformation.Sex = "不男不女";
userInformation.Age = 2200;
userInformation.Email = "xxxxx";
UserInformationValidator validationRules = new UserInformationValidator();
var result= validationRules.Validate(userInformation);
if (!result.IsValid)
{
Console.WriteLine( string.Join(Environment.NewLine, result.Errors.Select(x => x.ErrorMessage).ToArray()));
}
}
オブジェクトを検証するたびに、新しいバリデーターの型を作成しなければなりません。上記の UserInformationValidator のように、論理的には問題ありませんが、私は潔癖症なので、次にこれをカプセル化してみましょう。えへへ、より少ないコードでより多くのことを行います。
インストール
バリデーターを作成する前に、プロジェクトに FluentValidation.dll への参照を追加する必要があります。最も簡単な方法は、NuGet パッケージマネージャーまたは dotnet CLI を使用することです。
テンプレート化されたコードのカプセル化の探求
テンプレート化されたコードを親クラスに抽出する
上記のコードをよく見ると、新しいバリデーターを作成するたびに、AbstractValidator<T> を継承したクラスを作成しなければなりません。ここで T は検証したいクラスの型です。バリデーターの親クラスをカプセル化します。
public class CommonVaildator<T> : AbstractValidator<T>
{
}
検証ルールの追加
実際のビジネスロジックは UserInformationValidator バリデーター内に記述されており、このコードブロックで必要なのは RuleFor だけです。それ以外はすべて親クラスに統一してカプセル化できるはずです。この考え方に従って、長さバリデータールールをカプセル化してみましょう。
まず、RuleFor のプロトタイプを見てみましょう。
public IRuleBuilderInitial<T, TProperty> RuleFor<TProperty>(Expression<Func<T, TProperty>> expression)
そのパラメータは Func デリゲートです。では、Expression とは何でしょうか?
Expression は式木です!
式木は、ラムダ式を実行可能なロジックではなく、ツリー状のデータ構造として表現できるようにするコードです。
C# では Expression で定義され、構文木、つまりデータ構造の一種です。主に計算や演算が必要な構造を格納するために使用され、格納機能のみを提供し、演算は行いません。通常、Expression は Lambda と一緒に使用されます。ここでは詳しく説明しません!
これで、簡単に長さバリデータールールをカプセル化できます。
public void LengthVaildator(Expression<Func<T, string>> expression, int min, int max, string Message)
{
RuleFor(expression).Length(min, max).WithMessage(Message);
}
同様に、述語バリデータールールやメールバリデータールールなどもカプセル化できます。
public void MustVaildator(Expression<Func<T, string>> expression ,Func<T,string, bool> expression2, string Message)
{
RuleFor(expression).Must(expression2).WithMessage(Message);
}
public void EmailAddressVaildator(Expression<Func<T, string>> expression, string Message)
{
RuleFor(expression).EmailAddress().WithMessage(Message);
}
検証メソッドのカプセル化
上記でバリデーターをカプセル化しました。では、var result= validationRules.Validate(userInformation); のような検証メソッドをカプセル化するのは簡単です。コードは次のとおりです。
public static string ModelValidator<T>(T source, AbstractValidator<T> sourceValidator) where T : class
{
var results = sourceValidator.Validate(source);
if (!results.IsValid)
return string.Join(Environment.NewLine, results.Errors.Select(x => x.ErrorMessage).ToArray());
else
return "";
}
カプセル化後のコードのテスト
CommonVaildator<UserInformation> commonUserInformation = new CommonVaildator<UserInformation>();
commonUserInformation.LengthVaildator(o => o.UserName, 2, 30, "名前の長さが間違っています");
commonUserInformation.MustVaildator(o => o.Sex, (user, _) => user.Sex =="男"||user.Sex=="女" , "性別が間違っています");
commonUserInformation.ExclusiveBetweenVaildator(o=>o.Age,0, 200, "年齢が間違っています");
commonUserInformation.EmailAddressVaildator(o => o.Email, "メールアドレスが間違っています");
string msg= VaildatorHelper.ModelValidator(userInformation, commonUserInformation);
Console.WriteLine(msg);
このようにコードがずっと簡潔になりましたね。ここでは4種類の検証ルールのみをカプセル化しましたが、他のものはここではカプセル化しません。
まとめ
この記事は仕事の細かな部分から生まれました。これは私の即興のカプセル化です。より良いカプセル化コードをお持ちの方がいらっしゃいましたら、ぜひご意見をお聞かせください。独り善がりより皆で楽しむ方が良いですからね。今回はここまでです。お役に立てれば幸いです。