🌟

SOLID原則とReact+TypeScript

2023/09/29に公開

オブジェクト指向設計の 5 つの基本原則、SOLID 原則を React+TypeScript のサンプルで解説。

S - 単一責務の原則 (Single Responsibility Principle)

説明:
一つのクラスや関数は、一つの責務だけを持つべき

「単一責務の原則」は、その名の通り、一つのクラスや関数が一つの「役割」や「責務」だけを持つべきだという考え方です。
これによって、コードが読みやすくなり、バグの発生を減少させることができます。

React+TypeScript の例:

// ユーザーデータを取得する関数
const fetchUserData = (userId: string): Promise<User> => {
  // APIからデータを取得するロジック
};

// ユーザーのプロフィールを表示するコンポーネント
const UserProfile: React.FC<{ user: User }> = ({ user }) => (
  <div>
    <h2>{user.name}</h2>
    <p>{user.bio}</p>
  </div>
);

1. ユーザーデータを取得する関数: fetchUserData

この関数の役割は、指定されたユーザー ID に基づいて API からユーザーデータを取得することです。
データの取得以外の責務、例えばデータの表示や加工、はこの関数の役割ではありません。

const fetchUserData = (userId: string): Promise<User> => {
  // APIからデータを取得するロジック
};

2. ユーザーのプロフィールを表示するコンポーネント:UserProfile

このコンポーネントの役割は、受け取ったユーザーデータを表示することです。
データの取得や保存などの操作はこのコンポーネントの役割ではありません。

const UserProfile: React.FC<{ user: User }> = ({ user }) => (
  <div>
    <h2>{user.name}</h2>
    <p>{user.bio}</p>
  </div>
);

この 2 つの例から、各部分が自分の役割をきちんと果たしており、混在していないことがわかります。
このように役割を明確に分けることで、後でコードを見直す際やバグを修正する際にも、
どこを修正すればよいのかがはっきりと分かるようになります。

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

説明:
「オープン/クローズドの原則」とは、ソフトウェアの部品(クラスや関数など)は、
新しい機能の追加には対応できるように「オープン(開いている)」であるべきだが、
既存のコードの変更には「クローズド(閉じている)」であるべきだ、という考え方です。

React+TypeScript の例:

type ButtonProps = {
  onClick: () => void;
  children: React.ReactNode;
};

const PrimaryButton: React.FC<ButtonProps> = (props) => (
  <button
    onClick={props.onClick}
    style={{ backgroundColor: "blue", color: "white" }}
  >
    {props.children}
  </button>
);

const SecondaryButton: React.FC<ButtonProps> = (props) => (
  <button
    onClick={props.onClick}
    style={{ backgroundColor: "gray", color: "white" }}
  >
    {props.children}
  </button>
);

1. ボタンのプロパティ型: ButtonProps

すべてのボタンが持つ共通のプロパティを定義しています。これにより、新しいボタンタイプを追加する際にも、この型定義を使用することで一貫性を持たせることができます。

type ButtonProps = {
  onClick: () => void;
  children: React.ReactNode;
};

2. プライマリボタン: PrimaryButton

特定のスタイル(青背景)でボタンを表示するコンポーネントです。

const PrimaryButton: React.FC<ButtonProps> = (props) => (
  <button
    onClick={props.onClick}
    style={{ backgroundColor: "blue", color: "white" }}
  >
    {props.children}
  </button>
);

3. セカンダリボタン: SecondaryButton

異なるスタイル(灰色背景)でボタンを表示するコンポーネントです。

const SecondaryButton: React.FC<ButtonProps> = (props) => (
  <button
    onClick={props.onClick}
    style={{ backgroundColor: "gray", color: "white" }}
  >
    {props.children}
  </button>
);

上記のように、新しいボタンのスタイルや動作を追加する場合、
既存の PrimaryButton や SecondaryButton を変更することなく、
新しいコンポーネントを追加するだけで対応可能です。
このようにして、既存のコードは変更せずに新しい機能を追加することができるのが
「オープン/クローズドの原則」のメリットです。

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

「リスコフの置換原則」とは、あるクラスを継承したサブクラスが存在する場合、
そのサブクラスのインスタンスは、基底クラスのインスタンスとして振る舞うことができる、
という考え方です。
具体的には、サブクラスは、基底クラスの重要な振る舞いや性質を変更すべきではありません。

React+TypeScript の例:

interface Bird {
  fly: () => void;
}

class Sparrow implements Bird {
  fly() {
    console.log("Sparrow is flying");
  }
}

class Ostrich {
  run() {
    console.log("Ostrich is running");
  }
}

1. 鳥を表すインターフェース: Bird

飛ぶ動作を持つ鳥を定義しています。

interface Bird {
  fly: () => void;
}

2. スズメ: Sparrow

スズメは Bird インターフェースを実装しており、飛ぶ動作を持っています。

class Sparrow implements Bird {
  fly() {
    console.log("Sparrow is flying");
  }
}

3. ダチョウ: Ostrich

ダチョウは Bird インターフェースを実装していませんが、
代わりに走る動作を持っています。

class Ostrich {
  run() {
    console.log("Ostrich is running");
  }
}

この例で言うと、Ostrich は Bird インターフェースを実装していないため、
この原則に違反しているわけではありません。
ただし、もし Ostrich が Bird を実装し、fly メソッドが何もしないか、エラーを発生させるような実装になっていた場合、
それはリスコフの置換原則に違反していることになります。

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

説明:
クラスは、不必要なインターフェイスを実装するべきではない

React+TypeScript の例:

interface Worker {
  work: () => void;
}

interface Eater {
  eat: () => void;
}

class Human implements Worker, Eater {
  work() {
    console.log("Human is working");
  }

  eat() {
    console.log("Human is eating");
  }
}

class Robot implements Worker {
  work() {
    console.log("Robot is working");
  }
}

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

説明:
高レベルのモジュールは、低レベルのモジュールに依存すべきではなく、両方とも抽象に依存すべき

React+TypeScript の例:

interface Database {
  save: (data: string) => void;
}

class MongoDB implements Database {
  save(data: string) {
    console.log(`Saving ${data} to MongoDB`);
  }
}

class App {
  constructor(private database: Database) {}

  saveData(data: string) {
    this.database.save(data);
  }
}

以上、SOLID 原則を React+TypeScript サンプル付きで解説してみました。
日常のコーディングに適用することで、より品質の高いコードを書くことの糧になってもらえたら幸いです。

Discussion