0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

「Good Gode, Bad Code」を読んで業務上重要だと感じたルールを抜粋

Posted at

はじめに

以前にチーム開発、仕事におけるリーダブルなコードを書くための記事を書きました

非常に多くの反応をいただき、ありがたい限りです。

それだけ良いコードを書くことは重要視されていると感じたため、「Good Gode, Bad Code」という著書を購入したので、読んだ中でも業務上重要だと感じたルールを抜粋し紹介します。

対象読者

  • 経験が3年以内のジュニアエンジニア
  • 静的型付け言語を扱うエンジニア

1 理論編

1.1 コードの品質

1.1.2 コードの品質のゴール

  1. 正しく動くこと
  2. 正しく動作し続けること
  3. 要件の変更に対応しやすいこと
  4. 車輪の再発明をしないこと

以下はJavaを用いて、良いコードを書くための4つのポイントについて具体的に解説します。

1. 正しく動くこと

  • 説明: 最も基本的な要件は、コードが指定された動作を正確に行うことです。

    Javaの例
    public int add(int a, int b) {
        return a + b;
    }
    
  • ポイント: 単純な関数でも、テストを行って正確に動作することを確認することが重要です。

2. 正しく動作し続けること

  • 説明: コードが変更されたときや、新しい環境で動かされたときも正しく動作し続けることが求められます。

    Javaの例
    public class Calculator {
        private int result = 0;
    
        public int add(int value) {
            result += value;
            return result;
        }
    
        public int subtract(int value) {
            result -= value;
            return result;
        }
    }
    
  • ポイント: Calculatorクラスのメソッドを変更する場合、他のメソッドの動作に影響を与えないように注意が必要です。ユニットテストを書くことで、変更が他の機能に影響を与えていないことを確認できます。

3. 要件の変更に対応しやすいこと

  • 説明: 要件が変更されることは日常的に発生します。そのため、変更に柔軟に対応できる設計が重要です。

    Javaの例
    public interface Payment {
        void pay(int amount);
    }
    
    public class CreditPayment implements Payment {
        @Override
        public void pay(int amount) {
            // クレジットカードでの支払い処理
        }
    }
    
    public class CashPayment implements Payment {
        @Override
        public void pay(int amount) {
            // 現金での支払い処理
        }
    }
    
  • ポイント: インターフェースを使用することで、新しい支払い方法が追加されたときにも柔軟に対応することができます。

4. 車輪の再発明をしないこと

  • 説明: 既に提供されているライブラリやフレームワークを適切に利用することで、時間の節約、品質の向上、そして不必要なエラーやバグの回避が可能になります。

    例えば、リストのデータをフィルタリングや変換、集計する場合、自前でループを書いて処理を実装するのではなく、Java 8から導入されたStream APIを使用することで、簡潔で読みやすいコードを書くことができます。

    再発明の例
    List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
    List<String> uppercasedNames = new ArrayList<>();
    for (String name : names) {
        uppercasedNames.add(name.toUpperCase());
    }
    
    既存のツールを利用した例
    List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
    List<String> uppercasedNames = names.stream()
                                        .map(String::toUpperCase)
                                        .collect(Collectors.toList());
    
  • ポイント: クリーンで効率的なコードを書くためには、既存のライブラリやAPIのドキュメントを頻繁に参照し、それらのベストプラクティスを学ぶことが不可欠です。これにより、コードの量を減らし、読みやすく、そして保守しやすいコードを書くことができます。

1.1.3 コード品質の柱

  1. コードを読みやすくする
  2. 想定外の事態をなくす
  3. 誤用しにくいコードを書く
  4. コードをモジュール化する
  5. コードを再利用、汎用化しやすくする
  6. テストしやすいコードを書き、適切にテストする

良いコードを書くための続き: Javaでの具体的なアドバイス

良いコードは、理解しやすく、安全で、柔軟性があり、テストしやすいものであるべきです。以下の6つのポイントをJavaで詳しく解説します。

1. コードを読みやすくする

  • 説明: コードは書く時間よりも読む時間の方が長いため、明確で簡潔なコードが求められます。

    // 良い例
    public int calculateTotal(List<Integer> prices) {
        return prices.stream().mapToInt(Integer::intValue).sum();
    }
    
    // 悪い例
    public int x(List<Integer> y) {
        int z = 0;
        for (int i = 0; i < y.size(); i++) {
            z += y.get(i);
        }
        return z;
    }
    
  • ポイント:

    • 変数名やメソッド名は意味が伝わるような名称を使用すること。
    • 何をしているか
    • どのようにするか
    • 要素としてないが必要か
    • このコードを実行するとどんな結果が得られるのか

を明確にする。

2. 想定外の事態をなくす

  • 説明: 例外処理や入力値の検証を行い、予期しないエラーやバグのリスクを低減します。

    Javaの例
    public void setAge(int age) {
        if (age < 0 || age > 150) {
            throw new IllegalArgumentException("Age must be between 0 and 150.");
        }
        this.age = age;
    }
    

3. 誤用しにくいコードを書く

  • 説明: APIや関数の設計を明確にし、誤った使い方をしにくくします。

    Javaの例
    public final class DateRange {
        private final LocalDate startDate;
        private final LocalDate endDate;
    
        public DateRange(LocalDate startDate, LocalDate endDate) {
            if (startDate.isAfter(endDate)) {
                throw new IllegalArgumentException("Start date must be before end date.");
            }
            this.startDate = startDate;
            this.endDate = endDate;
        }
    }
    
  • ポイント: オブジェクトの不変性や適切なアクセス修飾子を使用して、不正な状態や操作を制限する。

4. コードをモジュール化する

  • 説明: 大きなタスクを小さな部分に分割して、それぞれを独立したモジュールとして実装します。

    Javaの例
    public class UserValidator {
        public boolean isValidName(String name) {
            // 名前のバリデーション
        }
    
        public boolean isValidAge(int age) {
            // 年齢のバリデーション
        }
    }
    

5. コードを再利用、汎用化しやすくする

  • 説明: 類似のタスクや操作を繰り返し実行する場合、共通のロジックをメソッドやクラスとして分離します。

    Javaの例
    public class StringUtils {
        public static boolean isNullOrEmpty(String str) {
            return str == null || str.isEmpty();
        }
    }
    

6. テストしやすいコードを書き、適切にテストする

  • 説明: 単体テストや統合テストを行うことで、コードの品質を維持・向上させます。

    Javaの例
    public class Calculator {
        public int add(int a, int b) {
            return a + b;
        }
    }
    
    // テストコード
    public class CalculatorTest {
        @Test
        public void testAdd() {
            Calculator calc = new Calculator();
            assertEquals(5, calc.add(2, 3));
        }
    }
    
  • ポイント: JUnitやMockitoなどのライブラリを使用して、効果的にテストを実施する。

1.2 抽象化レイヤー

1.2.1 疑似コードでのnullの扱い方

null参照は多くのプログラミング言語でエラーの原因となります。Javaでは、参照変数が予期せずnullになることでNullPointerExceptionが発生することがあります。これを防ぐためにnull安全なコードを書くことは非常に重要です。

1. Optionalを利用する

  • 説明: Java 8以降、Optionalクラスを使用して、nullの可能性を示すことができます。これにより、値が存在する場合としない場合の両方を安全に扱うことができます。
疑似コード
function findUser(userId) returns Optional<User>:
    user = fetchUserFromDatabase(userId)
    
    if user is not null:
        return Optional.of(user)
    else:
        return Optional.empty()
使用する際
Optional<String> nameOpt = findNameById(1);
nameOpt.ifPresent(name -> System.out.println(name));

1.2.2 なぜ抽象化レイヤーを作るのか

  • 複雑な問題を小さく切り分けるため

1.2.3 コードのレイヤー

  • 関数
  • クラス
  • インターフェース
  • パッケージ、名前空間、モジュール

1.2.3.2 関数

  • 単一のタスクを行う
  • 他の適切な名前をつけた関数を呼び出し、より複雑な振る舞いを構成する

1.2.3.3 クラス

  • 行数

    • 300行以下が理想
  • 凝縮
    凝縮とは、クラスのモノがどれだけ適切なクラスに「所属」しているかを評価するもの。凝縮度の高いクラスが理想

    • 逐次的凝縮
      クラスのメンバーが時間的な流れに従って連続的に実行される場合の凝縮。例えば、ログインプロセスを逐次的に実行するLoginProcessorクラスなど。
    • 機能的凝縮
      クラスのメンバーが特定の機能や目的に関連している場合の凝縮。例えば、数学的な計算を行うMathUtilitiesクラスなど。
  • 関心の分離
    一つのクラスは一つの関心事だけを持つべきです。これは、クラスが変更の理由を1つだけ持つべきであるという原則、通称「単一責任の原則」に基づいています。

// 悪い例
public class User {
    public void saveToDatabase() {
        // データベースへの保存処理
    }

    public void displayProfile() {
        // プロフィールの表示処理
    }
}

// 良い例
public class User {
    // Userの属性やメソッド
}

public class UserDatabase {
    public void save(User user) {
        // データベースへの保存処理
    }
}

public class UserProfile {
    public void display(User user) {
        // プロフィールの表示処理
    }
}

1.4 エラー

  • エラーは大きく分けて2種類ある
    • システムが回復可能なもの
    • システムが回復不可能なもの
  • エラーが起きた際は早く失敗し、回復不可能なエラーならば目立って失敗する方が良い
  • エラーを通知するテクニックは2つのカテゴリーに分けられる
    • 明示的: コードでのケイクの紛れもなく明確な部分
    • 暗黙的: コードでの契約の細かいコメントに埋もれて不明瞭か、そもそも記載されていない可能性がある

2 実践編

2.1 コードを読みやすくする

2.1.1 わかりやすい名前を使う

  • 説明: 変数、関数、クラスの名前は、その用途や役割を明確に伝えるものであるべきです。

    // 良くない例
    int d;
    
    // 良い例
    int daysUntilExpiration;
    
  • ポイント: 名前を短くするよりも、意味を明確にすることを優先しましょう。

2.1.2 コメントはコードが存在する理由を説明する

  • 説明: コメントは、コードが何をしているのかではなく、なぜそのコードが存在しているのかを説明するために使用すべきです。

    // 良くない例: 何をしているのかの説明
    // 合計を計算する
    int sum = a + b;
    
    // 良い例: なぜそのコードが必要なのかの理由
    // タイムゾーンの補正を行うための追加
    int adjustedTime = localTime + timeZoneOffset;
    
  • ポイント: コード自体は「何をしているのか」を示しているため、コメントでその内容を繰り返すのではなく、背景情報や特定の決定の理由などを伝えるためにコメントを使用しましょう。

2.1.2 一貫したコーディングスタイルにこだわる

  • 説明: ソースコード全体で一貫したコーディングスタイルを採用することで、読みやすさとメンテナンス性が向上します。

  • ポイント: インデント、命名規則、括弧の位置など、チームやプロジェクトごとのコーディングスタイルのガイドラインを持つことが推奨されます。

2.1.3 ネストの深いコードは避ける

  • 説明: コードのネストが深くなると、読む人はその複雑さに追われ、全体の流れがつかみにくくなります。

    // 良くない例
    if (conditionA) {
        if (conditionB) {
            if (conditionC) {
                // 処理
            }
        }
    }
    
    // 良い例
    if (!conditionA) return;
    if (!conditionB) return;
    if (!conditionC) return;
    // 処理
    
  • ポイント: 条件を早期にリターンすることや、複雑な条件をメソッドに分割することで、ネストの深さを減少させることができます。

3. ユニットテスト編

3.1 良いユニットテストとは

  • 破損を正確に検出する
  • 実装の詳細にとらわれない
  • よく説明された失敗
  • わかりやすいテストコード
  • 簡単かつ迅速に実行する

1. 破損を正確に検出する

概要: 良いユニットテストは、コードに問題がある場合にそれを確実に検出する能力が必要です。

例:

public int add(int a, int b) {
    return a + b;
}

@Test
public void testAdd() {
    assertEquals(5, add(2, 3));
}

上記のtestAddメソッドは、add関数が正しく2つの数字を加算するかどうかをテストします。

2. 実装の詳細にとらわれない

概要: テストはインターフェースやAPIの動作に焦点を当て、内部の実装の詳細には関心を持たないのがベストです。

例:

public List<String> fetchNames() {
    // DBから名前をフェッチする複雑なロジック
    // ...
    return names;
}

@Test
public void testFetchNames() {
    List<String> result = fetchNames();
    assertNotNull(result);
    assertFalse(result.isEmpty());
}

ここでは、fetchNamesの内部のDBのロジックを気にせず、結果がnullでなく、空でないことだけを確認しています。

3. よく説明された失敗

概要: テストが失敗した場合、なぜ失敗したのかを明確にするエラーメッセージを提供することが重要です。

例:

@Test
public void testAdd() {
    int expected = 5;
    int result = add(2, 3);
    assertEquals(expected, result, "Expected " + expected + " but got " + result);
}

4. わかりやすいテストコード

概要: テストコードは明確で、他の開発者が理解しやすくするために簡潔でなければなりません。

例:

@Test
public void testAdditionOfTwoNumbers() {
    int num1 = 2;
    int num2 = 3;
    int expectedResult = 5;
    
    int result = add(num1, num2);
    
    assertEquals(expectedResult, result);
}

このテストは、何をしているのかを明確に示しています。

5. 簡単かつ迅速に実行する

概要: テストは迅速に実行されるべきです。時間のかかるテストは開発のペースを遅くする可能性があります。

例:
データベースや外部APIへの呼び出しを含むテストは、モックやスタブを使用して、実際の呼び出しをシミュレートすることでテストの速度を向上させることができます。

@Test
public void testFetchNamesWithMock() {
    List<String> mockNames = Arrays.asList("Alice", "Bob");
    
    // Mocking the fetchNames method to return mockNames
    // ...
    
    List<String> result = fetchNames();
    
    assertEquals(mockNames, result);
}

このテストは、実際のデータベースへのアクセスを避け、モックデータを使用して迅速に実行されます。


まとめ

Good Code, Bad Codeでは具体的な例がたくさん記載されており、非常にわかりやすい内容になっていました。
今回紹介したものはほんの一部のため、気になった方は購入して読んだ方が良いかと思います。

実際の業務でもこちらの知識を活用して行きたいです。

X / Twitterにて日々技術/業務に関する発信をしているので、こちらもフォローしていただけると嬉しいです。

コメント、意見、いいねしていただけるとありがたいです

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?