この投稿では、定数の構成を変更する際に列挙を使用する場合の問題、または定数を使用する場合にコードの重複が存在する場合の問題に対する解決策を提供します。 他のケースでは、以下に説明するアプローチの適用は通常非実用的です。Javaの列挙の説明
Javaでは、特にバージョン1.5以降、いわゆる列挙型が登場しました。 名前付き定数に対して列挙型を使用することには多くの利点があります。
- コンパイラは正しい型チェックを保証します
- 可能なすべての列挙値を反復処理する便利さ
- スイッチブロックで占めるスペースが少ない(クラス名は不要)
- など
ただし、たとえばC ++と比較すると、Javaの列挙型は完全なオブジェクトであるため、開発者はより柔軟に対応できます。
- まず、すべての列挙はjava.lang.Enumクラスから継承されます。java.lang.Enumクラスには、次のような便利なメソッドがいくつかあります。
-name()-文字列としての定数の名前
-順序()-定数の順序(定数が宣言される順序に対応)
-valueOf()-クラスと名前で列挙オブジェクトを取得できる静的メソッド
- さらに、すでに発表されているように、列挙クラスは、列挙クラスのjava.lang.Class.getEnumConstants()メソッドを呼び出すことにより、可能なすべての列挙値を取得する機会があります。
- 列挙クラスでは、コンストラクター(プライベートコンストラクターのみ)、フィールド、およびメソッドを指定できます。
- 列挙は任意のインターフェイスを実装できます
- さらに、列挙内のメソッドは抽象的である場合があり、定数の特定のインスタンスがそのようなメソッドを決定できます(ところで、すでに定義済みのメソッドを再定義します)
上記のすべてを考えると、Javaでの転送は単なる定数(つまりデータ)ではなく、本格的なオブジェクトであり、その使用範囲について特定の結論を引き出すことができると言うことができます。
シンプルさが力です!
以下に提案するアプローチを使用する場合、かなり複雑であることを考慮する必要があります。
次の投稿に従って、この設計は単純な場合には使用しないでください。
その目的は、次のような困難な拡張指向の状況での生活を簡素化することです。
- 同じ列挙クラスを使用するいくつかの疎結合モジュールの存在
- 列挙定数の構成を変更するための先例の使用
- この列挙を使用したスイッチブロックでのコードの重複の存在
これらの症状がない場合、以下のアプローチの適用は疑わしく、さらには危険です。 コードの複雑さが増すと、エラーが発生した場合のより徹底した分析が必要になります。
変化する環境での列挙の使用
さて、実際に、私が検討したかった問題の本質に移りましょう。
プラスでスイッチが使用された場合、列挙を使用する必要があります。 列挙には継承されたメソッドを含めることができ、インターフェイスを実装できるため、スイッチブランチハンドラーのすべてのロジックを列挙クラスのメソッドに入れることができます。 これを事実として覚えて、次の問題を考慮してください。
列挙の見かけの単純さ、およびC ++などの言語の経験からデータとして扱うために使用されているという事実により、列挙はシステムのほとんどの異なる部分でシステムの動作を原則として制御します。 さらに、すべての条件付きロジックを列挙のメソッドに(分岐を取り除くために)転送すると、必然的に列挙クラスに含まれるロジックの飽和とオーバーフローが発生します。 これにより、モジュールの接続性が向上します。
この場合、次のことができます。
- 列挙のクラスをハンドラーに分割します。各ハンドラーは、システムのモジュールの1つに対応します。 このようにして、列挙クラス自体のインターフェースの輻輳の問題を解決します。
- 接続性の問題を解決するために残っています。 これを行うには、各ハンドラーにインターフェイスを割り当て、ファクトリーを介してインスタンスを受け取ります。 ファクトリー自体は、宣言的アプローチを使用して作成できます。 インターフェイスは、構成レベルで実装に接続されます(たとえば、xmlを介して)。
したがって、次のプラスとマイナスによって特徴付けられるアプローチが得られます。
- +コードの重複が最小限に抑えられます
- +コードの読みやすさを改善
- +ハンドラーのロジックは任意の方法で共有できます。 ハンドラーの継承階層は任意です
- +新しいハンドラー(モジュール)または列挙型要素を追加するとき、何も忘れられない
- + T.K. ハンドラーの階層に制限はありません;常にデフォルトのハンドラーを提供できます
- -コーディングコストの増加
- -コード構造の複雑さ
- -モジュール間の依存関係を明示的に削除する必要がある場合(モジュールの物理的な分離など)、工場出荷時の構成を最新に保つ必要があります。
これらの欠点はすべて、使用するIDEのプラグインの形でこの機能の適切なサポートを記述することで解決できます。 もちろん、これにはいくらかのコストが必要ですが、一元的に一度解決すれば、いつでもどこでも使用できます。
使用例
たとえば、次の一連のクラスが(異なるモジュールに)あります。
public enum DocumentStatus {
新規(0)、
ドラフト(1)、
発行済み(2)、
アーカイブ済み(3);
private DocumentStatus(int statusCode){
this.statusCode = statusCode;
}
public int getStatusCode(){
return statusCode;
}
private int statusCode;
}
// Web
パブリッククラスDocumentWorklfowProcessor {
...
public List <Operation> getAvailableOperations(DocumentStatus status){
<Operation>操作をリストします。
スイッチ(ステータス){
ケースNEW:
操作= ...;
休憩;
ケースドラフト:
操作= ...;
休憩;
ケース公開:
操作= ...;
休憩;
アーカイブされたケース:
操作= ...;
休憩;
}
戻り操作。
}
public void doOperation(DocumentStatus status、Operation op)はAPIExceptionをスローします{
スイッチ(ステータス){
ケースNEW:
// 1セットの操作パラメーター
休憩;
ケースドラフト:
//操作パラメータの別のセット
休憩;
ケース公開:
//操作パラメーターの3番目のセット
休憩;
アーカイブされたケース:
//など
休憩;
}
}
}
//スケジュールされたタスク
パブリッククラスReportGenerationProcessor {
...
public void generateReport(レポートレポート){
DocumentStatus status = report.getDocument()。GetStatus();
ReportParamsパラメーター。
スイッチ(ステータス){
ケースNEW:
ケースドラフト:
// params =レポートに表示される要素の選択基準
休憩;
ケース公開:
// params =レポートに表示される要素の他の選択基準
休憩;
アーカイブされたケース:
//など
休憩;
}
//レポート生成
}
}
モジュールはかなり弱く相互接続されていることがわかります。
顧客の要求に応じて、確認のために送信された新しいドキュメントステータス(VERIFY)があるとします。
新しいステータスを追加する場合、提案されたすべての場所に新しいケースを追加する必要があります。 どこかに追加するのを忘れるのはとても簡単です。 もちろん、例外をスローするデフォルトブロックを提供することもできますが、大規模なシステムでは、すべての場所が認識されることを保証しません。
このコードを次の形式に変換することが提案されています。
パブリックインターフェイスIReportGeneratorProcessor {
public ReportParams getReportParams();
}
パブリックインターフェイスIDocumentWorklfowProcessor {
public List <Operation> getAvailableOperations();
public void doOperation(Operation op)がAPIExceptionをスローします。
}
public enum DocumentStatus {
//ここでは、newの代わりに、getメソッドでファクトリーまたは遅延初期化を使用できます
NEW(0、新しいNewDocReportGeneratorProcessor()、新しいNewDocWorklfowProcessor())、
DRAFT(1、新しいDraftDocReportGeneratorProcessor()、新しいDraftDocWorklfowProcessor())、
公開済み(2、...)、
アーカイブ済み(3、...);
private DocumentStatus(int statusCode、IReportGeneratorProcessor reportProc、
IDocumentWorklfowProcessor docProc){
this.statusCode = statusCode;
this.reportProc = reportProc;
this.docProc = docProc;
}
public int getStatusCode(){
return statusCode;
}
public IReportGeneratorProcessor getReportGeneratorProcessor(){
return reportProc;
}
public IDocumentWorklfowProcessor getDocumentWorklfowProcessor(){
return docProc;
}
private int statusCode;
private IReportGeneratorProcessor reportProc;
private IDocumentWorklfowProcessor docProc;
}
// Web
パブリッククラスDocumentWorklfowProcessor {
...
public List <Operation> getAvailableOperations(DocumentStatus status){
return status.getDocumentWorklfowProcessor()。getAvailableOperations();
}
public void doOperation(DocumentStatus status、Operation op)はAPIExceptionをスローします{
status.getDocumentWorklfowProcessor()。doOperation(op);
}
}
//スケジュールされたタスク
パブリッククラスReportGenerationProcessor {
...
public void generateReport(レポートレポート){
DocumentStatus status = report.getDocument()。GetStatus();
ReportParams params = status.getReportGeneratorProcessor()。GetReportParams();
//レポート生成
}
}
クライアントコードがはるかに短くなり、理解しやすくなっていることがわかります。
もちろん、ハンドラー自体を記述する必要がありますが、新しい列挙要素を追加するときには、ハンドラーを記述する必要があります。 彼らがそれについて忘れる危険はもはやありません(意図的な妨害行為や「後の」放棄は考慮されません)、少なくとも彼らは考えます。 そして、すでに述べたように、いつでもデフォルトのハンドラーを取得できます。
public IDocumentWorklfowProcessor getDocumentWorklfowProcessor(){
return(docProc!= null)? docProc:DEFAULT_WORKFLOW_PROCESSOR;
}
PSこのアプローチの妥当性に関するコメントをhabrasocietyから待っています。 十分な票を獲得したら、このプラグインをIntelliJ IDEA、Eclipse、NetBeansに実装する準備ができました。
UPD。 「シンプルさは力です!」というセクションが、このアプローチの枠組み内での位置を示すために、対応する投稿に応じて追加されました。
UPD 2.一般的な需要により、彼は例を追加しました。 また、投稿の冒頭に、このソリューションの適用可能性に関する簡単な説明を追加しました。