📚

TypeScriptを使って学ぶSOLID原則1 ”単一責任の原則(Single Responsibility Principle)”

2025/03/10に公開

モチベーション

ソフトウェアを設計する際に重要な5つのガイドラインであるSOLID原則について学んでいます。

その一つである単一責任の原則(Single Responsibility Principle) について学んだので、アウトプットの一環で記事を執筆しました。

単一責任の原則(Single Responsibility Principle)とは

単一責任の原則とは下記のことを表します。

A class should have only one reason to change.
出典:https://www.geeksforgeeks.org/solid-principle-in-programming-understand-with-real-life-examples/

全てのモジュールはたった一つのアクターに対しての責務を負うべきである。
※責務:変更する理由(reason to change)
※アクター(概念ともいう):モジュールを使用するユーザーやステークホルダーのこと

例えば

レストランではそれぞれのスタッフの役割が以下のように分けられています。

  • シェフ
  • ウェイター
  • ディッシュウォッシャー

シェフは料理を作り、ウェイターはオーダーをとり、ディッシュウォッシャーは皿洗いをします。それぞれのスタッフがそれぞれの役割をこなすことでレストランはより良い料理やサービスを提供することができます。

もしシェフが慣れないオーダーを取ったり、ウェイターが皿を洗ったりするとミスにつながりサービス低下につながります。

実装例

冒頭のレストランのスタッフの例を実装しました。

守られていない実装例

class Employee {
    constructor(
        public name: string, 
        public id: number,
    ) {}

    // シェフがアクター
    cookMeal() {
        this.cookMeal();
        console.log(`${this.name}を調理しました。`);
    }

    // ウェイターがアクター
    serveDish() {
        this.getRegularHours();
        console.log(`${this.name}が料理をサーブしました。`);
    }

    // ディッシュウォッシャーがアクター
    doDishes() {
        console.log(`${this.name}が皿を洗いました。`);
    }
}

この例ではEmployeeクラスが調理する、サーブする、皿を洗うの3つの役割を持っています。このクラスに変更を加える場合、以下の理由が挙げられます。

  • 調理方法が変わった場合:i.e. 使用する調理器具が変わった
  • 料理の提供方法が変わった場合:i.e. 提供する料理の説明が変わった
  • 皿洗いの方法が変わった:i.e. 手洗いからディッシュウォッシャーを利用する方法へ変更

よってこれは単一責任の原則に反していることになります。

守られている実装例

class EmployeeInfo {
    constructor(
        public name: string, 
        public id: number,
    ) {}
}

class Chef{
    constructor(private employee:EmployeeInfo){}
    cookMeal(){
        console.log(`${this.name}を調理しました。`);
    }
}

class Waiter{
    constructor(private employee:EmployeeInfo){}
    serveDish(){
        console.log(`${this.name}が料理をサーブしました。`);
    }
}

class DishWasher{
    constructor(private employee:EmployeeInfo){}
    doDishes(){
        console.log(`${this.name}が皿を洗いました。`);
    }
}
    

EmloyeeクラスをそれぞれChef, Waiter, DishWasherクラスに分割することにより、それぞれのクラスが1つの役割を持つようになりました。

これによりそれぞれのクラスの変更理由は1つになったので、単一責任の原則を守れていることになります。

原則に違反した場合はどうなるか

単一責任の原則に違反した場合、以下のような影響が出ます。

コードの保守性が低下し、バグを生みやすくなります。さらに変更容易性も損なわれるので新機能の開発速度にも影響を及ぼします。

  1. あるアクターのために行った変更の影響が関係のない別のアクターにも及んでしまう
  2. ほんの少しの変更でも影響を受ける全てのモジュールを調査しなければならず、余分な工数がかかる

原則を守った場合のメリット

  1. 変更範囲を最小限に留めることができ、その変更の影響が他の箇所に及ばないことを担保しているためテストをする際も工数がかからない
  2. 変更範囲が明確になるため、どのコードを変更しなければならないのかをすぐにわかる
  3. コードの可読性が上がる
  4. 拡張性や保守性が上がる

どうすれば違反しない設計にできるか

モジュールは機能ごとに小さく作る

モジュールを実装する際には機能ごとに、最小単位レベルまで分割することでそのモジュールの責務をシンプルなものにすることができます。そうすることで「モジュールAは役割Aをこなす」といった明確な責務を持たせることができ、コードの拡張性が上がります。

この小さな機能群を組み合わせることで1つの大きなシステムを作ることができます。

アクター(概念)が異なるものは分ける

たとえ同じロジックであってもアクター(概念)が異なるのであれば、別のモジュールに切り出すことが重要です。

よく混同されやすいのですがこの原則は「モジュールは1つのことを行うべき」という原則ではないということです。

DRY原則(Don't Repeat Yourself)に背くことになりますが、アクター(概念)が違うものはDRYに従うべきでないです。あくまでも偶然、同じ規則性があったということです。見た目やロジックが似ているからと言って、まとめてしまうとコード上での結びつきが強くなり単一責任の原則に背くことになります。

この場合はWET原則(Write Everything Twice)に則りアクター(概念)ごとにロジックを実装すべきです。

まとめ

SOLID原則の一つである単一責任の原則について、初心者なりにまとめました。

初めから完璧な設計をすることは難しいと思います。当初は同じ概念だと捉えていても仕様変更のタイミングで異なる概念であったと判明することもあります。それが判明した時点で分割することにより、修正範囲が少ない状態でより良い設計に変更することが可能です。

ビジネスへの理解度が深まれば深まるほど、単一責任の原則を満たすことができ、より良い設計に近づけることができると思います。

Discussion