Solid原則シリーズ 単一責任の原則 ~神クラスを避けよう~

開発ログ

はじめに

「なんかここのテスト書くの難しいな。」、「なんかコード読みにくい、、、。」など、違和感を感じるものの言語化できないみたいなことありませんか?
その違和感はsolid原則を知ると解消されるかもしれません。

僕も最初は「なんか読みにくいな」などと違和感を感じることはありつつも、それが何かがわからずどのように修正すればいいかわかりませんでした。
ただ、solld原則について知った時は「ああ、だから違和感あったのか!」とつながりました

読んでいただいた皆さんにもぜひ同じ感覚を味わって欲しく、何回かに分けて紹介できればと思ってます。今回はまず「単一責任の原則」についてお話します。

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

単一責任の原則はクラスやモジュール一つに対して基本的に一つの役割・責任を持った方がいいよという原則です。

会社の部署を例にしてみましょう。
営業部、経理部、開発部、人事部などそれぞれで与えられる役割がありますよね。
それがもし、人事部なのに経理もやって営業もやって開発もやって、、、という状態ならどうなるでしょうか。
立ち上げ段階の時は回りはすると思いますが会社が大きくなってきたら疲弊する未来が見えますよね。

それがコードの世界でも言えます。
複数の役割を持つクラスは、大きくなればなるほどテストがしにくくなり、コードも読みにくくなります。
挙げ句の果てには「1か所触ってしまったらバグが起きる」ような爆弾のようなクラスができることだってあります。それでは開発者も幸せになれません、、、。

では具体的にどういうことなのか。コードを用いて以下で紹介します。

単一責任を守らないとどうなるか

class FitnessGym {
  private members: { name: string; hasPaid: boolean }[] = [];
  private machines: { id: string; type: string; lastMaintenanceDate: Date }[] =
    [];

  constructor(
    public gymName: string,
    private monthlyFee: number,
    private currentTaxRate: number,
  ) {}

  // 1. 【会員管理】
  registerMember(name: string): void {
    this.members.push({ name, hasPaid: false });
    console.log(`${name}様、入会ありがとうございます!`);
  }

  // 2. 【会計処理】
  calculateTotalCharge(): number {
    const subtotal = this.members.length * this.monthlyFee;
    return Math.floor(subtotal * (1 + this.currentTaxRate));
  }

  // 3. 【筋トレ機材】
  getMachineStatus(machineId: string): string {
    const machine = this.machines.find((m) => m.id === machineId);
    if (!machine) return "機材が見つかりません";
    return machine
  }

  // 4. 【トレーニング指導】
  getWorkoutAdvice(targetMuscle: string): string {
    if (targetMuscle === "胸") {
      return "ベンチプレスを3セット、各10回行ってください。";
    }
    if (targetMuscle === "背中") {
      return "ラットプルダウンを3セット、各12回行ってください。";
    }
    if (targetMuscle === "脚") {
      return "スクワットを3セット、各15回行ってください。";
    }
    if (targetMuscle === "肩") {
      return "ショルダープレスを3セット、各10回行ってください。";
    }
    return "ストレッチから始めましょう。";
  }

  // 5. 【清掃管理】
  getCleaningChecklist(): string[] {
    return ["床のモップ掛け", "マシンのアルコール消毒", "タオルの補充"];
  }
}

このFitnessGymクラスには、全部で5つの責任があります。 正直、フィットネスジムという文脈でこれらの機能がまとまっていること自体は、不自然ではないかもしれません。
ですが、開発の現場では問題が起きます。

消費税が変わったとき、新しいトレーニングメニューが増えたとき、会員情報の項目を追加したいとき。修正するのは、いつもこの同じクラスになります。

そうなると、「トレーニング回数を変えたはずが、うっかり消費税の計算ロジックを消してしまった」といった、意図しない場所でのバグが起きるリスクが出てきます。
また、一つのクラスが巨大なので、テストをする際の影響範囲も広くなってしまいます。

例のコードはまだ短いですが、システムが成長するにつれて、このクラスは何万行にも膨れ上がる可能性があります。 ある場所を直すと別の場所が壊れる。だから怖くて触れない。はい、**「神クラス(God Class)」**の完成です。

もう後戻りできない、、、。そんな状況にならないよう、普段から「このクラスの責任は一つになっているか?」を意識することが大切です。

単一責任を守ったらどうなるか

「じゃあ具体的にどうすればいいんだ」と感じる人のために、先ほどのコードを単一責任の原則に則って分割してみます。
FitnessGymが持っていた役割を以下の5つのクラスに分けていきます。

会員管理のクラス

class MemberManager {
  private members: { name: string; hasPaid: boolean }[] = [];

  registerMember(name: string): void {
    this.members.push({ name, hasPaid: false });
    console.log(`${name}様、入会ありがとうございます!`);
  }

  getMembers() {
    return this.members;
  }
}

会計処理のクラス

class BillingService {
  constructor(
    private monthlyFee: number,
    private currentTaxRate: number,
  ) {}

  calculateTotalCharge(members: { name: string; hasPaid: boolean }[]): number {
    const subtotal = members.length * this.monthlyFee;
    return Math.floor(subtotal * (1 + this.currentTaxRate));
  }
}

筋トレ機材のクラス

class MachineManager {
  private machines: { id: string; type: string; lastMaintenanceDate: Date }[] = [];

  getMachineStatus(machineId: string): string {
    const machine = this.machines.find((m) => m.id === machineId);
    if (!machine) return "機材が見つかりません";
    return machine.type;
  }
}

トレーニング指導のクラス

class WorkoutAdvisor {
  getWorkoutAdvice(targetMuscle: string): string {
    if (targetMuscle === "胸") {
      return "ベンチプレスを3セット、各10回行ってください。";
    }
    if (targetMuscle === "背中") {
      return "ラットプルダウンを3セット、各12回行ってください。";
    }
    if (targetMuscle === "脚") {
      return "スクワットを3セット、各15回行ってください。";
    }
    if (targetMuscle === "肩") {
      return "ショルダープレスを3セット、各10回行ってください。";
    }
    return "ストレッチから始めましょう。";
  }
}

清掃管理のクラス

class CleaningManager {
  getCleaningChecklist(): string[] {
    return ["床のモップ掛け", "マシンのアルコール消毒", "タオルの補充"];
  }
}

上記を統合した大本のFitnessクラス

最後に上記のすべてのクラスを組み合わせてフィットネスジム全体の窓口としての役割を果たすコードです。

class FitnessGym {
  private memberManager: MemberManager;
  private billingService: BillingService;
  private machineManager: MachineManager;
  private workoutAdvisor: WorkoutAdvisor;
  private cleaningManager: CleaningManager;

  constructor(
    public gymName: string,
    monthlyFee: number,
    currentTaxRate: number,
  ) {
    this.memberManager = new MemberManager();
    this.billingService = new BillingService(monthlyFee, currentTaxRate);
    this.machineManager = new MachineManager();
    this.workoutAdvisor = new WorkoutAdvisor();
    this.cleaningManager = new CleaningManager();
  }

  // ---- 各クラスへの処理の委譲(パススルー) ----

  registerMember(name: string): void {
    this.memberManager.registerMember(name);
  }

  calculateTotalCharge(): number {
    return this.billingService.calculateTotalCharge(
      this.memberManager.getMembers()
    );
  }

  getMachineStatus(machineId: string): string {
    return this.machineManager.getMachineStatus(machineId);
  }

  getWorkoutAdvice(targetMuscle: string): string {
    return this.workoutAdvisor.getWorkoutAdvice(targetMuscle);
  }

  getCleaningChecklist(): string[] {
    return this.cleaningManager.getCleaningChecklist();
  }
}

どうでしょうか? だいぶスッキリしましたよね。 こうすることで、多くのメリットが生まれます。

  1. テストが楽になる
    例えば会計処理の計算を変えるなら、BillingServiceだけを見ればOKです。
  2. 再利用ができる
    例えば「PersonalGym」という別のクラスを作るときにBillingServiceをそのまま使い回すことで会計処理を使いまわすことができます。
  3. 安心感
    「ここを変えたら関係ない箇所がバグるかも」という不安がグッと軽くなります

単一責任で気を付けること

単一責任の原則を守る際に気を付ける点が一つあります。**「クラスを分けすぎない」**ことです。
分けすぎると、逆に多くのファイルをまたぐことになり、コードを追うのが大変になりますので、、、。
単一責任の原則は「1クラス=1メソッド」ではなく、**「1クラス=1責任」**ということを忘れないでください。

例えば、トレーニング指導クラスに以下のようなメソッドがあっても、それは一つのクラスにまとめて大丈夫です。

  • トレーニングメニューの取得
  • トレーニング結果を記録
  • トレーニング負荷の計算

これらはすべて「トレーニング指導」という一つの責任の中に含まれるからです。

まとめ

今回はSOLID原則のうち「単一責任の原則」を紹介しました。 大きなシステムだと、複数の責任が積み重なった巨大なクラスに出会うことは珍しくありません。
僕も何万行とまではいかないですが、それなりに大きなクラスに遭遇した時があり、それはそれは読むのが大変でした、、、。
この記事を読んで、そんな巨大なクラスに恐怖を抱くのではなく、「どうやって分割してやろうか!」とワクワクするきっかけになれたら、とても嬉しいです。
次回は**「開放閉鎖の原則(Open-Closed Principle)」**に触れていきます。 実は、今回例に挙げた「トレーニング指導のクラス」がその原則に違反しているのです。何がいけないのかは次回お話しします!

最後にですが以下の動画はSolid原則についてサンプルコードを用いていてわかりやすかったので興味あったら是非見てみてください!
書籍では頭に全く入らなかった僕でもすっと理解できました!

ここまで読んでくださり、ありがとうございました!

コメント

タイトルとURLをコピーしました