例外や冗長性なしに入力検証を実行する方法


11

特定のプログラム用のインターフェイスを作成しようとすると、通常、検証されていない入力に依存する例外をスローしないようにします。

よくあることは、次のようなコードを考えたことです(これは単なる例であり、実行する機能は気にしないでください(Javaの例))。

public static String padToEvenOriginal(int evenSize, String string) {
    if (evenSize % 2 == 1) {
        throw new IllegalArgumentException("evenSize argument is not even");
    }

    if (string.length() >= evenSize) {
        return string;
    }

    StringBuilder sb = new StringBuilder(evenSize);
    sb.append(string);
    for (int i = string.length(); i < evenSize; i++) {
        sb.append(' ');
    }
    return sb.toString();
}

OK、だからそれevenSizeは実際にユーザー入力から派生したと言う。それで、私はそれが均一であるかどうかわかりません。しかし、例外がスローされる可能性があるこのメソッドを呼び出したくありません。そこで、次の関数を作成します。

public static boolean isEven(int evenSize) {
    return evenSize % 2 == 0;
}

しかし、今では同じ入力検証を実行する2つのチェックがあります。ifステートメント内の式との明示的なチェックですisEven。コードが重複しているため、見栄えがよくないため、リファクタリングしましょう。

public static String padToEvenWithIsEven(int evenSize, String string) {
    if (!isEven(evenSize)) { // to avoid duplicate code
        throw new IllegalArgumentException("evenSize argument is not even");
    }

    if (string.length() >= evenSize) {
        return string;
    }

    StringBuilder sb = new StringBuilder(evenSize);
    sb.append(string);
    for (int i = string.length(); i < evenSize; i++) {
        sb.append(' ');
    }
    return sb.toString();
}

OK、それで解決しましたが、今は次のような状況になります。

String test = "123";
int size;
do {
    size = getSizeFromInput();
} while (!isEven(size)); // checks if it is even
String evenTest = padToEvenWithIsEven(size, test);
System.out.println(evenTest); // checks if it is even (redundant)

冗長なチェックが行われました。値が偶数であることは既にわかっていますが、padToEvenWithIsEvenこの関数を呼び出したときに常にtrueを返すパラメーターチェックを実行します。

今のisEvenコースの問題を提起しませんが、パラメータのチェックが面倒であれば、これはあまりにも多くの費用が発生する場合があります。それに加えて、冗長な呼び出しを実行することは、単に正しくないと感じます。

「検証済みの型」を導入するか、この問題が発生しない関数を作成することにより、この問題を回避できる場合があります。

public static String padToEvenSmarter(int numberOfBigrams, String string) {
    int size = numberOfBigrams * 2;
    if (string.length() >= size) {
        return string;
    }

    StringBuilder sb = new StringBuilder(size);
    sb.append(string);
    for (int i = string.length(); i < size; i++) {
        sb.append('x');
    }
    return sb.toString();
}

しかし、これにはスマートな思考とかなり大きなリファクタリングが必要です。

isEven二重のパラメータチェックを繰り返し実行することを回避できる(より)一般的な方法はありますか?padToEven無効なパラメーターを実際に呼び出して例外をトリガーしないようにしたいのですが。


例外なく、例外のないプログラミングを意味するわけではありません。ユーザー入力は設計によって例外をトリガーしませんが、ジェネリック関数自体にはまだパラメーターチェックが含まれています(プログラミングエラーから保護する場合のみ)。


例外を削除しようとしていますか?実際のサイズがどうあるべきかについての仮定をすることによってそれをすることができます。たとえば、13を渡すと、12または14のいずれかにパッドし、チェックを完全に回避します。これらの仮定のいずれかを行うことができない場合、パラメーターが使用できず、関数を続行できないため、例外が発生します。
ロバートハーヴェイ

@RobertHarveyそれは-私の意見では-最小限のサプライズの原則と、フェイルファーストの原則に対してまっすぐに行きます。入力パラメーターがnullの場合は、nullを返すのとよく似ています(そして、当然ながら、説明されていないNPEの結果を正しく処理するのを忘れています)。
マールテンボデウェス

エルゴ、例外。正しい?
ロバートハーヴェイ

2
待って、何?アドバイスを求めてここに来たのですか?私が言っているのは(明らかに微妙ではないように)、それらの例外は設計によるものであるため、それらを排除したい場合、おそらく正当な理由があるはずです。ところで、私はamonに同意します。おそらく、パラメーターに奇数を受け入れられない関数はないはずです。
ロバートハーヴェイ

1
@MaartenBodewes:ユーザー入力の検証を実行padToEvenWithIsEven しないことを覚えておく必要があります。入力の有効性チェックを実行して、呼び出しコードのプログラミングエラーから自身を保護します。この検証がどの程度必要かは、呼び出しコードを書いている人が間違ったパラメータを渡すリスクに対してチェックのコストをかけるコスト/リスク分析に依存します。
バートヴァンインゲンシェナウ

回答:


7

あなたの例の場合、最良の解決策はより一般的なパディング関数を使用することです。呼び出し元が偶数サイズに埋め込みたい場合は、自分で確認できます。

public static String padString(int size, String string) {
    if (string.length() >= size) {
        return string;
    }

    StringBuilder sb = new StringBuilder(size);
    sb.append(string);
    for (int i = string.length(); i < size; i++) {
        sb.append(' ');
    }
    return sb.toString();
}

値に対して同じ検証を繰り返し実行する場合、または型の値のサブセットのみを許可する場合は、microtypes / tiny型が役立ちます。パディングなどの汎用ユーティリティの場合、これは良い考えではありませんが、ドメインモデルで値が特定の役割を果たしている場合、プリミティブ値の代わりに専用タイプを使用することは大きな前進になります。ここで、以下を定義できます。

final class EvenInteger {
  public final int value;

  public EvenInteger(int value) {
    if (!(value % 2 == 0))
      throw new IllegalArgumentException("EvenInteger(" + value + ") is not even");
    this.value = value;
  }
}

今、あなたは宣言することができます

public static String padStringToEven(EvenInteger evenSize, String string)
    ...

内部検証を行う必要はありません。このような単純なテストでは、オブジェクト内にintをラップすると実行時のパフォーマンスの点でよりコストが高くなりますが、型システムを有利に使用すると、バグを減らし、設計を明確にすることができます。

このような小さな型を使用すると、検証を実行しない場合でも、たとえばa FirstNameからaを表す文字列を明確にするために有用ですLastName。このパターンは、静的に型付けされた言語で頻繁に使用します。


場合によっては、2番目の関数はまだ例外をスローしませんか?
ロバートハーベイ

1
@RobertHarveyはい、たとえばNULLポインターの場合。しかし今では、メインチェック(数値が偶数である)が関数から呼び出し側の責任に追い込まれています。その後、適切と思われる方法で例外に対処できます。答えは、検証コード内のコードの重複を取り除くことよりも、すべての例外を取り除くことに重点を置いていると思います。
アモン

1
素晴らしい答え。もちろんJavaでは、データクラスを作成することに対するペナルティは非常に高くなります(多くの追加コード)。しかし、それはあなたが提示したアイデアよりも言語の問題です。私の問題が発生するすべてのユースケースにこのソリューションを使用するとは限りませんが、プリミティブの過剰使用やパラメーターチェックのコストが非常に難しいエッジケースには良いソリューションです。
マールテンボデウェス

1
@MaartenBodewes検証を行いたい場合、例外のようなものを取り除くことはできません。さて、代替手段は、検証済みオブジェクトまたは失敗時にnullを返す静的関数を使用することですが、基本的には利点はありません。ただし、検証を関数からコンストラクターに移動することにより、検証エラーの可能性なしに関数を呼び出すことができます。これにより、呼び出し元にすべての制御が与えられます。例:EvenInteger size; while (size == null) { try { size = new EvenInteger(getSizeFromInput()); } catch(...){}} String result = padStringToEven(size,...);
アモン

1
@MaartenBodewes重複検証は常に行われます。たとえば、ドアを閉めようとする前に、すでに閉じているかどうかを確認できますが、すでに閉じている場合は、ドア自体を再び閉じることもできません。呼び出し元が無効な操作を実行しないことを常に信頼する場合を除き、これから抜け出す方法はありません。あなたの場合unsafePadStringToEven、チェックを実行しない操作があるかもしれませんが、検証を避けるためだけに悪い考えのようです。
plalx

9

@amonの答えの拡張として、EvenInteger関数型プログラミングコミュニティが「スマートコンストラクター」と呼ぶものと組み合わせることができます。これは、ダムコンストラクターをラップし、オブジェクトが有効な状態であることを確認する関数です(ダムを作成します)コンストラクタクラスまたは非クラスベースの言語モジュール/パッケージプライベートで、スマートコンストラクタのみが使用されるようにします)。トリックはOptional、関数をより構成可能にするために(または同等の)を返すことです。

public final class EvenInteger {
    private final int value;

    private EvenInteger(value) {
        this.value = value;
    }

    public static Optional<EvenInteger> of(final int value) {
        if (value % 2 == 0) {
            return Optional.of(new EvenInteger(value));
        }
        return Optional.empty();
    }

    public int getValue() {
        return this.value;
    }
}

次に、標準Optionalメソッドを使用して入力ロジックを簡単に記述できます。

class GetEvenInput {
    public Optional<EvenInteger> askOnce() {
        int size = getSizeFromInput();
        return EvenInteger.of(size);
    }

    public EvenInteger keepAsking() {
        return askOnce().orElseGet(() -> keepAsking());
    }
}

keepAsking()do-whileループを使用して、より慣用的なJavaスタイルで記述することもできます。

Optional<EvenInteger> result;
do {
    result = askOnce();
} while (!result.isPresent());

return result.get();

それから、コードの残りの部分では、それがEvenInteger本当に均一であり、私たちの偶数チェックが一度だけ書かれたという確信をもって信頼することができますEvenInteger::of


一般にオプションは、潜在的に無効なデータを非常によく表します。これは、データと入力の有効性を示すフラグの両方を返す、多数のTryParseメソッドに類似しています。コンプリートタイムチェック付き。
バシレフ

1

検証の結果が同じであり、検証が同じクラスで実行されている場合、検証を2回実行することは問題です。それはあなたの例ではありません。リファクタリングされたコードでは、最初に行われるisEvenチェックは入力の検証であり、失敗すると新しい入力が要求されます。それはクラスの外部から呼び出すことができるパブリックメソッドpadToEvenWithEven上にあるように、第2のチェックは、最初の完全に独立している、異なる結果(例外)を有しています。

あなたの問題は、偶然同一のコードが非ドライと混同される問題に似ています。実装と設計を混同しています。それらは同じではなく、同じ行が1つまたは12個あるからといって、同じ目的に使用されていて、永久に互換性があると考えられるわけではありません。また、おそらくあなたのクラスにやりすぎさせるかもしれませんが、これはおそらく単なるおもちゃの例なので...

これがパフォーマンスの問題である場合、検証を行わないプライベートメソッドを作成することで問題を解決できます。このメソッドは検証を実行した後にパブリックpadToEvenWithEvenが呼び出し、他のメソッドが代わりに呼び出します。パフォーマンスの問題でない場合は、割り当てられたタスクを実行するために必要なチェックを別のメソッドに実行させます。


OPは、関数のスローを防ぐために入力検証が行われると述べました。したがって、チェックは完全に依存し、意図的に同じです。
D Drmmr

@DDrmmr:いいえ、依存していません。それはコントラクトの一部であるため、関数はスローします。私が言ったように、答えは、プライベートメソッドを作成して例外をスローする以外のすべてを実行し、パブリックメソッドがプライベートメソッドを呼び出すようにすることです。パブリックメソッドは、チェックを保持します。このメソッドは、検証されていない入力を処理するという異なる目的に使用されます。
-jmoreno
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.