🌊

SOLIDの原則について

2024/02/04に公開

TODO

  • L(リスコフ)の実例を記載する
  • D(依存性逆転)の実例を記載する

はじめに

このページはSOLIDの原則について、学習した内容を整理しているページです。

SOLIDとは?

ソフトウェアエンジニアのRobert C. Martinが提唱したもので、オブジェクト指向プログラミングにおいてソフトウェアの拡張性や保守性を高めるための5つのルールのことを指します。
私自身学習していく中で守れていない原則があるなと思った箇所もあり、とても勉強になるルールだと思いました。
下記にそれぞれのルールを記載して行きます。

S(Single Responsibility)単一責任の原則

クラスは、単一の責任を持つべきだ

単一責任の原則とは、一つのモジュールには一つだけ責任を持つようにしようとした」ルールことを言います。例を交えて下記に記載していく。

ダメな例

class Task {

    // タスクの登録・編集

    // メモの登録・編集

    // ユーザー情報の登録・編集
}

このダメな例では、Taskクラスの中にメモやユーザーといった異なる概念があり
Taskクラスがどういったクラスなのかよくわからなくなってしまっています。
これではこのTaskクラスは何がしたいクラスなのかわからず、今後の改修作業が難しくなってしまいます。

ここでは、上記に記載した概念という言葉が大切になってきます。
次にいい例を見ていきましょう。

いい例

class Task {

    // タスクの登録・編集
    register(){}

    edit(){}
}

class Memo{
    // メモの登録・編集
    register(){}

    edit(){}
}

class User {
    // ユーザー情報の登録・編集
    register(){}

    edit(){}
}

このいい例では、先程の駄目な例とは違い、タスク、メモ、ユーザーの異なる概念を別々のクラスにしており各クラスが何を行いたいのか明確になっています。そのため各クラスの責任が明確になり、今後の改修作業がしやすくなっていきます。

このように各クラスの責任が明確となり責任が1つになっていることを単一責任の原則といいます。
これを実現するには、ビジネスについても理解を深める必要があり、私自身とても大切なルールだと感じました。

O(Open-Closed)オープン・クローズドの原則

クラスは、拡張にはオープンで、変更にはクローズドであるべきだ

変更が発生した場合に、既存のコードには修正を加えずに、新しくコードを追加するだけで対応できるような設計にするといったルールになります。
どういったことか例を交えて記載していこうと思います。

ダメな例

enum Kind {
  DAILYWORK = "dailywork",
  WEEKlyWORK = "weeklyeork",
}

function main(title: string, body: string, typeTask: string) {
  const task = new Task(title, body, typeTask);
  task.getTaskDetail(typeTask);
}

class Task {
  private kind: number;

  constructor(
    private title: string,
    private body: string,

    typeTask: string
  ) {
    this.title = title;
    this.body = body;
    if (Kind.DAILYWORK === typeTask) {
      this.kind = 0;
    }
    if (Kind.WEEKlyWORK === typeTask) {
      this.kind = 1;
    }
  }

  getTaskDetail(typeTask: string) {
    if (Kind.DAILYWORK === typeTask) {
      //   ....
    }
    if (Kind.WEEKlyWORK === typeTask) {
      /// ...
    }
  }
}

このTaskクラスでは引数のtypeTaskによって処理の分岐が行われていますが、もしtypeTaskの数が増えた場合、constructorやgetTaskDetailなど条件分岐指定た箇所が全て修正対象となってしまいます。

いい例

enum Kind {
  DAILYWORK = "dailywork",
  WEEKlyWORK = "weeklyeork",
}

function main(title: string, body: string, typeTask: string) {
  const kindCheck = typeTask === Kind.DAILYWORK || typeTask === Kind.WEEKlyWORK;
  if (!kindCheck) throw new Error("正しいtypeTaskを入力してください");

  let task: Task;
  if (Kind.DAILYWORK === typeTask) {
    task = new DailyTask(title, body);
  } else {
    task = new WeeklyTask(title, body);
  }
  task.getTaskDetail();
}

interface Task {
  getTaskDetail(): string;
}

class DailyTask implements Task {
  constructor(
    private title: string,
    private body: string,
    private kind: number = 0
  ) {
    this.title = title;
    this.body = body;
  }

  getTaskDetail() {
    //   ....
    return "DailyTaskの詳細を返却";
  }
}

class WeeklyTask implements Task {
  constructor(
    private title: string,
    private body: string,
    private kind: number = 1
  ) {
    this.title = title;
    this.body = body;
  }

  getTaskDetail() {
    //   ....
    return "WeeklyTaskの詳細を返却";
  }
}

この例ではinterfaceを使用して、クラスをtypeTask毎に分割しました。
これにより各Taskクラス内の条件分岐がなくなり、typeTaskが新しく追加された際は新しくクラスを作成して、呼び出し側の処理を追加すれば対応することが可能となりました!

※やりすぎるとクラスのファイル数が大くなってしまうので注意が必要。

L (Liskov Substitution) リスコフの置換原則

SがTのサブタイプである場合、プログラム内のT型のオブジェクトをS型のオブジェクトに置き換えても、そのプログラムの特性は何も変わらない

子クラスが親クラスと同じ動作を実行できない場合、バグになる可能性があるため親クラスやその子クラスの一貫性を保つことを目的としたルールです。

I (Interface Segregation) インターフェイス分離の原則

クライアントが使用しないメソッドへの依存を、強制すべきではない

インターフェイスを継承する際に使用しないメソッドを継承するなといったルールです。
使用しないメソッドを継承していると思わぬ事故につながるため注意が必要です。

D (Dependency Inversion) 依存性逆転の原則

上位モジュールは、下位モジュールに依存してはならない。どちらも抽象化に依存すべきだ
抽象化は詳細に依存してはならない。詳細が抽象化に依存すべきだ

終わりに

SOLIDの原則について、各ルールを守るためには、S(単一の責任)を意識したクラス設計を行い必要であればO(オープンクローズド)を意識した継承を実施する。この時にL(リスコフ)、I(インターフェイス分離)、D(依存性逆転)を注意しながら行うこと守れるルールだと感じた。

参考サイト

Discussion