Aikido

クラスが単一責任の原則に従うべき理由

読みやすさ

ルール
クラス すべき 持つ 単一の 責任を持つべきである。
クラスは 処理 複数の 関心 違反する
 単一 責任 原則。

サポート言語: JS、 TS、 PY、 JAVA、 C/C++、
C#、 Swift/Objective C、 Ruby. PHP、 Kotlin、 
Scala、 Rust、 Haskell、 Groovy、 Dart。 Julia,
Elixit、 クロジュール、 OCaml、 デルファイ

はじめに

機能が多すぎるクラスはボトルネックとなる。認証、メール送信、バリデーションを扱うクラスは、いずれかの領域が進化するたびに変更が必要となり、無関係な機能の破損リスクを生む。テストでは、一部をテストする場合でもクラス全体をモックする必要がある。単一責任の原則は、クラスが変更される理由は一つだけであるべきと定めている。

なぜそれが重要なのか

コードの保守性:複数の責任を持つクラスは、いずれかの関心の進化がクラス全体に影響するため、より頻繁に変更される。

テストの複雑性:複数の責任を持つクラスのテストでは、たとえ1つの機能をテストする場合でも、すべての依存関係をモック化する必要がある。

再利用性:依存関係をすべて伴わずに単一の責任を抽出することはできない。開発者は複数の責任を持つクラスを整理するよりも、コードを複製する。

チーム連携:複数の開発者が異なる機能のために同じクラスを同時に開発すると、頻繁にマージ競合が発生する。単一責任のクラスは競合なく並行開発を可能にする。

コード例

非準拠:

class UserManager {
    async createUser(userData) {
        const user = await db.users.insert(userData);
        await this.sendWelcomeEmail(user.email);
        await this.logEvent('user_created', user.id);
        await cache.set(`user:${user.id}`, user);
        return user;
    }

    async sendWelcomeEmail(email) {
        const template = this.loadEmailTemplate('welcome');
        await emailService.send(email, template);
    }

    async logEvent(event, userId) {
        await analytics.track(event, { userId, timestamp: Date.now() });
    }
}

問題点:このクラスはデータベース操作、メール送信、ロギング、キャッシュ処理を扱っている。メールテンプレート、ロギング形式、キャッシュ戦略の変更はすべてこのクラスの修正を必要とする。ユーザー作成のテストにはメールサービス、アナリティクス、キャッシュのモックが必要となり、テストが遅く脆弱になる。

✅ 準拠:

class UserRepository {
    async create(userData) {
        return await db.users.insert(userData);
    }
}

class EmailNotificationService {
    async sendWelcomeEmail(email) {
        const template = await this.templateLoader.load('welcome');
        return await this.emailSender.send(email, template);
    }
}

class UserEventLogger {
    async logCreation(userId) {
        return await this.analytics.track('user_created', {
            userId,
            timestamp: Date.now()
        });
    }
}

class UserService {
    constructor(repository, emailService, eventLogger, cache) {
        this.repository = repository;
        this.emailService = emailService;
        this.eventLogger = eventLogger;
        this.cache = cache;
    }

    async createUser(userData) {
        const user = await this.repository.create(userData);
        await Promise.all([
            this.emailService.sendWelcomeEmail(user.email),
            this.eventLogger.logCreation(user.id),
            this.cache.set(`user:${user.id}`, user)
        ]);
        return user;
    }
}

なぜこれが重要なのか: 各クラスには明確な責任が一つずつ割り当てられています:データの永続化、メール送信、イベント記録、またはオーケストレーションです。メールテンプレートの変更は メール通知サービスユーザー作成のテストでは、依存関係に対して単純なスタブを使用できます。クラスは異なる機能間で独立して再利用可能です。

結論

単一責任の原則は、クラスを可能な限り小さくすることではなく、各クラスが変更される明確な理由を一つだけ持つことを保証することです。クラスが複数の関心事を処理し始めたら、それぞれの責任を焦点を絞ったインターフェースを持つ独自のクラスに抽出することでリファクタリングします。これにより、無関係な機能間に変更が波及することなく、コードのテスト、保守、進化が容易になります。

よくある質問

ご質問は?

クラスが責任を過剰に担っている場合、どのように見分ければよいですか?

変更の理由が複数あるクラスを探してください。メール処理ロジック、ログ形式、データベーススキーマの変更がすべて同じクラスを修正する必要がある場合、そのクラスは責任が多すぎます。メソッド名を確認しましょう:sendEmail()、logEvent()、validateData()のように無関係な動詞を同じクラスで扱っている場合、それは危険信号です。300~400行を超えるクラスは複数の責任を示すことが多いですが、サイズだけでは決定的ではありません。

クラスを分割すると、ファイル数が増え複雑さが増すのでは?

ファイル数が多いからといって複雑さが増すわけではない。50行ずつの10個の特化したクラスは、全てを扱う500行の単一クラスよりも理解しやすい。重要なのは各クラスが単純で明確な目的を持つことだ。現代のIDEにおけるナビゲーションではファイル数は無関係となる。複雑さの削減は、無関係な要素を考慮せずに各クラスを独立して考察できることに由来する。

複数の操作を自然に調整する必要があるクラスについてはどうでしょうか?

調整そのものが責任である。UserServiceクラスは、UserRepository、EmailService、EventLoggerへの呼び出しを、それらを自ら実装することなく調整できる。これがオーケストレーターまたはファサードパターンである。違いは、オーケストレーターが複数の関心を直接実装するのではなく、専門化されたクラスに委譲する点にある。これは薄い接着剤コードであり、ビジネスロジックではない。

この原則は、静的メソッドを持つユーティリティクラスにどのように適用されますか?

ユーティリティクラスは、関連性のない静的メソッドを次々と追加しがちであるため、単一責任原則を特に破りやすい。StringUtilsクラスはフォーマット補助機能から始まり、検証、解析、暗号化、エンコーディング機能まで拡大する可能性がある。これらをStringFormatter、StringValidator、StringEncoderといった特化したユーティリティクラスに分割すべきである。各クラスは関連する操作を一貫性を持ってまとめたものとなる。

この原則に反している既存のクラスをリファクタリングするにはどうすればよいですか?

まずクラス内の明確な責任を特定する。最も単純なものを最初に抽出して新規クラス化し、テストを更新し、動作を確認する。大規模なリファクタリングを試みるのではなく、反復的に繰り返す。絞め殺しの木パターンを活用する:単一責任の新規クラスを作成し、旧クラスから段階的にコードを移行する。旧クラスが空または最小限になったら非推奨とする。各ステップは動作しテスト可能な増分であるべきだ。

単一責任とは単一メソッドを意味するのか?

いいえ。クラスは、それらがすべて同じ責任に関連している限り、複数のメソッドを持つことができます。UserRepositoryクラスは、create()、update()、delete()、findById()メソッドを持つ可能性があります。これらはすべて、ユーザーデータの永続化という単一の責任を果たすためです。これらのメソッドは、同じ関心事の凝集性のあるバリエーションであり、別々の関心をまとめてパッケージ化したものではありません。

今すぐ安全を確保しましょう

コード、クラウド、ランタイムを1つの中央システムでセキュアに。
脆弱性を迅速に発見し、自動的に修正。

クレジットカードは不要。