Javaコードをより簡単で直感的にする方法

あなたは皆、見事に書いています

そして粗さを追加します。

x / fトランボ



Javaコードの記述は簡単ではありませんが、非常に簡単です。 困難は起動時に始まり、さらに悪い場合は変更が必要です。 2年前に彼のコードを開いた後、誰もが少なくとも一度は疑問に思いました。 Wrikeで製品を開発しており、10年以上にわたってこれを行ってきました。 同様の状況が繰り返し発生しました。 この間、私たちはコードを書く際に、より簡単で視覚的なものにするための多くの原則を開発しました。 私たちのアプローチには特別なものは何もありませんが、Javaでコードを記述するのが慣習的である方法とは非常に異なります。 しかし、誰かが私たちのアプローチや自分自身にとって有用な何かを見つけるかもしれません。









不変データモデルの使用方法



各メソッドのパラメータと結果の両方が不変である場合、コードははるかに単純になることを実践が示しています。



ImmutableList<Double> multiplyValuesByFactor( final ImmutableList<Double> valueList, final double factor) { … }
      
      





さらに、メソッドまたはクラスフィールドのすべてのローカル値に対して同じことを達成することはほとんど常に可能です。 ただし、クラスにはゲッターのみを含めることができます。 複雑なデータモデルを構築する必要がある場合の対処方法:セッターのみでビルダーを作成し、その中にbuild()メソッドを使用して不変オブジェクトを構築します。



このアプローチにより、コードはよりシンプルで読みやすくなりますが、確認する必要があります。 不変状態での作業をサポートするための例をいくつか示します。 デフォルトでは、 Rust言語はすべての「変数」を不変に定義します。



 let x = 5;
      
      





そして、変数を正確に取得するには、追加の努力をする必要があります。

 let mut y = 6; y=7;
      
      





Erlangには変数がまったくありません。絶対にすべての値は不変ですが、それでも複雑なアプリケーションを開発することは可能です。 ロバートC.マーティンは、このテーマについて、よく提示された理論とあまり良い例ではない興味深いプレゼンテーションをしていますが、理論はまだ一見の価値があります。



Tだ! 手動リソース管理の回避



ファイルを開いたら、閉じる必要があります。 データベースとの接続を受信したら、それを解放する必要があります。 一般的に、トリッキーなものはありません。 しかし、実際には、リソースを操作するときにJava開発者が間違いを犯す傾向があることを示しています。おそらく、メモリを手動で管理する喜びを奪われているためでしょう。 スキルは発達していません。 簡単な例:



 try(final ZipInputStream zipInputStream = new ZipInputStream( new FileInputStream(file), charset)) {...}
      
      





FileInputStreamが閉じられない可能性があるため、上記のコードには明らかなエラーがあります。



簡単で安価な解決策は、一般的にリソースを管理する必要性をプログラマから奪うことです。 たとえば、次のように:



 void processFile(final File file, FileProcessor fileProcessor) throws IOException { try (final FileInputStream fileInputStream = new FileInputStream(file)) { fileProcessor.processFile(fileInputstream); } }
      
      





この場合、ファイル処理ロジックのどこかでエラーが発生しても、少なくともすべてのリソースが正しく解放されて閉じられます。



2つの値を返す方法



これはインタビューで特に顕著です。初心者プログラマーがメソッドから2つの値、たとえば合計と平均を一度に返す必要がある場合にst迷に陥ります。 決定は、fromの程度が異なります



 return new Pair<>(sum, avg)
      
      





または



 return new AbstractMap.SimpleEntry<>(sum, avg)
      
      





前に



 return new Object[]{sum, avg}
      
      





同時に、きちんとした解決策は非常に簡単です:



 public class MathExt { public static class SumAndAverage { private final long sum; private final double average; public SumAndAverage(final long sum, final double average) {...} public long getSum() {...} public double getAverage() {...} } public static SumAndAverage computeSumAndAverage(final ImmutableList<Integer> valueList) { ... } }
      
      





それは完全に正常であり、さらに、補助データモデルを使用するメソッドの隣で補助データモデルを定義すると便利です。 これは、JavaクローンであるC#でさえ、絶対にすべてのプログラミング言語で行うのが通例です。 ただし、Javaプログラマーの間では、「各クラスは別のファイルで定義する必要があります。そうでないとコードが悪いので、それがポイントです」という奇妙な誤解に遭遇する場合があります。 Javaでは、1.1より前のバージョンで内部クラスを作成することは実際に不可能でしたが、1997年2月19日以降この問題は解決され、20年間利用可能なこの新しい言語機能の使用を妨げるものはありません。



実装を隠す方法



メソッドを実装する過程で、それをいくつかのより単純なメソッドに分解することが必要になることがあります。 この場合、必然的に問題が発生します。これらの補助メソッドをどこで決定するのでしょうか? もちろん、ロジックをプライベートメソッドに分割することもできますが、クラスが十分に大きい場合は不便になります。 プライベートメソッドは必然的にファイル全体に散らばってしまい、どのメソッドがどこで使用されるのかは、私たちが望むほど明白ではありません。 コードをきちんと整理し、読み取り中のファイルのスクロールを減らすのに役立つ簡単なソリューションは、メインのパブリックメソッド内に補助メソッドを定義することです。



たとえば、グラフの各ノードに対してコールバック関数が呼び出され、当然ノードごとに1回だけ、グラフを深く走査するコードを以下に示します。



 public static void traverseGraphInDepth( final Graph graph, final Consumer<Node> callback ) { new Runnable() { private final HashSet<Node> passedNodeSet = new HashSet<>(); public void run() { for(final Node startNode:graph.listStartNodes()){ stepToNode(startNode); } } void stepToNode(final Node node) { if (passedNodeSet.contains(node)) { return; } else { passedNodeSet.add(node); callback.accept(node); for(final Node nextNode:graph.listNextNodes(node)){ stepToNode(nextNode); } } } }.run(); }
      
      





いくつかのヘルパーメソッドを定義できます。



 private static void stepToNodeImpl( final Graph graph, final Node node, final Consumer<Node> callback, final HashSet<Node> passedNodeSetMutable) {...}
      
      





しかし、そのような解決策はかなり面倒であり、補助的な方法自体は孤独です。 たとえば、メソッドstepToNodeImplの名前だけを見ると、実際にグラフを詳細に走査するために使用されることは明らかではありません。 これを理解するには、まずその使用法をすべて見つける必要があります。



多くのロジックがメソッド内に隠されており、個別に正しくテストすることはできないという合理的な批判があるかもしれません。 問題は、プライベートメソッドを含むすべての実装の詳細が個別のテストでカバーされるかどうかです。 その場合、おそらくテストのためにロジックを分解する必要があります。 そうでない場合は、コードをよりきちんと整理することをお勧めします。



実際には、状況に応じて、Runnable、Callable、Consumer、またはSupplierインターフェースを使用してメソッドの内部ロジックを実装すると便利です。



値の計算方法



単一の値の計算は、複雑なルールに従って計算される場合、または追加の中間計算が必要な場合、重要なタスクになります。 その結果、コードは「ガベージ」になる場合があります。 メソッドのさらなるロジックは、クライアントブラウザのバージョンがサポートされているかどうかに依存するとしましょう。 これを行うには、最初にブラウザーのタイプを判別し、次にタイプに応じてバージョンを比較し、おそらくいくつかの追加パラメーターを考慮する必要があります。



 final boolean isSupportedBrowser = ((Supplier<Boolean>)()->{ final BrowserType browserType = … final boolean isMobileBrowser = … final boolean isTabletBrowser = … finalfinalif ( … && … ) { return true; } if ( … || … ) { return true; } else { if ( … ) { return true; … return false; }).get();
      
      





注:もちろん、この例では、サポートされるクライアントの計算ロジックは純粋に特定のものであると想定されています。それ以外の場合は、別のヘルパーメソッドで取り出す必要があります。



isSupportedBrowserの値をコードで直接計算することは可能ですが、すべての補助変数はスコープ内に残り、計算の開始時と終了時はそれほど明確ではありません。 上記の例では、構造は次のとおりです。



 isSupportedBrowser = ((Supplier<boolean>)()->{ … }).get();
      
      



フラグの計算を担当するロジックの部分を明確に分離し、プロセスで必要であったがそれ以上必要ではないすべての中間値を非表示にするのに役立ちます。



退屈なコードを構造化する方法



コードは常に複雑であるとは限らず、場合によっては非常に複雑です。 あるデータモデルを別のデータモデルに移動するタスクは、一般的なものです。 結局、このメソッドは500行かかります。 まだ理解できる。 私たちはそれを受け取り、ここに置き、それを受け取り、そこに置きます。 まれなifまたは追加の計算がありますが、天候は行いません。 同時に、コードをより小さなメソッドに分割しようとすると、さらに悪化する可能性があります。 メソッド間でデータを転送したり、メソッドの中間結果の追加モデルを決定したりする必要があります。



空の行は、このようなコードを整理するのに非常に役立ちます。 コードを段落に分割することにより、コードをさらに読みやすくすることができます。 しかし、さらに先へ進んで、{}で意味のある瞬間を強調することもできます-中括弧。 これは、コードをブロックで構成するのに役立つだけでなく、セクションの1つだけに必要な値をスコープから非表示にします。



 OutputReport transformReport(final InputReport input) { final OutputReport output = new OutputReport(); { //transform report header: final boolean someFlagUsedOnlyHere = … … 50 lines of code … } { //transform report body: … 50 lines of code … { //transform section 01: … 50 lines of code … } … { //transform section 04 (end section): … 50 lines of code … } } { //transform report summary: … 50 lines of code … } return output. }
      
      





もちろん、これは常に最良のアプローチではありません。 {}ブラケットをいくつ追加しても、コードは改善されず、書き換えるだけで済みます。 すべてが非常に簡単かつ素晴らしくメソッドに分割されることが起こります。 ただし、{}を使用すると、必要に応じてコードに表現力を追加できます。



おわりに



アプリケーションは、どんなに大きくても小さくても、最終的には単純なもので構成されます。条件、ループ、式、データモデル、最終的にはコード行です。 アプリケーションのアーキテクチャがどれほど美しく呼ばれていても、どのプログラミングパターンやフレームワークが使用されているかに関係なく、主なことは、画面に表示されるコードがどのように機能するかを理解することがどれだけ簡単か難しいことです。 上記の例では、コードの複雑さを少し抑え、表現力を少し高めることができます。 少なくとも私たちにとっては便利です。



All Articles