I … Interface Segregation Principle: インターフェイス分離の原則
オブジェクト指向の考え方を用いることによって、関連のあるデータ構造とそれを操作する手続きを結びつけて「オブジェクト」とし、それらオブジェクトを組み合わせてソフトウェアを設計・開発することができる。
関連のあるデータ構造とそれを操作する手続きの結びつけ方、オブジェクト同士の組み合わせ方について適切な取り決めを設け、よりよいソフトウェアを設計・開発するために考案された原則として「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原則に違反した設計・開発を行ってしまうと、
- 新しい機能を追加するために既存のコードに大量の修正を加える必要がある
- 既存の機能を変更することでバグを生んでしまう可能性が高い
- 既存のコードを理解するために多くの時間を費やしてしまい、機能の追加や修正に時間がかかりがちになる
- 機能の追加時に既存のコードを再利用できないため、開発効率が悪くなる
などの不都合が生まれる。
本記事では、SOLID原則のひとつ「インターフェイス分離の原則」について考え方、アンチパターンとその解決策の例をTypeScriptのサンプルコードとともに紹介する。
概要
インターフェイス分離の原則とは、インターフェイスとクライアント(インターフェイスの利用者)があるとき、インターフェイスに用意されてある(利用するクライアントにとって)不必要なメソッドにクライアントが依存しなくてもよいように、分割できるインターフェイスは分割するべきであるという原則。
原則に違反してはいけない理由
インターフェイスに用意されている不必要なメソッドにクライアントが依存してしまうと、クライアントでは「例外を返すだけのメソッド」のような意味のないメソッドを用意しなければならない。また、そのようなクライアントが増えると、インターフェイスに変更を加えたときの影響範囲も増えてしまい、バグを生む可能性が高まる。
インターフェイス分離の原則に違反している例
TypeScriptのサンプルコードで、インターフェイス分離の原則に違反している例を紹介する。
タスク管理アプリケーションの「タスク」に関する情報を持つTask
オブジェクトと、「タスクのタイトル」を更新するupdateTaskTitle
関数との関係について。ITaskRepository
はDBなどの永続化層のインターフェイスを表しているが、この例ではあまり重要でないため説明は省略する。
export type Task = {
id: string;
title: string;
details: string;
status: "ToDo" | "InProgress" | "Done";
createdAt: Date;
updatedAt: Date;
};
const updateTaskTitle = async (
task: Task,
repository: ITaskRepository,
): Promise<void> => {
await repository.updateTitle({ id: task.id, title: task.title });
};
export interface ITaskRepository {
registerTask: (props: { title: string; details: string }) => Promise<void>;
getTask: (props: { id: string }) => Promise<Task>;
updateTitle: (props: { id: string; title: string }) => Promise<void>;
updateDetails: (props: { id: string; details: string }) => Promise<void>;
// etc...
}
updateTaskTitle
関数の仮引数としてTask
型のtask
オブジェクトを設定しているが、この部分がインターフェイス分離の原則に違反している可能性がある。
違反している理由
updateTaskTitle
関数では、Task
型のtask
オブジェクトを仮引数として設定しているが、関数内で実際に使われているのは、Task
型のうちid
とtitle
のみである。そのため、たとえば、Task
型が以下のように変更されてしまった場合、バグが発生する。
type Task = {
id: string;
taskInfo: {
title: string;
details: string;
status: "ToDo" | "InProgress" | "Done";
},
createdAt: Date;
updatedAt: Date;
};
updateTaskTitle
関数ではtask.title
がundefined
になってしまうため、repository.updateTitle
が必ず失敗してしまう。
const updateTaskTitle = async (
task: Task,
repository: ITaskRepository,
): Promise<void> => {
await repository.updateTitle({ id: task.id, title: task.title });
// => TS2339: Property 'title' does not exist on type 'Task'.
};
解決策
updateTaskTitle
関数ではstring型のid
とstring型のtitle
しか利用しないため、仮引数をTask
まるごとではなくid
とtitle
に分割することで、Task
の変更によるバグなどは防ぐことができるようになる。(updateTaskTitle
関数を利用する側からtask.taskInfo.title
を渡せばよい)
const updateTaskTitle = async (
task: {id: string, title: string},
repository: ITaskRepository,
): Promise<void> => {
await repository.updateTitle({ id: task.id, title: task.title });
};
※ Task
が、各プロパティの整合性を保つためのバリデーターメソッドなどを備えたクラス等であった場合、必ずしもインターフェイスを分割すべきであるとは言えないため、注意が必要。