TypeScriptでSOLID原則〜単一責任の原則〜

TypeScriptでSOLID原則〜単一責任の原則〜

SOLID原則とは?

SOLID原則とは、オブジェクト指向プログラミングにおいて、変更しやすい理解しやすい再利用しやすいモジュール(クラスや関数などの、ソフトウェアのを構成する部品)を設計・開発するための原則のことです。ソフトウェアエンジニアRobert C. Martinに提唱された多くの設計原則を5つにまとめたものの頭文字をとって命名されました。

  • S … Single Responsibility Principle: 単一責任の原則
  • O … Open-Closed Principle: 開放閉鎖の原則
  • L … Liskov Substitution Principle: リスコフの置換原則
  • I … Interface Segregation Principle: インターフェイス分離の原則
  • D … Dependency Inversion Principle: 依存性逆転の原則

オブジェクト指向プログラミングは、機能の変更や追加に対して柔軟に対応できるソフトウェアを構築するための手法です。しかし、複数の開発者が様々な事情でソフトウェアに機能の変更や追加を施し続けていくことによって、ソフトウェアは柔軟性を失っていきがちです。

  • 新しい機能を追加するために既存のコードに大量の修正を加える必要がある
  • 既存の機能を変更しようとした場合、既存コードの変更による影響範囲がはっきりせず、バグを生んでしまう可能性が高い
  • 既存コードを理解するために多くの時間を費やしてしまい、機能の追加や変更に時間がかかる
  • 既存コードが再利用しにくくなっており、機能の追加時に似たような処理を別の場所に書かなければならず、開発効率が悪い

SOLID原則を学ぶことで、ソフトウェアが上記のようなツラい状態になることを回避できる(もしくはツラい状態から回復できる)可能性があります。

本記事では、SOLID原則のひとつ「単一責任の原則」について考え方、アンチパターンとその解決策の例をTypeScriptのサンプルコードとともに紹介します。

単一責任の原則とは?

概要

単一責任の原則とは、ソフトウェアを構成する個々のモジュールが責任を負う対象は、たったひとつにするべきであるという原則です。

ソフトウェアの既存の機能に変更を加えたり新たな機能を追加する理由は、そのソフトウェアのユーザーやステークホルダーを満足させるためです。この「ソフトウェアのユーザーやステークホルダー」が、単一責任の原則における「モジュールが責任を負う対象」です。

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

たとえば対象Aと対象Bに関して責任を負ってしまっている部品があった場合、それはAの動作のためのコードとBの動作のためのコードが一体になってしまっていることになります。そうすると、Aの動作に関する改修をしたつもりが、その影響がBにも及んでしまいバグが生まれてしまう可能性があります。

複数の対象に対して責任を負っている部品がある場合、その部品を、責任を負う対象の数に分割してあげることで、アンチパターンを回避することができます。

単一責任の原則に違反している例

TypeScriptのサンプルコードで、単一責任の原則に違反している例を紹介します。

たとえば、以下のようなEmployeeクラスは単一責任の原則に違反しているといえます。

class Employee {
  public name: string;
  public department: string;
  // etc...

  public constructor(...) {...};

  /**
   * 給与計算のメソッド
   * 経理部門に対して責任を負っている
   */
  public calculatePay = (): Money => {...};

  /**
   * 労働時間レポートを出力するメソッド
   * 人事部門に対して責任を負っている
   */
  public reportHours = (): string => {...};

  /**
   * 従業員情報をDBに保存するメソッド
   * データベース管理者に対して責任を負っている
   */
  public save = (): void => {...};

  /**
   * 所定労働時間を算出するメソッド
   * `calculatePay`と`reportHours`の両方で必要な処理のため、メソッドに切り出して共通化している
   */
  private getRegularHours = (): number => {...};
}

原則に違反している理由

このクラスは、経理部門、人事部門、データベース管理者の3つの対象について責任を負っています。共通の処理をregularHoursメソッドとして切り出しており、うまくコードの重複を回避しているように見えますが、これが、バグの原因になることも考えられます。

たとえば、経理部門から、所定労働時間の算出方法を変更したい依頼があったとします。改修担当者は、calculatePayからgetRegularHoursを呼んで所定労働時間を変更していることを確認し、getRegularHoursに変更を加えます。ユニットテストをパスし、経理部門の担当者に動作確認もしてもらい問題なかったため、この変更は本番環境にデプロイされます。

ここで問題なのは、改修担当者はgetRegularHoursreportHoursからも呼ばれていることを確認していないことです。もし、経理部門が扱う所定労働時間と人事部門が扱う所定労働時間の算出方法が異なるものだった場合、人事部門は所定労働時間の算出方法が誤ったものに変更されていることに気づかないまま、getRegularHoursが算出した値を使い続けることになります。

解決策

「メソッドに変更を加える際は、そのメソッドがどこから呼ばれているかしっかり確認する」では、ケアレスミスを防ぐことが出来ず、根本的な解決策にはなりません。

たとえば、EmployeeクラスをPayCalculatorクラス・HourReporterクラス・EmployeeSaverクラス・EmployeeDataクラスに分割することで、上記のようなミスを防ぐことができます。

/**
 * 従業員に関するデータのみをプロパティとして持つ
 * メソッドは持たない
 */
class EmployeeData {
  public name: string;
  public department: string;
  // etc...

  public constructor(...) {...};
}

class PayCalculator {
  public constructor (
    private readonly employeeData: EmployeeData,
  ) {};

  public execute = (): Money => {...};

  private getRegularHours = (): number => {...};
}

class HourReporter {
  public constructor (
    private readonly employeeData: EmployeeData,
  ) {};

  public execute = (): string => {...};

  private getRegularHours = (): number => {...};
}

class EmployeeSaver {
  public constructor (
    private readonly employeeData: EmployeeData,
  ) {};

  public execute = (): void => {...};
}

Employeeクラスを「責任を負う対象」の数に分割しています。この場合、経理部門から所定労働時間の算出方法を変更したい依頼があった場合、PayCalculatorgetRegularHoursを変更しても他の部門に影響がないことは明らかです。

所感

単一責任の原則は、SOLID原則の中でも特に重要な原則のひとつだと思います。今回は、原則に違反していることによるバグの例を紹介しましたが、単一責任の原則を意識することによって、モジュールの可読性がよくなる、再利用しやすくなるなど、いいことがたくさんあるので、ぜひ普段の設計・開発時には意識したいですね。

採用情報

メンバーズエッジで最高のチームで最高のプロダクトを作りませんか?

最高のプロダクトをつくる 最高のチームで働く

在宅でも、地方でも、首都圏でも。多様な働き方で最高のチームをつくり、お客様のプロダクトパートナーを目指します。アジャイル開発を通じ、開発現場の第一線で活躍し続けたいエンジニアを募集しています。