【SOLID原則シリーズ】インターフェース分離の原則とは ~必要な機能だけを使え~

開発ログ

はじめに

Solid原則シリーズも4回目となり終盤になりました!
今回は「インターフェース分離の原則」について紹介します。

インターフェースに分離することで、必要な機能だけを利用できるようになります。
開放閉鎖の原則においてもそのメリットについて触れてきました。
ただ、「分離すればするほどいい」というわけではありません。
今回は、インターフェース分離のメリットと、分離するときに気をつけたいポイントについて見ていきます。

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

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

「インターフェースを利用するクラスは自分が利用しないメソッドへの依存を避けるべき」

という原則です。

簡単にいうと、

「必要な機能だけを持ったインターフェースに分けよう」

ということです。

例えば、筋トレを例として挙げます。

「ワークアウト」というインターフェースがあり、共通した特徴として以下を持っていたとします。

  • 回数を設定して行う
  • セット数を設定して行う
  • 重量を設定して行う

それに対して、腕立て伏せというクラスがこのインターフェースを使用した場合、ダンベルなどの重量を使わないため、本来は必要のない「重量を設定する」という特徴まで強制されてしまいます。

この場合は、インターフェース分離の原則に違反しています。

コードで見ると実際にどうなるか、次で見てみましょう。

インターフェース分離に違反すると

上記の例をコードとして起こすとどうなるか見てみます。

/** ワークアウトのインターフェース */
interface Workout {
  setReps(): void;
  setSets(): void;
  setWeight(): void;
}

/** 腕立て伏せのクラス(ワークアウトインターフェースを使用) */
class PushUp implements Workout {
  setReps(): void {
    console.log("回数を設定");
  }

  setSets(): void {
    console.log("セット数を設定");
  }

  setWeight(): void {
    // ★腕立て伏せでは重量を設定しない
    throw new Error("腕立て伏せでは重量を設定できません");
  }
}

このコードを見ると、PushUpクラスではsetRepsとsetSetsは必要ですが、setWeightは不要になります。

しかし、Workoutインターフェースを実装しているので、必要のないsetWeightまで実装せざるを得ないのです、、、。
今回の例では、setWeightを呼び出されたときにエラーを発生させています。

もちろん、以下のようにして何もしないという方法もあります。

setWeight(): void {
  // TODO: 実装不要
}

ただ、これでは一切使わないメソッドを実装するためだけのコードが発生します。

これは積み重なれば、使えると思って呼び出したメソッドでエラーが起きたり、呼び出したのに何も処理が起きなかったりなど、バグの温床になりかねません。

さらに、今後WorkoutインターフェースにPushUpクラスで使わないような新しいメソッドを追加した場合、その使わないメソッドがさらに必要になってしまいます。

では、どのようにインターフェースを分離すれば良いコードになるのでしょうか。

インターフェース分離の原則を守るなら

インターフェース分離の原則を守った場合、以下のようにすればよい感じになりそうです。

interface Workout {
  setReps(): void;
  setSets(): void;
}

interface WeightedWorkout {
  setWeight(): void;
}

さらにインターフェースを分割するのです。

これによってPushUpクラスも以下のようになります。

class PushUp implements Workout {
  setReps(): void {
    console.log("回数を設定");
  }

  setSets(): void {
    console.log("セット数を設定");
  }

  // 不要なsetWeightを呼ぶ必要がなくなった
}

逆にウェイトトレーニングのクラスを定義する場合、以下のようになります。

/** ベンチプレスのクラス */
class BenchPress implements Workout, WeightedWorkout {
  setReps(): void {
    console.log("回数を設定");
  }

  setSets(): void {
    console.log("セット数を設定");
  }

  setWeight(): void {
    console.log("重量を設定");
  }
}

使う分だけインターフェースを呼びます。

TypeScriptなら以下のようにもできますね。見た目はすっきりします。

interface Workout {
  setReps(): void;
  setSets(): void;
}

/** WeightedWorkoutはWorkoutを継承して、さらにsetWeight()を追加。 */
interface WeightedWorkout extends Workout {
  setWeight(): void;
}

/** WeightedWorkoutインターフェースを使用 */
class BenchPress implements WeightedWorkout {
  setReps(): void {
    console.log("回数を設定");
  }

  setSets(): void {
    console.log("セット数を設定");
  }

  setWeight(): void {
    console.log("重量を設定");
  }
}

とはいえ、インターフェースを細かく分けすぎても、組み合わせるインターフェースが多くなって逆に読みにくくなることがあります。

以下のような現象が見え始めたら、インターフェースの分割をしてみるといいと思います。
もちろん、これらがあったからといって、必ずインターフェースを分割する必要があるわけではありません。

  • エラーを吐くだけのメソッドを作る必要がある
  • 空のメソッドを実装する必要がある
  • // TODO: 実装不要というコメントを書く必要がある

終わりに

今回はインターフェース分離の原則について見ていきました。

インターフェースを分離することで、クラスごとに必要な機能だけを持たせることができます。
一方で、何でも細かく分ければいいというわけではありません。
今回の例のように、「このクラスでは使わないメソッドを実装しなければならない」という状況が出てきたら、インターフェースの分割を検討してみると良いと思います。

僕も当時は「うわ、空のメソッドでてきちゃったどうしよう」となってしまったので、同じような状況になってしまった場合は「は!インターフェース分離の原則だ」と思い出していただければと思います。

今はAIも使えますので、インターフェース分離の原則というキーワードを使えば、よいコードも生成してくれるはずです!

ここまで読んでいただきありがとうございました。

参考

【SOLID原則】#4 太ったインターフェースの害 – 使わないメソッドを押し付けるな

インターフェイス分離の原則 – Qiita

コメント

タイトルとURLをコピーしました