かすてらすねお。

見聞録的ななにか。

『Clean Code』(2017)第10章「クラス」を読んだ

第10章 クラス(pp.189-207)

サマリー

Class Organization(クラスの構成)(p.189)

  • Javaの標準的な慣例に従う
  • 変数リストから始める
    • public static 定数が最初
    • 次に private static 変数
    • 最後に private インスタンス変数
    • public 変数を持つべき理由はほとんどない
  • 関数は、変数リストの後に配置
    • public 関数は変数リストの後に配置
    • privateユーティリティ関数は、それを呼び出すpublic関数の直後に配置

Encapsulation(カプセル化)(p.190)

  • 変数とユーティリティ関数はprivateにすべき
  • テストのためにprotectedにする必要が出てくることもある
  • カプセル化の緩和は最後の手段とすべき

Classes Should Be Small!(クラスは小さくしなければならない!)(pp.190-193)

  • 第一のルールは「小さくあること」
  • 第二のルールは「さらに小さくあること」
  • クラスの大きさは「責務」で測る

    • 責務とは、クラスの「変更を必要とする理由」のこと
    • 悪い例:リスト10-2、SuperDashboardクラスは以下の2つの責務を持つため大きすぎる

      public class SuperDashboard extends JFrame implements MetaDataUser {
          public Component getLastFocusedComponent()
          public void setLastFocused(Component lastFocused)
          public int getMajorVersionNumber()
          public int getMinorVersionNumber()
          public int getBuildNumber()
      }
      
      1. バージョン情報の追跡(ソフトウェアの出荷時に更新が必要)
      2. JavaSwingコンポーネントの管理(GUIの変更時に更新が必要)
    • 良い例:リスト10-3、単一の責務を持つVersionクラス

      public class Version {
          public int getMajorVersionNumber()
          public int getMinorVersionNumber()
          public int getBuildNumber()
      }
      
  • クラス名は責務を表現すべき

    • 悪い例:ProcessorManagerSuperなどの曖昧な名前
    • 良い例:Version(バージョン情報の管理という単一の責務を表現)
  • クラスの説明は25単語以内で、「if」「and」「or」「but」を使わずに書けるべき
    • 悪い例:「SuperDashboardは最後にフォーカスを持っていたコンポーネントへのアクセスを提供し、そしてバージョンと構築番号の追跡も行う」
    • 良い例:「Versionはソフトウェアのバージョン情報を管理する」

The Single Responsibility Principle(単一責務の原則)(pp.193-194)

  • Single Responsibility Principle(SRP):クラスは変更する理由を1つだけ持つべき
  • 多くの小さなクラスで構成される方が、少数の大きなクラスより理解しやすい
  • 動作するコードを書くことと、クリーンなコードを書くことは別の活動
    • 具体例はリスト 10-5 PrintPrimes.java の例を参照

Cohesion(凝集性)(pp.194-195)

  • クラスはインスタンス変数の数を少なくすべき
  • メソッドはそれらの変数を操作すべき
  • 凝集度は高い方が望ましい
  • 凝集度が低下したら、クラスを分割すべき
  • 【考察】凝集性とは
    • クラスの変数とメソッドがどれだけ密接に関連しているかを表す指標
    • 最も凝集性が高い状態:クラスの各メソッドが、すべてのインスタンス変数を使用する
  • 例:リスト10-4、高凝集なStackクラス

      public class Stack {
          private int topOfStack = 0;
          List<Integer> elements = new LinkedList<Integer>();
    
          public int size() { 
              return topOfStack; // topOfStack: インスタンス変数
          }
    
          public void push(int element) {
              topOfStack++;          // topOfStack
              elements.add(element); // elements: インスタンス変数
          }
    
          public int pop() throws PoppedWhenEmpty {
              if (topOfStack == 0) // topOfStack
                  throw new PoppedWhenEmpty();
              int element = elements.get(--topOfStack); // element, topOfStack
              elements.remove(topOfStack);              // topOfStack
              return element;
          }
      }
    

Maintaining Cohesion Results in Many Small Classes(凝集度に気を配ると、大量の小さなクラスが生まれる)(pp.195-202)

  • 大きな関数を小さく分割すると、新しいクラスを抽出する機会が生まれる
    • 例: リスト10-5 からリスト10-6, 10-7, 10-8 へ分割
  • 関数内の変数をインスタンス変数に昇格させることで、関数の分割が容易になる
  • しかし、それによってクラスの凝集度が低下する場合は、クラス自体を分割すべき
  • 関数の分割は、より良い抽象化とクラス構造の発見につながる

Organizing for Change(変更のために最適化する)(pp.202-205)

  • 多くのシステムは継続的に変更が発生する
  • クラスは拡張に対して開かれ、修正に対して閉じているべき(OCP: Open/Closed Principle)
  • 新機能は既存コードの修正ではなく、システムの拡張で実現すべき
  • クラスが2つ以上の理由で変更される必要がある場合、SRPに違反している
  • OCPに沿った例とそうでない例
    • 違反している例:リスト10-9、変更のたびにクラスを修正する必要がある

      public class Sql {
          public Sql(String table, Column[] columns)
          public String create()
          public String insert(Object[] fields)
          public String selectAll()
          // 略
      }
      
    • OCPに従っている例:リスト10-10、拡張で対応できる設計

      abstract public class Sql {
          public Sql(String table, Column[] columns)
          abstract public String generate();
      }
      
      public class CreateSql extends Sql {
          public CreateSql(String table, Column[] columns)
          @Override public String generate()
      }
      
      public class SelectSql extends Sql {
          public SelectSql(String table, Column[] columns)
          @Override public String generate()
      }
      
      public class UpdateSql extends Sql {
          public UpdateSql(String table, Column[] columns, Object[] fields)
          @Override public String generate()
      }
      

Isolating from Change(変更から切り離す)(pp.205-207)

  • 具象クラスへの依存は変更の影響を受けやすい
  • インターフェースと抽象クラスを使って影響を isolate すべき
  • 依存性逆転の原則(DIP: Dependency Inversion Principle):具象ではなく抽象に依存すべき
  • テスト容易性を高めるためにも、具象への依存を避けるべき
  • 結合度を最小限に抑えることで、システムの柔軟性と再利用性が向上する
  • 具体例

    • 具象に直接依存している悪い例:

      // TokyoStockExchangeに直接依存
      public class Portfolio {
          private TokyoStockExchange exchange;
          public Portfolio() {
              exchange = new TokyoStockExchange();
          }
          public Money value() {
              // exchangeの具体的な実装に依存
              return exchange.currentPrice();
          }
      }
      
    • DIPに従った良い例:

      // 抽象インターフェースを定義
      public interface StockExchange {
          Money currentPrice(String symbol);
      }
      
      // 具象クラスはインターフェースを実装
      public class TokyoStockExchange implements StockExchange {
          public Money currentPrice(String symbol) {...}
      }
      
      // 抽象に依存
      public class Portfolio {
          private StockExchange exchange;
          public Portfolio(StockExchange exchange) {
              this.exchange = exchange;
          }
          // ...
      }
      
  • 【補足】テストコードについて

    • Stub(スタブ)
      • 本番の実装を置き換えるためのテスト用の代替品
      • DIP に従って実装すると置き換えやすい
    • TokyoStockExchange が本番実装で、FixedStockExchange が Stub 実装
    • currentPrice メソッドの実装
      • TokyoStackExchange:実際の API を呼び出して株価を取得する(毎回値が変わる)
      • FixedStockExchangeStub:常に同じ固定値を返す(テストが安定する)
    • 例:

      // 本番実装:実際の株式取引所から価格を取得
      public class TokyoStockExchange implements StockExchange {
        public Money currentPrice(String symbol) {
            // 実際のAPIを呼び出して動的な株価を取得
            return realTimePrice(); // 毎回値が変わる
        }
      }
      
      // Stub実装:テスト用の固定値を返す
      public class FixedStockExchangeStub implements StockExchange {
        private Map<String, Integer> prices = new HashMap<>();
      
        // テストケースで価格を設定できる
        public void fix(String symbol, int price) {
            prices.put(symbol, price);
        }
      
        public Money currentPrice(String symbol) {
            // 設定された固定値を返すだけ
            return new Money(prices.get(symbol));
        }
      }
      

考察

  • クリーンコードにおけるクラス設計は単にコードを整理することではなく、以下を同時に表現することである
    1. 責務の明確な分離
    2. 変更の影響範囲の最小化
    3. テスト容易の確保
  • クリーンなクラス設計は投資である
    • シンプルさやクリーンさと拡張性はトレードオフの関係にある
    • 著者はクリーン化への投資を怠る傾向があることを指摘している
      • 「問題は、多くの人がプログラムが動作するようになった時点で完了としてしまうことです。」(p.194)
    • 著者は投資の目的を以下のように述べている
      • 「……こうした複雑さに対処するのに必要なのは、開発者がいつでも直接理解しなければならない箇所を容易に探し出せるように、全体を構成するということです。」(同頁、太字は原文通り)
  • すぐに得られる見返りはあるか
    • レビュワーがすぐにコードの意図を理解でき、より的確にフィードバックできる
    • 問題発生時に該当箇所を特定しやすく、テスト失敗の原因を特定しやすい
    • コードについての会話がより具体的になる
    • 自分のコードを見返した時の理解が容易
  • デメリットやリスクはあるか
    • 開発速度が一時的に低下する
    • 過剰な抽象化につながる可能性がある
    • ファイル数が増え、構造が複雑化する
    • チーム内で理解度や意欲の差が生まれる
    • 「完璧なコード」を求めすぎる
  • 対策
    • 完璧を求めすぎない
    • 段階的に改善する
    • 実際に変更が必要になった時にリファクタリングを検討する

Follow me on X: @suneo3476Web