Masayan tech blog.

  1. ブログ記事一覧>
  2. Clean Code アジャイルソフトウェア達人の要約と読んだ感想

Clean Code アジャイルソフトウェア達人の要約と読んだ感想

公開日

Clean Code アジャイルソフトウェア達人

ボブおじさん(ロバートC. マーチン)著

この本を読むと得られるもの

  • 「クリーンコード」とは何かを理解できる
  • 「クリーンコード」を書くための具体的な方法、アンチパターンが理解できる
  • リーダブルコードの発展版といったイメージ(類似する内容も含まれている)
  • 具体的なリファクタリング工程のサンプルがついているので、リファクタリングの方法を理解できるようになる

以降で、全てを紹介し切ることはできないので、私自身が特に気づきを得た部分を抜粋して紹介(全内容を見たい方はぜひ購入して読んでみてください。)

第2章 意味のある名前

検索可能な名前をつける

複数のファイルから参照されるようなスコープの広い変数には、具体的で検索可能な名称をつけるべき

Bad

# file1.py
x = "https://api.example.com"

# file2.py
from file1 import x
response = requests.get(x)

Good

# file1.py
API_URL = "https://api.example.com"

# file2.py
from file1 import API_URL
response = requests.get(API_URL)

インターフェースと実装

インタフェース名にIというprefixをつけたり、Interfaceというsuffixをつけることは冗長なので避けるべき。こういったprefixなどをつけずしてインターフェースであるということを表す名称をつけるべきで、どうしても表現できないのであれば、実装クラスに「Impl」等の名称をつける方が好ましい

Bad

public interface ICar {
    void drive();
}

public class Car implements ICar {
    @Override
    public void drive() {
        // implementation
    }
}

Good

public interface Car {
    void drive();
}

public class CarImpl implements Car {
    @Override
    public void drive() {
        // implementation
    }
}

解決領域の用語と問題領域の用語

  • プログラムを作成する際、問題領域(ドメイン)の用語と解決領域(ソリューション)の用語をシチュエーションに合わせて的確に使い分けるべき
  • 良いソフトウェア設計では、問題領域の用語をコードに反映させることが推奨され、コードはビジネスロジックをより直感的に表現し、理解しやすくなる
  • 問題領域とは関係の薄い、プログラマチックな処理に関しては解決領域の用語を使うという区別が重要

問題領域の用語

  • ビジネスロジックやアプリケーションの目的を表すために使用される
  • ビジネスルール、エンティティ、操作などを表すために使用される
  • 例えば、銀行アプリケーションでは、「口座」、「振込」、「残高」など

解決領域の用語

  • データ構造、アルゴリズム、パターン、フレームワークなどを表すために使用される
  • 例えば、「リスト」、「ツリー」、「クラス」、「インターフェース」など

第3章 関数

逓減規則

コードは上から下へ物語のように読める必要がある。都度、関数やメソッドの詳細にジャンプしないといけないような実装は避けるべき

switch文より多態性

Bad

public class MyClass {
    public void doSomething(String type) {
        switch (type) {
            case "type1":
                // do something for type1
                break;
            case "type2":
                // do something for type2
                break;
            // more cases...
        }
    }

    public void doSomethingElse(String type) {
        switch (type) {
            case "type1":
                // do something else for type1
                break;
            case "type2":
                // do something else for type2
                break;
            // more cases...
        }
    }
}

Good

  • MyClassのメソッドはtypeに応じた振る舞いをTypeBehaviorインターフェースを実装したクラスに委譲することで、各メソッドでのswitch文の使用を避けることができる
  • 新たなtypeが追加された場合でも、新たなTypeBehaviorの実装を追加するだけで対応可能
public interface TypeBehavior {
    void doSomething();
    void doSomethingElse();
}

public class Type1Behavior implements TypeBehavior {
    @Override
    public void doSomething() {
        // do something for type1
    }

    @Override
    public void doSomethingElse() {
        // do something else for type1
    }
}

public class Type2Behavior implements TypeBehavior {
    @Override
    public void doSomething() {
        // do something for type2
    }

    @Override
    public void doSomethingElse() {
        // do something else for type2
    }
}

public class MyClass {
    private TypeBehavior behavior;

    public MyClass(TypeBehavior behavior) {
        this.behavior = behavior;
    }

    public void doSomething() {
        behavior.doSomething();
    }

    public void doSomethingElse() {
        behavior.doSomethingElse();
    }
}

関数の引数の数について

  • 引数はないのがベスト
  • 引数が増えるほど理解が難しくなり、実装詳細を確認しないといけないケースが増える
  • 引数オブジェクトを有効活用すべし
  • フラグ引数は害悪
  • assertEqualsの引数の順番

引数オブジェクト

Ex.円の方程式

Bad

円の中心のx座標、y座標、および半径

Circle makeCircle(double x, double y, double radius)

Good

Pointオブジェクト(x座標とy座標を保持する)と半径

Circle makeCircle(Point center, double radius)

副作用を避ける

副作用は、関数の振る舞いを予測しにくくするため、可能な限り副作用を避け、純粋な関数(同じ入力に対して常に同じ出力を返し、副作用のない関数)を使用することが推奨される
副作用・・・グローバル変数の変更、ファイルの読み書き、クエリ発行、など

Bad

let total = 0;

function add(a, b) {
    total = a + b;  // This changes the state of an external variable
}

Good

function add(a, b) {
    return a + b;
}

コマンド・照会の分離(Command Query Separation: CQS)原則

  • 関数、メソッドが状態を変更する(コマンド)か、情報を返す(照会)かのどちらか一方だけを行うべきであるという原則
  • 両方を同時に行う関数、メソッドは混乱を招きやすい

Bad

incrementAndGetメソッドは、状態を変更(カウントを増やす)し、値を返す(現在のカウント)という2つの操作を行ってる

public class Counter {
    private int count = 0;

    // This method violates the CQS principle because it changes state and returns a value
    public int incrementAndGet() {
        count++;
        return count;
    }
}

Good

public class Counter {
    private int count = 0;

    // This method is a command. It changes state but does not return a value
    public void increment() {
        count++;
    }

    // This method is a query. It returns a value but does not change state
    public int getCount() {
        return count;
    }
}

第5章 書式化

変数宣言の垂直距離

変数が宣言されてから最初に使用されるまでのコード行数のことを指し、変数は使用する直前で宣言することが推奨される。これにより、変数のスコープが小さくなり、コードの可読性と保守性が向上する

Bad

result変数が宣言されてから使用されるまでに他の関連しないコードが存在する

public class Example {
    public void badCase() {
        int result; // Variable declared here

        // A lot of unrelated code...
        for (int i = 0; i < 100; i++) {
            // More unrelated code...
            for (int j = 0; j < 100; j++) {
                // Even more unrelated code...
            }
        }

        // Some more unrelated code...
        for (int i = 0; i < 100; i++) {
            // More unrelated code...
            for (int j = 0; j < 100; j++) {
                // Even more unrelated code...
            }
        }

        result = calculateResult(); // Variable used here
        System.out.println(result);
    }

    private int calculateResult() {
        // Some calculation...
        return 42;
    }
}

Good

public class Example {
    public void goodCase() {
        // A lot of unrelated code...
        for (int i = 0; i < 100; i++) {
            // More unrelated code...
            for (int j = 0; j < 100; j++) {
                // Even more unrelated code...
            }
        }

        // Some more unrelated code...
        for (int i = 0; i < 100; i++) {
            // More unrelated code...
            for (int j = 0; j < 100; j++) {
                // Even more unrelated code...
            }
        }

        int result = calculateResult(); // Variable declared and used here
        System.out.println(result);
    }

    private int calculateResult() {
        // Some calculation...
        return 42;
    }
}

依存関数の垂直距離

呼び出される側の関数は呼び出す側の関数のすぐ下で定義されるべき

Bad

methodBはmethodAのすぐ下に配置されていない

public class Example {
    public void methodA() {
        // Some code...
        methodB();
        // Some more code...
    }

    public void methodC() {
        // Some code...
    }

    public void methodB() {
        // Some code...
    }
}

Good

public class Example {
    public void methodA() {
        // Some code...
        methodB();
        // Some more code...
    }

    public void methodB() {
        // Some code...
    }

    public void methodC() {
        // Some code...
    }
}

第6章 オブジェクトとデータ構造

デメテルの法則

  • オブジェクト指向プログラミングにおける設計原則の一つで、"オブジェクトはその直接の隣人のみと交流すべきであり、遠くの知り合いと直接交流すべきではない"という考え方
  • オブジェクト間の疎結合を促進し、コードの可読性と保守性を向上させるための原則

具体的には、あるオブジェクトが別のオブジェクトのメソッドを呼び出す場合、そのオブジェクトは以下のいずれかに該当するべき

  • 自身のオブジェクト
  • メソッドの引数として渡されたオブジェクト
  • 自身が作成したオブジェクト
  • 自身のインスタンス変数に格納されているオブジェクト

Bad

Driverクラスのstart_carメソッドがCarクラスのengine属性のstartメソッドを直接呼び出しているため、デメテルの法則に違反

class Engine:
    def start(self):
        pass

class Car:
    def __init__(self):
        self.engine = Engine()

class Driver:
    def __init__(self, car):
        self.car = car

    def start_car(self):
        self.car.engine.start()  # デメテルの法則に違反

Good

Carクラスにstart_engineメソッドを追加し、Driverクラスはこのメソッドを呼び出すように変更。これにより、DriverクラスはCarクラスの内部構造(engine属性)を知る必要がなくなる

class Engine:
    def start(self):
        pass

class Car:
    def __init__(self):
        self.engine = Engine()

    def start_engine(self):  # CarクラスがEngineのstartメソッドをラップ
        self.engine.start()

class Driver:
    def __init__(self, car):
        self.car = car

    def start_car(self):
        self.car.start_engine()  # デメテルの法則を遵守

アクティブレコード

アクティブレコード(ORM)のメソッドにビジネスロジックを含めるべきでない。単にレコードを取得するためのデータ構造として扱うべき(オブジェクトとして扱ってはならない)

例えばECシステムでCustomerモデルがあり、その会員がお届け日時を指定できるかどうかは、その会員のランクとそのほかの条件によって変動するというビジネスロジックがある場合、会員のランクをチェックして、真偽値を返すというメソッドをモデルに実装すべきでない

第8章 境界

サードパーティのコード

  • 各言語が用意しているユーティリティモジュールやサードパーティのモジュールは非常に強力で、多くの機能を提供してくれる一方、これらの全ての機能がアプリケーションで必要とされるわけではない
  • 意図しない処理を防ぐ目的で、アプリケーションが必要とする機能のみを提供するラッパークラスを作成することはとても効果的
  • 結合度が弱くなるので、上記以外にも、依存しているモジュールに変更があった場合の影響を受けにくくなる

Bad

datetimeモジュールの全ての機能が利用可能。しかし、アプリケーションが必要とするのは現在の日時を取得する機能だけの場合、過剰すぎる

from datetime import datetime

# Usage
current_time = datetime.now()

Good

DateTimeWrapperクラスでは、datetimeモジュールのnowメソッドのみを提供している。これにより、アプリケーションが必要とする機能のみが利用可能となり、意図しない処理を防ぐことができる

from datetime import datetime

class DateTimeWrapper:
    @staticmethod
    def now():
        return datetime.now()

# Usage
current_time = DateTimeWrapper.now()

第9章 ドメイン特化テスト言語

  • テストを読みやすくするために、テストのためだけに用意される関数やユーティリティをテスト言語と呼ぶ
  • テスト言語を使用すると、テストケースの作成がより直感的になり、テストの可読性が向上するとともに、テストの準備やアサーションのロジックが一箇所に集約されるため、テストの保守性も向上する

テストケースの準備やアサーションを用意する例

import unittest

class TestUser(unittest.TestCase):
    def test_user(self):
        user = create_user_with_name_and_age("John", 25)

        assert_user_has_name_and_age(self, user, "John", 25)
def create_user_with_name_and_age(name, age):
    user = User()
    user.name = name
    user.age = age
    return user

def assert_user_has_name_and_age(testcase, user, name, age):
    testcase.assertEqual(user.name, name)
    testcase.assertEqual(user.age, age)

DSLについては第11章でも言及されている

第10章 クラス

カプセル化とテスト

テストを第一優先に考え、場合によってはカプセル化を緩めることも検討する(private → protected)

第17章 においと経験則

あって当然の振る舞いが実装されていない

関数名、メソッド名からその振る舞いを予測できないため、実装の詳細を毎回確認しないといけなくなる

Ex.日付の数値から曜日の文字列を生成する関数
引数のバリデーションがない(0から6の範囲外の数値や、数値以外の値が引数として渡された場合の考慮がない)

Bad

def get_day_of_week(date_enum):
    days_of_week = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
    return days_of_week[date_enum]

Good

def get_day_of_week(date_enum):
    if not isinstance(date_enum, int) or not 0 <= date_enum <= 6:
        return None  # or raise a custom exception

    days_of_week = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
    return days_of_week[date_enum]

セレクタ引数

セレクタ引数とは、関数の振る舞いを制御するために使用される引数のこと。関数の複雑さが増し、読み手は関数の全体的な振る舞いを理解するのが難しくなり、テストの複雑さも増す

Ex. reverse引数がセレクタ引数として機能し、名前のフォーマットを制御

def format_name(first_name, last_name, reverse=False):
    if reverse:
        return f"{last_name}, {first_name}"
    else:
        return f"{first_name} {last_name}"

マジックナンバー

マジックナンバーは、その値が何を意味するのか明確でない、またはその値が何度も繰り返し使われる値のことを指す。これは数値だけでなく、文字列や他のデータ型にも当てはまる

Bad

この関数では、"USA"、"Canada"、"Australia"という文字列がマジックナンバーとして使われている。

def get_shipping_cost(country):
    if country == "USA":
        return 5.00
    elif country == "Canada":
        return 10.00
    elif country == "Australia":
        return 15.00
    else:
        return 20.00

Good

この問題を解決するためには、これらのマジックナンバーをEnumや定数に置き換えることが推奨される

USA = "USA"
CANADA = "Canada"
AUSTRALIA = "Australia"

def get_shipping_cost(country):
    if country == USA:
        return 5.00
    elif country == CANADA:
        return 10.00
    elif country == AUSTRALIA:
        return 15.00
    else:
        return 20.00

規約より構造

  • 設計やコーディングルールを規約として守ってもらうのではなく、ルールをそもそも破ることができない構造を作る、ことが重要

Ex.カプセル化
オブジェクトの内部状態を直接操作することを防ぎ、代わりにメソッドを通じてのみその状態を変更することを強制する概念。これは「構造」による制約。

class BankAccount:
    def __init__(self, initial_balance):
        self._balance = initial_balance

    def deposit(self, amount):
        if amount > 0:
            self._balance += amount
        else:
            print("Deposit amount must be positive.")

    def withdraw(self, amount):
        if amount > 0 and amount <= self._balance:
            self._balance -= amount
        else:
            print("Withdrawal amount must be positive and not exceed the current balance.")

    def get_balance(self):
        return self._balance

このBankAccountクラスでは、_balance属性はプライベートとして宣言されており、クラスの外部から直接アクセスすることはできない。代わりに、deposit、withdraw、get_balanceといったメソッドを通じてのみ_balanceの値を操作できる

このように、カプセル化は「構造」によってコーディング設計を強制し、クラスの内部状態を適切に管理することを保証する。これにより、不適切な状態変更や予期しない副作用を防ぐことができる

些細なテストを省略しない

簡単な関数やクラスであっても、テストは必ず作成することで、ドキュメントとしての機能を担保する

まとめ

いかがでしたでしょうか。本記事ではClean Code アジャイルソフトウェア達人の要約と読んだ感想について紹介させていただきました。ボブおじさんの長年のリファクタリング経験から得たノウハウや具体的なリファクタリング例をこの1冊で知ることができるので、保守性の高いコードを設計・実装できる中級者以上のレベルになるためには読んでおくべき1冊だと感じました。