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のサンプルコードとともに紹介します。

インターフェイス分離の原則とは?

概要

インターフェイス分離の原則とは、インターフェイスとクライアント(インターフェイスの利用者)がいるときに、インターフェイスに用意されてある不必要なメソッドやプロパティ(利用するクライアントにとって不必要)にクライアントが依存しなくてもよいように、分割できるインターフェイスは分割するべきであるという原則です。

原則に違反してはいけない理由

クライアントが不必要なメソッドに依存しているということは、クライアントが知る必要がない情報を持ってしまっているということです。たとえばインターフェイスが変更になった場合は(クライアントが関係しない部分であっても)その変更に引っ張られてクライアントも修正しなければいけなくなったりします。

インターフェイス分離の原則に違反している例

TypeScriptのサンプルコードで、インターフェイス分離の原則に違反している例を紹介します。

タスク管理アプリケーションの「タスク」に関する情報を持つTaskオブジェクトと、「タスクのタイトル」を更新するupdateTaskTitle関数との関係についてのコードです。

export type Task = {
  id: string;
  title: string;
  details: string;
  status: "ToDo" | "InProgress" | "Done";
  createdAt: Date;
  updatedAt: Date;
};
const updateTaskTitle = async (
  task: Task,
  updateTitle: (props: {id: string, title: string}) => Promise<void>
): Promise<void> => {
  await updateTitle({ id: task.id, title: task.title });
};

updateTitleは、実際にDBなどと接続して永続化処理を行う処理が実装された関数です。

updateTaskTitle関数について、仮引数としてTask型のtaskオブジェクトを設定していますが、この部分がインターフェイス分離の原則に違反している可能性があります。

原則に違反している理由

updateTaskTitle関数では、Task型のtaskオブジェクトを仮引数として設定していますが、関数内で実際に使われているのは、Task型のうちidtitleのみです。

Task型が以下のように変更されてしまった場合を考えます。

type Task = {
  id: string;
  taskInfo: {
    title: string;
    details: string;
    status: "ToDo" | "InProgress" | "Done";
  },
  createdAt: Date;
  updatedAt: Date;
};

この場合、task.titleundefinedになってしまうため、updateTaskTitle関数の修正が必要になります。

const updateTaskTitle = async (
  task: Task,
  updateTitle: (props: {id: string, title: string}) => Promise<void>
): Promise<void> => {
  await updateTitle({ id: task.id, title: task.title });
  // => TS2339: Property 'title' does not exist on type 'Task'.
};

updateTaskTitle関数は1つのタスク(idで特定)のタイトルを変更(titleで指定)するための関数ですが、Taskの構造の変更という本来関係ないはずの変更に影響を受けてしまっています。この例ではTypeScriptコンパイラがエラーを出してくれるので修正が必要なことに気付けますが、ある変更に対して本来関係ないはずの場所のコードも変更が必要になることは、避けるようにするべきです。

解決策

updateTaskTitle関数ではstring型のidとstring型のtitleしか利用しないため、仮引数をTaskまるごとではなくidtitleに分割することで、updateTaskTitleTaskの変更による変更を受けなくなります。updateTaskTitle関数を利用する側からtask.taskInfo.titleを渡せばよいです。

const updateTaskTitle = async (
  task: {id: string, title: string},
  updateTitle: (props: {id: string, title: string}) => Promise<void>
): Promise<void> => {
  await updateTitle({ id: task.id, title: task.title });
};

所感

この原則は、ReactやVue.jsなどのUIコンポーネントの設計にも役に立ちます。UIコンポーネントの受け取る仮引数(props)をインターフェイス分離の原則に従って設計することで、汎用的な(コンテキストに限定されない)UIにすることができます。

注意が必要な点として、この原則はどのような場面でも必ず有効であるとは限らないということを意識する必要があります。たとえば、プロパティが互いに整合性を保つように設計されていて、それぞれの値を単独で扱うべきではないようなクラスについては、インターフェイスは分離せずにクラスをそのまま渡したほうがよい場合もあります。

状況に応じて、適切なインターフェイスを設計したいですね。

採用情報

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

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

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