※本記事は、“Ten Java coding antipatterns to avoid: Worst practices #10 through #6” の翻訳記事です。

2022年7月15日| 9分読む

Ian Darwin


これらの最悪の慣習を回避し、既存のコードを保守またはリファクタリングするときに修正する必要があります

 

経験を積むと、誰もが良い習慣と悪い習慣の知識を得ることができ、コーディングとコード・レビューの両方に反映されます。Java ChampionであるTrisha Geeは、「コード・レビューでの5つのアンチパターン」の記事で、コード・レビュー・プロセスの最悪のいくつかの習慣を指摘しました。私はコーディング・プロセス自体に10個のアンチパターンを指摘したいと思います。この記事に半分あり、次の記事で最悪の犯罪者がJava Magazineに掲載されています。

明確に伝えると、これらの最悪の習慣を回避し、既存のコードを保守またはリファクタリングするときにそれらを排除すべきです。もちろん、コード・レビュー中にこれらの問題を見つけたら解決します。

 

最悪の習慣 #10: 散らかったimport

クラス上部のimportされたクラスおよびメソッドのリストは、使用しているAPIへの参照になります。*で終わるimportは特定の情報をほとんど伝達せず、さらに悪いことに、未使用のimportは誤解を招きます。並びにほとんど一貫性のないのimportは、読取りに時間がかかり、メンテナンス時の頭痛の種になります。。

より優れた方法: IDEでimportを管理できるようにします。Eclipse IDEは、これに対する非常に優れたサポートを提供します。「importの編成」機能は、1回のクリックで、未使用のimportを削除し、欠落しているimportを追加し、リストを一貫した順序でソートします。最初にjavaクラス、次にjavax、次にサードパーティ・クラス、次に静的import を配置します。IntelliJ IDEAでもこれらすべてが機能しますが、これらを有効にするには3つまたは4つの設定を調整する必要があります。

真の告白: 名前のない大規模なアプリケーション・プロジェクトの若くて愚かな技術リーダーだったとき、Eclipseの設定で未使用のimportのメッセージング・レベルをWarningからErrorに設定し、これをプロジェクト・リポジトリにコミットしました。もちろん、開発チームの講義と指導がうまくいかなかったのでこの最悪の習慣を行いました。これは、importをプロジェクト全体で整理する計画の一部でした。この設定の変更は受け入れられませんでしたが、反対した何人かの開発者は、(Ctrl+Shift+Oを使用して)修正がいかに簡単であったか、そしてこの変更によって、そのプロジェクトでのimportの長いリストがいかに読みやすくなったかに気づき、賛成してくれるようになりました。

 

最悪の習慣 #9: インデントの不整合性

インデント・チャンピオン言語はPythonで、中カッコやキーワードのかわりにインデントを使用して制御フローまたはメソッドの本体を示します。したがって、Pythonコードが正しくインデントされていない場合はコンパイルされません。

Java (他のCファミリ言語など)では、ブロック構造に中カッコが使用され、空白は無視されます。それでも、一貫したインデントは重要です。インデントはコンパイラで必要ではありませんが、人が読むために必要です。次のコードを考えてみます。

if (condition)
    statement1;
    statement2;
statement3;

一見すると文1および2がifによって制御されているかのように見えます。ただし、文2はPythonではなくJavaであるため、そうではありません。

または、次のコードを考えてみます。

statement1;
   statement2;
 statement3;

プログラマーは何を考えていたのでしょうか?コードは、風の強い日に滝によって噴出した何かのようです。文には同じレベルの制御フローがあるため、すべて同じ列で開始すべきです。繰り返しになりますが、最新のIDEでは、「インデントの修正」などの機能を使用して、この問題をすばやく修復できます。

Ctrl+A または Cmd+A を使用してファイル全体を選択するか、マウスで1つのメソッドを選択して選択します。次に、「Edit」または「Code」メニューからインデント修復を選択します。問題が解決しました!

 

最悪の習慣 #8: リンクのないJARファイル

Javaが初めて世に出た時、Windows開発者の悪夢の1つが解決されると思われました。バージョンの不一致しない共有オブジェクト(Windowsの.dllファイルや、他のプラットフォームの.soファイルなど)が混在する「DLL Hell」です。

残念ながら問題は解決されませんでした。これは、Java Platform Module System (JPMS)が対処する部分になっています。MavenGradleなどのツールがこの問題に長年役立っていますが、リンクのないJARファイルがまだ表示されることがあります。

最悪のケースは、次のような名前のファイル・フォルダを持つプロジェクトです。

util.jar
system.jar
financial.jar
report.jar

ファイルの日付は約10年前のものでした。4つの各プロジェクトは、その間にメンテナンス・プログラムによって更新されましたが、メイン・アプリケーションが依存しているライブラリJARファイルのバージョンの記録はありませんでした。ただし、「 lib フォルダ内に存在するJARファイル」をドキュメント形式と判断した場合を除きます。

一部のJARファイルは、長年にわたり複数のニュースで話題となったセキュリティの問題が発生したサードパーティAPI(有罪性を保護するために名前が変更された)のものですが、チームの開発者は、バージョン管理されたJARファイルへの移動をあまり心配していないようでした。影響のあるバージョンを使用しているかどうかはわからなかったためです。

私は、この数年前にこのようなプロジェクトをいくつか作っているかもしれないと認めますが、それを避けるという約束をしました。

今日、すべてのプロジェクトはMavenまたはGradleによって管理され、それぞれが各依存関係のグループ(通常は組織)、アーティファクト(JAR名)およびバージョン番号の指定を取得し、一致するJARファイルをフェッチします。そのファイルには、ファイル名にアーティファクト名とバージョン番号が含まれます。たとえば、プロジェクトのMaven構成ファイル (pom.xml)に次のものがある場合があります。

 

<dependency>
    <groupId>com.darwinsys</groupId>
    <artifactId>darwinsys-api</artifactId>
    <version>1.7.5</version>
</dependency>

pom.xmlのこのコードは、Mavenに対して、 darwinsys-api-1.7.5.jar ファイルをダウンロードし、(メタデータ・ファイルとともに)ホーム・ディレクトリ~/.m2/repository/com/darwinsys/darwinsys-api/1.7.5)の慎重に構築されたツリーに格納するように指示しています。このようにして、2つ以上のプロジェクトで同じJARファイルが必要な場合、JARは1回のみダウンロードされます。

ここでは、システムの1つのあるMavenローカル・リポジトリを解説します。

$ ls ~/.m2/repository
aopalliance biz bouncycastle cglib com dev eclipse edu info io
jakarta javax jaxen jline log4j ...
$ ls ~/.m2/repository/com/darwinsys/darwinsys-api
1.5.14
1.5.15
1.7.5
maven-metadata-central.xml
maven-metadata-central.xml.sha1
resolver-status.properties
$ ls ~/.m2/repository/com/darwinsys/darwinsys-api/1.7.5
_remote.repositories
darwinsys-api-1.7.5.jar
darwinsys-api-1.7.5.jar.sha1
darwinsys-api-1.7.5.pom
darwinsys-api-1.7.5.pom.sha1
$

pom.xml ファイルを調べると、その特定のプロジェクトでどのバージョンのAPIが使用されているかが明らかになるだけでなく、JARファイルが集中管理されたMavenリポジトリであるMaven Centralからのものであったことも明確になります(少なくとも、デフォルトが何であるか、および pom.xml ファイルに他のリポジトリがリストされていない場合)。

さらに、JARファイル自体のバージョン番号がファイル名に埋め込まれています。

MavenはSecure Hash Algorithm (sha)ファイルを使用して、JARファイルが改ざんされていないことを確認します。ビルド・ツールをデバッグ・モードで実行すると、クラスパスにある各JARファイルのフルパスを含む非常に詳細な出力が表示されます。さらに、Mavenには mvn 依存性などの機能があります。ツリーは、すべてのサブおよびサブサブ依存関係をツリー形式で表示します。

JARの依存性を制御し続けることは、ソフトウェア開発を規律にすることです。ぜひ始めましょう!

 

最悪の習慣 #7: 意味のない名前

Ianの「コーディングの第一法則」を引用する良い機会です

作成時以外は、数文字以上の名前を入力しないでください。

ほとんどの開発者(2つまたは3つの vi ダイハードを除く)が最近フル機能のIDEを使用していることを考えると、すべての主要なIDEには本当に優れたコード補完機能があるため、長い名前を入力する理由はありません。

ただし、メソッド、フィールド、クラス、変数およびその他の要素に意味のある名前を付けないようにする理由はありません。

i, j, kなどの変数名は、古い形式のforループを使用して配列にインデックスを作成したり、何かをカウントしている場合にのみ許可されます。また、ローカルで使用されるStringの場合は、数行のメソッドのヘッダー、または短く自己完結型のラムダを記述する場合はsなどの名前も許可されます。

他のすべてについて、有用な名前を選択してください。

これは、varキーワードを使用して型宣言を指定する必要がない場合に特に重要です。なぜ?変数名は、使用する変数を読む人に対する唯一の手がかりになります。次の例を考えてみます。

 

 for (int i = 0; i < functionData.length; i++) {
        functionData[i] = someFunction(i);
}

customerNames.forEach(s->s.substring(1)); // "s" OK here

int bodyFontSize = 11;

変数のことだけを伝えているのではなく、メソッド名も意味のあるものである必要があります。JUnitテストを記述する場合、 test1() やttest2() などの名前は役に立たないだけでなく、誤解を招く場合があります。このようなネーミングは、存在しない順序をを暗示しているだけです。。

JUnitは、記述した順序でメソッドを実行するよう要求しません。メソッドは、実際にはリフレクションAPIで指定された順序で実行されます。これは、「ソートされず、特定の順序ではない」メンバーを返すようにドキュメントに書かれています。

この反パターンの例を次に示します。

 

 @Test
    public void test1() {   // Bad
        // test here...
    }

もっと良い方法があります。

    @Test
    public void testPositiveResultsCorrect() { // Better
        // test here...
    }

あなたは、あなたが書いた数ヶ月後、または数年後にこのコードを読む必要のある人々の一人だということを忘れずにいましょう。自分自身に対して親切になりましょう!

最悪の習慣 #6: パンクしたタイヤの再発明

このアンチパターンのタイトルは、私が何年も前に書いた文書からのものです。”車輪の発明”は、すでに存在するものをデザインし作成ことを表す一般的な英語の慣用表現です。当時の同僚のジェフ・コリヤー(Geoff Collyer)と私は、遥か彼方の銀河で共著したCコーディングスタイルの文書でその表現を一歩進めました。「パンクしたタイヤの再発明」とは、プログラマが標準ライブラリまたは共通ライブラリですぐに使用できる機能のコードを記述するだけでなく、新しいコードが公開されているAPIより劣った処理を実行したことを表しています。

この反パターンの例を次に示します。

String[] candidates = getStrings();
String searchingFor = "The Lost Boys";
int found = -1;
for (int i = 0; i < candidates.length; i++) { // flat tire
    if (candidates[i].equals(searchingFor)) {
        found = i;
    }
}

もっと良い方法があります。

Arrays.sort(candidates); // start of "better" approach
found = Arrays.binarySearch(candidates, searchingFor);

バイナリ検索では入力をソートする必要があるため、2番目の方法の実行速度が遅くなる可能性があります。確かにその通りです。ただし、アンチパターンのプログラマは、一致を検索するときにループから抜け出すのを忘れたため、コードの効率はとにかくひどくなります。

公開されているAPIの再発明は新しいものではなく、多くの場合APIに関する不完全な知識を示しています。もちろん、Javaのような広大な標準ライブラリが言語にある場合は、そのエラーを簡単に解決できます。

次に、Java 1.7以降、プラットフォームにあったAPIの再発明の例を示します。

var x = getValue();     // legacy way
if (x == null) {
    x = getSomeDefaultValue();
}
System.out.println(x);

もっと短く、良い方法があります。

var y = Objects.requireNonNullElse(getValue(), getSomeDefaultValue());
System.out.println(y);

1つ目の例のプログラマは、標準の Objects.requireNonNullElse()ライブラリ・ルーチンを使用でき、これには、いくつかの共通操作のコーディングの削減に役立つ様々なオーバーロードがあります。

nullおよびnull以外の引数の処理について、null値に対応する必要がある場合はOptionalクラスを確認し、ラムダでOptionalのクイズを解いてみましょう

 

まとめ

この記事では、私の好きなアンチパターンの5つを掲載しています。今後の記事「回避すべき10個のJavaコーディング・アンチパターン: 最悪の習慣 #5から#1」では、他の5つを詳しく見てみましょう。

 

より詳しい情報