抽象構文ツリーや中間モデルなど、複雑なモデルを変換するクラスがあります。モデルは有効、無効、または部分的に無効のいずれかです。つまり、モデルにはエラーが含まれていますが、モデルの一部は有効であり、さらに処理することができます。
最も簡単なエラー報告は、例外を使用することです。
public class Transformer {
public TargetModel transform(SourceModel model)
throws InvalidModelException {
// ...
}
}
明らかに、これは複数のエラーを報告することを許可せず(少なくとも、例外にさらに情報を添付しない場合)、例外は例外的な状況である必要があります。
Martin Fowlerは彼の記事でこの問題に対処しました検証でスロー例外を通知で置き換える。彼の方法を問題に適用すると、次のようになります。
public abstract class Problem {
// ...
}
public final class ProblemA {
// ...
}
public final class ProblemB {
// ...
}
public class Transformer {
public static final class TransformationResult {
private Collection<Problem> problems;
private Optional<TargetModel> targetModel;
// ...
}
public TransformationResult transform(SourceModel model) {
// ...
}
}
次に、ビジターパターンまたはinstanceof
チェックを使用してエラーを区別します。これは、タイプセーフと冗長性の間のトレードオフです。
別の可能な解決策は、オブザーバーパターンを使用することです。
public class Transformer {
public interface ProblemListener {
public void onProblemA(...);
public void onProblemB(...);
// ...
}
public void addProblemListener(ProblemListener listener) {
// ...
}
public void removeProblemListener(ProblemListener listener) {
// ...
}
public TargetModel transform(SourceModel model) {
// ...
}
}
オブザーバーパターンを使用すると、メモリにエラーを蓄積する必要がなくなり、過度のinstanceof
チェックや訪問者が不要になるという利点があります。その一方で、ビジターパターンよりも制御フローを覆い隠します。
私はすべての解決策が可読コードにつながるわけではないと思います。関数型言語で十分なメモリがある場合、2番目のソリューションを使用しますが、Java構造パターンマッチングでは、どちらも冗長なinstanceof
チェックまたはビジターパターンの使用が必要になります。私が見落としていること、または私が考慮しなかった含意?ソリューションのいずれかでの経験は何ですか?
入力としてデータを受け取り、タプルを返す関数として検証を説明できます
inputdata -> (outpudata, Errors)
または多分inputdata -> (outpudata, errors, warnings)
inputdata
がデータであることは明らかですが、入力したいデータです。
ユースケースに応じて
outputdata
はinputdata
と同じです
outputdata
はある種のValidationResult
であり、data
はinputdata
であり、多分validatedData
は有効なサブセットを表します(一部の結果を受け入れる場合)。
Fowlersの記事の要点は、例外をスローする素朴なアプローチにはいくつかの欠点があります。例外は、ある種の早期終了です。ユースケースによっては、すべてが明らかになるわけではなく、最初のエラーのみが発生します。複雑なフォームアプリを作成している場合、ユーザーにallアクションが必要なことを通知してもらいたいと考えています。第2に、無効なデータは実際にはexceptionally
ではなく、データが正しくない場合があります。このシナリオでは、例外は少しやりすぎです。 3番目:例外は、ある種のgoto
のようなプログラミング構造につながる可能性があり、プログラミングフローの理解を複雑にする可能性があります。これには例外を使用しないでください。
Javaでは、errors
とwarnings
のメンバーをValidationResult
のメンバーにして、hasErrors
とhasWarnings
とgetErrors
とgetWarnings
のそれぞれを記述します。addErrors
とaddWarnungs
.
あなたの問題は次のようになります:
一般的な検証関数:inputdata -> (outpudata, errors, warnings)
一連の検証関数を呼び出します
単一のバリエーター:data -> (errors, warnings)
または単一の検証の戻り値の型としてValidationResult
を使用する
単一のバリエーター:inputdata -> (outpudata, errors, warnings)
各呼び出しの後、すべてのerrors
とwarnings
をValidationResult
の対応するコレクションに追加するか、ValidationResult
に単純なmerge()
-Methodを実装します。
最後に最後のValidationResult
を返します
できました。
最も単純なエラー報告は、例外を使用することです[...]これは、複数のエラーを報告することを許可しません(少なくとも、例外にさらに情報を添付しない場合)
それなら...なぜそうしないのですか?
public class InvalidModelException extends Exception {
private Collection<Problem> problems;
}
InvalidModelException
は抽象的で、特定の詳細を保持する特殊な実装によってサブクラス化されている必要があります。各問題は個別のブロックで処理できます。
TargetModel target;
try {
target = transform(source);
} catch (ProblemA ex) {
ex.getDetailsA();
} catch (ProblemB ex) {
ex.getDetailsB();
} catch (InvalidModelException ex) {
ex.getGenericDetails();
}
または、複合オブジェクトを返します。
public Result<TargetModel, Diagnostic> transform(SourceModel model);
if (result.isInvalid()) {
if (result.getDiagnostic() instanceof ...)
...
// or
switch (result.getProblemType()) {
case Problem.A:
break;
case Problem.B:
break;
default:
break;
}
throw new Exception();
}
target = result.getTarget();