RIT Tech Blog

株式会社RITのエンジニアが知見を共有する技術ブログです。

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

エンジニアの前田です。

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

オブジェクト指向で用いられる五つの原則の頭字語である、SOLIDのうちIの部分です。
不要なインターフェースに依存することを避けるべきという原則です。

不要なインターフェイスとは?

例えば以下のようなインターフェイスがあったとします。

// アクション全ての複合クラス
interface Action {
  run: () => void;
  stop: () => void;
  fly: () => void;
}

class Car impletements Action {
  run() {
    console.log('走る');
  }
  stop() {
    console.log('止まる')
  }
  fly() {
    console.log('車は飛べません')
  }
}

class Plane impletements Action {
  run() {
    console.log('走る');
  }
  stop() {
    console.log('止まる');
  }
  fly() {
    console.log('飛ぶ');
  }
}

ActionインターフェイスはCarクラスやPlaneクラスなどの乗り物ができるアクションのインターフェイスです。 上の例を見ていただければ分かるように、Carクラスは本来持つべきでないflyメソッドを実装することを余儀なくされています。 もし、Carクラスがflyという不要なメソッドを実装していることを知らない人がCarクラスのflyメソッドを使ってしまうと問題が起きる可能性があります。

以上のことから、

  • 不要な実装をしなければならない <- Carクラスのflyメソッド

  • フログラム上の不具合の原因 <- Carクラスはflyメソッドを使えないはずなのに使えてしまう

の2つの問題が発生していることがわかります。

インターフェイスを分離する

Actionインターフェイスを以下のように乗り物ごとに分離しましょう。

// 車のアクション
interface CarAction {
  run: () => void;
  stop: () => void;
}

// 飛行機のアクション
interface PlaneAction {
  run: () => void;
  stop: () => void;
  fly: () => void;
}

class Car impletements CarAction {
  run() {
    console.log('走る');
  }
  stop() {
    console.log('止まる')
  }
}

class Plane impletements PlaneAction {
  run() {
    console.log('走る');
  }
  stop() {
    console.log('止まる');
  }
  fly() {
    console.log('飛ぶ');
  }
}

乗り物ごとにActionのインターフェイスを分けることで、Carクラスがflyメソッドを実装する必要がなくなりました。

基底クラスを用意する

インターフェイスを分けることで、CarクラスとPlaneクラスを同一のクラスとして扱うことができなくなり困ることがありそうです。 そんな時は以下のように基底クラスを用意しましょう。

// 乗り物のインターフェイスの基底クラス
class NorimonoAction {
  run: () => void;
  stop: () => void;
}

// 車のアクション
interface CarAction extends NorimonoAction  {}

// 飛行機のアクション
interface PlaneAction extends NorimonoAction {
  fly: () => void;
}

function eachRun(norimonos: Norimono[]) {
  norimonos.forEach(norimono => {
    norimono.run();
    norimono.stop();
  })
  console.log("finish all run");
}

以上のように基底クラスを作ることで、CarとPlaneを安全に同一のものとしても扱うことができるようになりましたね

メソッドの引数の場合のインターフェイスの分離の必要性

メソッドの引数についても分離しておいたほうがいいよという話も簡単に説明しておきます。 メソッドの引数を分離していない場合の例を以下に示します。

interface Post {
  message: string;
  userId: number;
}

function displayMessage(post: Post) {
  document.getElementById("area1").innerText = post.message;
}

displayMessageメソッドは特定の文字列を画面に表示するシンプルなメソッドです。 displayMessageの引数としてPostオブジェクトをそのまま渡しています。 以下の問題が発生しそうです。

  • postオブジェクトの構造が変わった時に、displayMessageメソッドにも変更が波及する

  • postオブジェクトのmessageプロパティ以外を画面に表示させたい時

そんなときはインターフェイスを分離させましょう

function displayMessage(message: string) {
  document.getElementById("area1").innerText = message;
}

displayMessage(post.message);

例外

以下のように、オブジェクトに密接したメソッドの場合、無理に分離させるのではなくそのまま使ったほうがいい場合もあります

interface User {
  id: number;
  name: string;
  role: string;
  comment: string;
}

function displayUserInfo(user: User) {
  document.getElementById("area1").innerText = `id: ${user.id}, name: ${user.name}, comment: ${user.comment}`;
}

displayUserInfoの引数にuserをそのまま渡していますね。 これをインターフェイスで分離すると、id, name, commentをそれぞれ引数に分けて渡すことになって面倒そうですね。引数の順番を間違えたりエラーの原因にもなりそうです。 そもそも、Userオブジェクトの詳細を表示するメソッドなのでUserオブジェクト以外を渡す必要もなく、分離する必要がなさそうです。 時と場合で使い分けましょう。

以上です。ありがとうございました。