🐈

PowerRule で複雑なルールを書く

2023/08/11に公開

PowerRule で複雑なルールを書く

思い付きでバリデーションライブラリ PowerRule を作ってみました。

なんで作ったのかとか、使い方とかその辺を解説します。

メソッドチェーンのデメリット

メソッドチェーンをつかった Fluent ななんちゃら系は英文に近づけて書けるようになっており、可読性が高いと言われています。

例えば NUnit だとこんな感じで書けます。

Assert.That(actual, Has.Property("Name").EqualTo("山田太郎").And.Property("Age").EqualTo(20));

本当に読みやすいかなあ? と思うこともあります。条件が長くなるとわけがわからなくなります。

日本人だから英語として成立しててもあまり読みやすくないんですよね... それよりは PowerAssert のように、条件式として書けた方がわかりやすいです。

PAssert.IsTrue(() => actual.Name == "山田太郎" && actual.Age == 20);

条件式として書けることのメリットに、新しいことを覚えなくてもいいというのがあります。補完が効くのである程度は何とかなるのですが、NUnit では "山田" で始まる検証をする方法を調べなければなりません。(NUnit Constraints ってこんなにあるんですよ...) PowerAssert はよく知っているメソッドが使えます。

// NUnit
Assert.That(actual.Name, Does.StartWith("山田"));

// PowerAssert
PAssert.IsTrue(() => actual.Name.StartsWith("山田"));

NUnit 標準で用意されていない Constraints の場合は自力で書く必要があります。例えば素数かどうかを検証する場合、NUnit の Constraints の作成方法を調べなければなりません。PowerAssert では、素数かどうかを調べる関数を作るだけで済みます。

PAssert.IsTrue(() => IsPrimeNumber(actual));

メソッドチェーンは一見簡単そうに見えますが、要件が複雑化するとだいたいややこしくなります。これは、ライブラリに頻繁に API が追加されやすいことにつながります。ひどいケースでは仕様の安定性に欠けてしまい、ライブラリのバージョンアップでアプリケーションが壊れることもあります。

今ではアプリケーションの寿命よりライブラリやフレームワークの寿命の方が短いですよね。あんまりですよ。t-wada さんが「テストフレームワークはアプリケーションより寿命が長くないといけない!」と力説していましたが、本当にそうです。

PowerRule

そんな PowerAssert にあやかったのが PowerRule です。オブジェクトの値を検証する、いわゆるバリデーションライブラリです。

目標を次のように設定しました。

  • 一度使い方を覚えれば、ルールが複雑化しても追加の知識が不要なこと
  • 仕様が安定して、PowerRule のバージョンアップがあまり発生しないこと
  • PowerRule のバージョンアップでアプリが壊れないこと
  • メソッドチェーンではなく、なるべく記号 (式) を使うこと

使い方

プロジェクトへの追加はいつもの通りです。nuget.org からダウンロードします。

$ dotnet add package PowerRule

検証ルールは Rule<T> クラスを継承し、Verify() メソッドを実装します。

class SexRule : Rule<Person>
{
    private readonly Sex _sex;

    public SexRule(Sex sex)
    {
        _sex = sex;
    }

    public override VerifyResult Verify(Person obj)
    {
        if (_sex == obj.Sex) return Valid();
        return Invalid();
    }
}

使うときは Verify() メソッドを呼び出すだけなのですが、それだけだと PowerRule を使う理由はありません。

PowerRule はルールの合成 (AndOrNot) をメソッドチェーンではなく、演算子 (& / | / ~) で行います。

次のように単機能のルールクラスをたくさん作って、それを組み合わせます。

var marriageableRule =
    ~new MarriedRule() & (
        (new SexRule(Sex.Male) & AgeRule.GreaterOrEquals(18)) |
        (new SexRule(Sex.Female) & AgeRule.GreaterOrEquals(16) & DaysDivorcedRule.GreaterOrEquals(100))
    );

まあ、複雑になってくると本当に見やすいのか? って問題があるんですけど、一時変数を使えばもう少し見やすくなります。

var whenMaleRule = new SexRule(Sex.Male) & AgeRule.GreaterOrEquals(18);
var whenFemaleRule = new SexRule(Sex.Female) & AgeRule.GreaterOrEquals(16) & DaysDivorcedRule.GreaterOrEquals(100);
var marriageableRule = ~new MarriedRule() & (whenMaleRule | whenFemaleRule);

非同期の場合は AsyncRule<T> を継承し VerifyAsync() を実装します。AsyncRule 同士の合成も可能ですし、Rule も含めて合成できます。

最後に

PowerRule はメソッドチェーンを利用したバリデーションライブラリのように短く書くことはできません。その代わり、少ない知識で複雑なルールへも対処できます。

正直しょぼいです。これくらい俺でも書けるわ! って人もいっぱいいるでしょう。その代わり、仕様が安定しています。

頻繁にアップデートされないのもメリットなのですが、それが本当にメリットかどうかは今後に期待ですよね... 実際使ってみると不満が出てきそうですし。

  • ルールのテキストにする機能が欲しい。Sex == Sex.Male && Age >= 18 みたいに。
  • どの検証に失敗したのかの情報を取得できるようにしたい。

余談

昔は NUnit Constraints を使いこなしている俺かっけー! だったんですけどねぇ... いろんなフレームワークやライブラリを使ってるといちいち覚えるのが面倒なんですよ... 歳とったなあ...

Discussion