12
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【感想】良いコード/悪いコードで学ぶ設計入門

Posted at

はじめに

著書「良いコード/悪いコードで学ぶ設計入門」を読んでの感想をまとめました。
本書から抜粋して特に「どの言語にも通用する普遍的な考え方だな」と感じたことや、「これは筆者が一番伝えたいポイントだろうな」と個人的に解釈した点に焦点を当て、読後の感想をまとめています。

あくまで個人の感想・まとめであり、内容の正確性を保証するものではありません。

3章 クラス設計 - すべてにつながる設計の基盤 -

クラス単体で正常に動作するよう設計する

良いクラスの構成要素

良いクラスは、主に以下の要素で構成されます。

  • インスタンス変数
  • インスタンスを不正状態から防御し、正常に操作するメソッド
GoodClass
field: type
method(): type

このように、データと処理が1箇所にまとまっていることを高凝集と言います。
クラス内部の要素(インスタンス変数やメソッド)が、互いに強く関連し合っていて、一つのまとまった目的や役割のために協力し合っている状態」を指します。

良くないクラスの構成要素

BadClass_A
field: type
BadClass_B
method(): type

インスタンス変数とメソッドのどちらかが欠けても良くない(ただし、例外もあり)
このようにデータと処理がバラバラに散らばっている状態を低凝集と言います。
この状態ではインスタンス変数に不正な値が混入するなどの問題が生じやすくなります。

第6章 条件分岐 -迷宮化した分岐処理を解きほぐす技法 -

switch文の重複


// ユーザーのタイプを表す enum
enum UserType {
    NORMAL, PREMIUM, GUEST
}

// ユーザー情報を持つクラス(簡略化)
class User {
    private UserType type;
    // ... 他のフィールド ...

    public User(UserType type) {
        this.type = type;
    }

    public UserType getType() {
        return type;
    }
    // ...
}

// --- 処理 1: 課金計算を行うサービス ---
class BillingService {
    public int calculateCharge(User user) {
        // ユーザータイプに応じた課金計算ロジック
        switch (user.getType()) {
            case NORMAL:
                return 1000; // 通常ユーザー料金
            case PREMIUM:
                return 2000; // プレミアムユーザー料金
            case GUEST:
                return 0;    // ゲストユーザー料金(無料)
            default:
                throw new IllegalArgumentException("未知のユーザータイプです: " + user.getType());
        }
    }
}

// --- 処理 2: ユーザーへの通知メッセージを生成するサービス ---
class NotificationService {
    public String getWelcomeMessage(User user) {
        // ユーザータイプに応じたメッセージ生成ロジック
        switch (user.getType()) { // UserType に対して重複)
            case NORMAL:
                return user.getType() + "さん、ようこそ!";
            case PREMIUM:
                return user.getType() + "会員様、特別な特典があります!";
            case GUEST:
                return "ゲスト様、ログインして特典をゲット!";
            default:
                throw new IllegalArgumentException("未知のユーザータイプです: " + user.getType());
        }
    }
}

public class Main {
    public static void main(String[] args) {
        User normalUser = new User(UserType.NORMAL);
        User premiumUser = new User(UserType.PREMIUM);

        BillingService billingService = new BillingService();
        System.out.println("通常ユーザーの料金: " + billingService.calculateCharge(normalUser)); // 1000
        System.out.println("プレミアムユーザーの料金: " + billingService.calculateCharge(premiumUser)); // 2000

        NotificationService notificationService = new NotificationService();
        System.out.println("通常ユーザーへのメッセージ: " + notificationService.getWelcomeMessage(normalUser)); // NORMALさん、ようこそ!
        System.out.println("プレミアムユーザーへのメッセージ: " + notificationService.getWelcomeMessage(premiumUser)); // PREMIUM会員様、特別な特典があります!
    }
}

問題点

  • UserType enumに新しいタイプ(例: ADMIN)を追加した場合、BillingServiceとNotificationServiceに存在する全てのswitch文の該当箇所を修正する必要がある
  • switch文が多くの場所に散らばっていると、修正漏れが発生しやすくなり、バグの原因となる
  • UserType に応じた「振る舞い」が Userクラス自身から離れ、利用する側の各所に分散してしまっている状態と言える

解決策

インターフェース(Interface)を使い、「型による条件分岐を、Interface を通した振る舞いの違い(ポリモーフィズム)に置き換える

インターフェース(Interface)とは?

インターフェースは、そのメソッドの具体的な処理は書かずに「変数」「メソッドの型」を記述したものです。

(例)インターフェースは「リモコンのボタンの設計」のようなものです。
電源ボタンを押したら、テレビがオンオフする」、「チャンネルボタンを押したら、チャンネルが変わる」といった操作(メソッド)は定義します。
しかしそれが、どのメーカーのリモコンなのか、内部でどのように信号を送るのかといった具体的な実装はインターフェース自体には含まれません。テレビ側は、そのリモコンインターフェースの契約を満たすリモコンであれば、メーカーを問わず受け付けることができます。
istockphoto-1475402623-612x612.jpg

参考:【図解で理解】Javaのinterfaceのメリットと使い方について

ポリモーフィズムとは?

ポリモーフィズムとは、「多様性」や「多態性」という意味で、オブジェクト指向において、同じ名前のメソッドが異なるクラスで異なる振る舞いをすることです。

参考:プログラマー1年生がポリモーフィズムについて学んだのでRPGで説明する。


// --- ユーザーの種類ごとの「振る舞い」を定義するインターフェース ---

// 課金計算ができる、という役割を定義
interface Chargeable {
    int calculateCharge(); // このメソッドを持つものは課金計算ができる
}

// 歓迎メッセージを提供できる、という役割を定義
interface WelcomeMessageProvider {
    String getWelcomeMessage(); // このメソッドを持つものは歓迎メッセージを生成できる
}

// --- ユーザーの種類ごとの具体的なクラス ---
// それぞれが上記の「振る舞い」インターフェースを実装する

// 通常ユーザー
class NormalUser implements Chargeable, WelcomeMessageProvider {

    private String name;

    public NormalUser(String name) {
        this.name = name;
    }

    // Chargeable インターフェースの実装
    @Override
    public int calculateCharge() {
        return 1000; // 通常ユーザーの計算ロジック
    }

    // WelcomeMessageProvider インターフェースの実装
    @Override
    public String getWelcomeMessage() {
        return name + "さん、ようこそ!"; // 通常ユーザーのメッセージ生成ロジック
    }
}

// プレミアムユーザー
class PremiumUser implements Chargeable, WelcomeMessageProvider {
    private String name;

    public PremiumUser(String name) {
        this.name = name;
    }

    // Chargeableインターフェースの実装
    @Override
    public int calculateCharge() {
        return 2000; // プレミアムユーザーの計算ロジック
    }

    // WelcomeMessageProviderインターフェースの実装
    @Override
    public String getWelcomeMessage() {
        return name + "会員様、特別な特典があります!"; // プレミアムユーザーのメッセージ生成ロジック
    }
}

// ゲストユーザー
class GuestUser implements Chargeable, WelcomeMessageProvider {
     // ゲストユーザーは名前がないかもしれない
     public GuestUser() {
         // ...
     }

    // Chargeableインターフェースの実装
    @Override
    public int calculateCharge() {
        return 0; // ゲストユーザーの計算ロジック
    }

    // WelcomeMessageProviderインターフェースの実装
    @Override
    public String getWelcomeMessage() {
        return "ゲスト様、ログインして特典をゲット!"; // ゲストユーザーのメッセージ生成ロジック
    }
}

// --- サービス側のコード ---
// サービスは具体的なユーザータイプを知る必要がなくなり、インターフェースに依存する

class BillingService {
    public int processBilling(Chargeable chargeableUser) {
        System.out.println("課金処理を実行します...");
        // Chargeable インターフェースのメソッドを呼び出すだけ
        return chargeableUser.calculateCharge();
    }
}

class NotificationService {
    public String sendWelcomeNotification(WelcomeMessageProvider messageProviderUser) {
        System.out.println("メッセージを送信します...");
        // WelcomeMessageProvider インターフェースのメソッドを呼び出すだけ
        return messageProviderUser.getWelcomeMessage();
    }
}


public class Main {
    public static void main(String[] args) {
        // それぞれの具体的なユーザータイプのインスタンスを作成
        Chargeable normalChargeable = new NormalUser("山田");
        WelcomeMessageProvider normalMessageProvider = new NormalUser("山田");

        Chargeable premiumChargeable = new PremiumUser("佐藤");
        WelcomeMessageProvider premiumMessageProvider = new PremiumUser("佐藤");

        Chargeable guestChargeable = new GuestUser();
        WelcomeMessageProvider guestMessageProvider = new GuestUser();


        BillingService billingService = new BillingService();
        // BillingService は Chargeable なオブジェクトなら何でも受け取れる
        System.out.println("山田さんの料金: " + billingService.processBilling(normalChargeable)); // NormalUser の calculateCharge() が呼ばれる
        System.out.println("佐藤さんの料金: " + billingService.processBilling(premiumChargeable)); // PremiumUser の calculateCharge() が呼ばれる
        System.out.println("ゲストさんの料金: " + billingService.processBilling(guestChargeable)); // GuestUser の calculateCharge() が呼ばれる


        NotificationService notificationService = new NotificationService();
        // NotificationService は WelcomeMessageProvider なオブジェクトなら何でも受け取れる
        System.out.println("山田さんへのメッセージ: " + notificationService.sendWelcomeNotification(normalMessageProvider)); // NormalUser の getWelcomeMessage() が呼ばれる
        System.out.println("佐藤さんへのメッセージ: " + notificationService.sendWelcomeNotification(premiumMessageProvider)); // PremiumUser の getWelcomeMessage() が呼ばれる
        System.out.println("ゲストさんへのメッセージ: " + notificationService.sendWelcomeNotification(guestMessageProvider)); // GuestUser の getWelcomeMessage() が呼ばれる

    }
}

改善したこと

  1. 「振る舞い」の抽象化:
    「〜ができる」という「役割」を決めました。(例:「計算できる人」「あいさつできる人」という役割)

  2. 「振る舞い」の実装を各具象クラスに委譲:
    普通の人、プレミアムな人、ゲストの人、それぞれの「人」が、その「役割」に従って、自分がどう計算するか、自分がどうあいさつするか、というやり方を自分自身の中に持つようにしました。

  3. サービス側のInterfaceへの依存:
    計算サービスやあいさつサービスは、「計算できる人」や「あいさつできる人」という「役割」を持つ人なら誰でも受け取るようにしました。どんな種類の人か(普通の人かプレミアムな人か)は、サービス側で意識する必要はありません。
    だから、種類ごとに処理を分ける(switch文)必要がなくなりました。

  4. ポリモーフィズムの活用:
    「計算して!」とお願いすると、お願いされた人(お願いされた役割を持つ人)それぞれのやり方で計算してくれます。これがポリモーフィズムです。サービス側のコードは同じですが、お願いされた人によって結果(計算方法)が変わります。

第8章 密結合 - 絡まって解きほぐせない構造 -

密結合

密結合とは、「異なるクラス同士が、互いに強く依存し合っている構造」を指します。
いろんなクラスが依存しあっていて責務がないので、デバッグや変更が難しい構造になります。

例:電球とソケット/コンセント

  • 家電(電球): 何か機能を持つもの(ロジックや振る舞い)
  • コンセントやソケット: 家電が動作するために必要な「供給源」や「接続口」
LightBulb(電球)
Light(光る)
ConnectSocket(ソケットに接続)

SpecificSocket(特定の規格のソケット)
SendElectric(電気を提供する)

※上記は独自に作成した例です

この場合は、電気を「特定の規格のソケット」からしか受け取ることができません。
もし規格が違っていた場合、電気が光らないので、特定のソケットに依存しているということになります。

単一責任の原則

単一責任の原則とは、「一つのクラスやモジュールが持つべき責任は、一つにすべきだ」という原則です。

疎結合

密結合とは逆の構造であり、関心事それぞれが分離、独立している構造のことです。

例:電球とソケット/コンセント
ソケットは規格に合った差し込み口を持っており、電球はその規格に合ったネジや口金を持っています。

ElectricitySource(電源コンセント)
SendElectric(電気を提供する)

StandardSocket(電球ソケット)
SendElectric(電気を提供する)

LightBulb(電球)
Light(光る)
RecieveElectric(電気を受け取る)

※上記は独自に作成した例です

この場合は電球が特定のソケットから切り離されており、電気供給という役割にのみ依存しています。
疎結合にすることで、依存される側(ソケットや電気供給源)の実装を自由に変更したり、別の実装に差し替えたりしやすくなり、コード全体の柔軟性や拡張性が高まります。
この疎結合の考え方がインターフェースや抽象クラスの仕組みにつながっているとわかりました。

第10章 名前設計 - あるべき構造を見破る名前 -

関心の分離

関心の分離とは、「異なる関心事(例えば、ユーザー認証、商品の注文処理、データの永続化など)を、コードの異なる部分(クラス、モジュール、パッケージなど)で扱うように分けよう」という考え方です。

例えば、「ユーザー認証」という関心事、「ユーザーのプロフィール管理」という関心事、「ユーザーの課金処理」という関心事、といった具合です。

つまり、「関心事、ユースケース、目的や役割ごとに分離する」という考え方です。

第13章 モデリング - クラス設計の土台 -

単一目的の原則

単一目的の原則とは、クラスやモジュールは、その名前や役割から期待される「ただ一つの目的」に集中すべきであるという考え方です。例えば、Userクラスは「ユーザーという概念そのもの」を表現することに集中し、ユーザーの認証処理はAuthenticationService、権限管理はAuthorizationService、課金処理はBillingServiceといったように、異なる目的に関する処理は別のクラスやモジュールに分離します。

(命名は例です)

第14章 リファクタリング - 既存コードを成長に導く技 -

ユニットテストでリファクタリングのミスを防ぐ

リファクタリングはコードの内部構造を変更するため、「意図せず元の機能を壊してしまった」というリスクが常に伴います。このリスクを最小限に抑え、安全にリファクタリングを行うためにユニットテストを実施するということが改めてわかりました。

テストコードを用いたリファクタリングの流れ

  1. あるべき構造のひな型クラスを作る
  2. ひな型クラスに対してテストコードを書く
  3. テストを失敗させる
  4. テストを成功させるための最低限のコードを書く
  5. ひな型クラス内部でリファクタリング対象のコードを呼び出す
  6. テストが成功するよう、あるべき構造へロジックを少しずつリファクタしていく

第15章 設計の意義と設計への向き合い方

国家規模の経済損失

開発チームには20人いて、レガシーコードによる実装遅延が1人1日3時間発生していると仮定します。単純計算で、開発チーム全体で1日あたり3×20=60時間損失が発生していることになりますね。これが1ヶ月ともなると実働日数20日として1200時間、1年間となると14400時間も損失が生じます。低生産による損失は、少しずつ確実に蓄積していきます。

リファクタリングとは「綺麗なコードを書く」という表面的な話に留まらず、チームの生産性やビジネスに影響を及ぼす重要な作業であることを改めて認識されられました。

第16章 設計を妨げる開発プロセスとの戦い

経緯と礼儀

コードレビューってなると技術的な正しさを追求しがちなのですが、攻撃的なコメントはどんなに正しい内容でも許されないと本書では述べています。
コードレビューで一番重要なのは、レビューを受ける側への敬意を第一に意識することだといいます。

以下の指針は抜粋です。

すべきこと 解説
理由 なぜ間違っているか、どういう変更が正しいかを説明します。「間違っています」だけでは相手に伝わりません。
理由を聞く 相手の意図が不明瞭なときは、遠慮せず変更理由を聞きます。
適度な時間内に返信する 24時間以内に返信できなければ、いつまでに返信できるかをコメントで残すなど、適切に対応します
ポジティブに述べる 「すべての欠点を見つける」という気持ちではなく、難しい仕事を引き受けてくれたり、良い変更をしてくれた人に感謝する姿勢でレビューすること。
すべきでないこと 解説
人を辱めない 相手は最善を尽くしている前提だと、「なぜ気づかなかった?」などのコメントは無意味です。
極端な言葉やネガティブ表現を使わない 「まともな人ならこうはしない」などのネガティブ表現をレビュー時に使用しない。人ではなく、コードについて議論すること。
"自転車置き場"の議論をしない どちらでもいいようなことについて、レビューで決着をつけようとしないこと。レビューの目的は「勝ち負け」ではない。

悪い例

ここで〇〇メソッドは使わないでください。パフォーマンスが悪いです。こういう実装は何もいいことがありません。

良い例

動作しますし、十分良い変更です。ただ、パフォーマンスをもう少し改善したいです。〇〇メソッドでも実装できますが、××メソッドの方が実行速度で有利です。

おわりに

本書を通して、コード設計の基礎となる様々な原則やパターン、そして「良いコード」と「悪いコード」は何か?について、具体的な例を交えながら学ぶことができました。これらの考え方は、どの言語やプロジェクトにおいても共通して適用できる、普遍的な「設計の思考法」だと感じました。

参考記事

12
4
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
12
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?