java-recipes

ホーム 入力値バリデーション › V-02

V-02: ビジネスルール検証

金額・納期・在庫数といった業務固有のルールを検証する方法を解説します。 V-01(数値・文字列・日付の形式チェック)との違いや、複数のルールを一括で評価してエラーを集約するパターンを学びましょう。

ビジネスルール検証とは

V-01 で学んだ「形式チェック」は、入力値が正しい形式かどうか(数値か・日付形式か・メールアドレス形式か)を検証します。 一方、ビジネスルール検証は「業務上の意味として正しいかどうか」を検証します。

形式チェックとビジネスルール検証の違い

種類チェック内容の例対応するページ
形式チェック「この文字列は整数か」「日付形式か」V-01
ビジネスルール検証「金額は0より大きいか」「納期は未来か」「在庫は足りるか」V-02(このページ)
  • 注文処理で金額・納期・在庫数をまとめて検証するとき
  • 複数の条件を同時にチェックして、エラーをまとめてユーザーに返したいとき
  • 業務ルールの追加・変更が頻繁に起きるシステムで、ルールを整理して管理したいとき

サンプルコード

BusinessRuleValidationSample.java
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

public class BusinessRuleValidationSample {

    // バリデーション結果を表す
    static class ValidationResult {
        private final List<String> errors = new ArrayList<>();

        public void addError(String message) {
            errors.add(message);
        }

        public boolean isValid() {
            return errors.isEmpty();
        }

        public List<String> getErrors() {
            return errors;
        }
    }

    // 注文バリデーター
    static class OrderValidator {
        // 請求金額 > 0
        public static void validateAmount(BigDecimal amount, ValidationResult result) {
            if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
                result.addError("請求金額は0より大きい値を指定してください");
            }
        }

        // 納期 > 本日
        public static void validateDeliveryDate(LocalDate deliveryDate, ValidationResult result) {
            if (deliveryDate == null || !deliveryDate.isAfter(LocalDate.now())) {
                result.addError("納期は本日より後の日付を指定してください");
            }
        }

        // 在庫数 >= 注文数
        public static void validateStock(int stockCount, int orderCount, ValidationResult result) {
            if (orderCount <= 0) {
                result.addError("注文数は1以上を指定してください");
            } else if (stockCount < orderCount) {
                result.addError("在庫数(" + stockCount + ")が注文数(" + orderCount + ")を下回っています");
            }
        }

        // 複合バリデーション(全フィールドまとめて)
        public static ValidationResult validateOrder(
                BigDecimal amount, LocalDate deliveryDate, int stockCount, int orderCount) {
            ValidationResult result = new ValidationResult();
            validateAmount(amount, result);
            validateDeliveryDate(deliveryDate, result);
            validateStock(stockCount, orderCount, result);
            return result;
        }
    }

    public static void main(String[] args) {
        System.out.println("=== 正常ケース ===");
        ValidationResult result1 = OrderValidator.validateOrder(
            new BigDecimal("10000"),
            LocalDate.now().plusDays(7),
            100,
            5
        );
        System.out.println("Valid: " + result1.isValid());

        System.out.println("\n=== 複数エラーケース ===");
        ValidationResult result2 = OrderValidator.validateOrder(
            new BigDecimal("-100"),
            LocalDate.now().minusDays(1),
            3,
            10
        );
        System.out.println("Valid: " + result2.isValid());
        for (String error : result2.getErrors()) {
            System.out.println(" - " + error);
        }
    }
}

Java 8 では ValidationResult をクラスで表現し、addError() でエラーを蓄積します。エラーが1件見つかっても処理を止めず、全フィールドをチェックしてから返すのがポイントです。

よくあるミス・注意点

バリデーションを呼び出し元に直接書きがち

注文処理の呼び出し元に if 文を直接並べて書くと、同じルールが複数の場所に散らばりメンテナンスが困難になります。 バリデーションロジックは専用のクラスやメソッドにまとめて、1か所だけ修正すれば全体に反映される設計にしましょう。

エラーメッセージが英語だけになりがち

エラーメッセージを「Invalid amount」のように英語だけで返すと、ユーザーや運用担当者が内容を理解しにくくなります。 「請求金額は0より大きい値を指定してください」のように、誰でも読める日本語メッセージを心がけましょう。 ただし、ログに残すシステムエラーは英語でも問題ありません。

最初のエラーが見つかったらすぐ return してしまいがち

1件目のエラーが見つかった時点で処理を中断すると、ユーザーは修正のたびに次のエラーを発見することになります。 全フィールドのチェックを最後まで実行してからまとめてエラーを返すことで、ユーザーが一度に全ての問題を把握できます。

テストする観点

  • 金額の境界値:金額 = 0 のときエラーになること、金額 = 1 のときエラーにならないこと
  • 納期の境界値:本日と同じ日付はエラー、明日以降はエラーにならないこと
  • 在庫ぴったり:在庫数 = 注文数のときエラーにならないこと(在庫数 = 注文数 - 1 はエラー)
  • 注文数 = 0 や負の数はエラーになること
  • 全フィールドが不正な場合に複数エラーがまとめて返ること
  • 全フィールドが正常な場合に isValid() が true を返すこと
  • amount = null、deliveryDate = null でも NullPointerException が発生しないこと

GitHub でソースコードを見る →