最近、SOLID原則の「I」 を強く意識しています。
はじめに
同僚がスタンド機能付きのモバイルバッテリーを使ってたんですが、スタンドの部分が壊れたみたい。
それがSOLID原則の「I」、「インターフェース分離の原則」 を意識し始めたきっかけです。
SOLID原則について
ソフトウェア設計においての原則です。
読みやすく、メンテナンスがしやすいコードを書くために考えたルールですね。
- 単一責任の原則(Single Responsibility Principle)
- オープン・クローズドの原則(Open/Closed Principle)
- リスコフの置換原則(Liskov Substitution Principle)
- インターフェース分離の原則(Interface Segregation Principle)
- 依存性逆転の原則(Dependency Inversion Principle)
ISP ~ Interface Segregation Principle
さっそくですが、今回の主題である、SOLID原則の「I」。
インターフェース分離の原則です。
クライアントが利用しないメソッドへの依存を強制してはならない。
原則
- 大きな汎用的な
Interface
を実装しない、単機能に特化したInterface
を設計する - 使用しないメソッドに依存させない
って感じですかね。
ちょっとこれだけじゃ説明不足だと思います。
コードで説明します
エンジニアの方々は「文章読むよりコードで理解したい」って人も多いはず。
動物を例に。
原則を守っていない設計
まずはNG例から。
interface Animal {
public function eat();
public function sleep();
public function fly();
}
class Bird implements Animal {
public function eat() {
}
public function sleep() {
}
public function fly() {
}
}
class Dog implements Animal {
public function eat() {
}
public function sleep() {
}
public function fly() {
}
}
Bird
、Dog
どちらも、Animal Interface
に依存してしまいますよね。
このままでは、Dog
は「飛んで!」と命令されたら飛ばなきゃいけません。
原則を守った設計
interface Animal {
public function eat();
public function sleep();
}
interface FlyingAnimal {
public function fly();
}
class Bird implements Animal, FlyingAnimal {
public function eat() {
}
public function sleep() {
}
public function fly() {
}
}
class Dog implements Animal {
public function eat() {
}
public function sleep() {
}
}
大空に羽ばたく動物たちのためにFlyingAnimal Interface
を用意しました。
これで、Dog
は「飛んで!」って命令されても「そんなことは教えられてないから知らないよ」で通せますよね。
ちょっと解説
インターフェースを分離することで本来不要であるメソッドの実装をしなくてよくなりました。
仮に飛び方を忘れたFlyingAnimal
がいたとしたら、Flying Interface
を見直して「また飛べるように向き合ってあげればいいよね。」って感じです。
もしNG例のように 1つ の大きく汎用的なInterface
として設計してしまった場合だと、飛べなくなった原因を探す必要があります。大変...。
知らないうちに依存する部分が大きくなってそうです。
文章で説明します
原則を守る利点についてまとめました。
1. クラスの責務を明確できる
Interface
が「特化した特定の機能に焦点を当てたもの」かつ「小さければ小さいほど」、クラス自身も特定の責務に集中できます。
「1つのメソッドで1つの処理」を意識してコードを書いているんですが、クラス単位でも責務を明確化できるかと思います。(単一責任の原則ですね)
2. 再利用性
「コンポーネント化」というと少しニュアンスが違うような気もしますが...。
多機能で汎用的な大きいInterface
は出番が限られます。一度きりの仕様で一度きりの出番となることも多くなりそう。
でも、単機能に特化した小さなInterface(設計書)
であれば、その機能を導入したい場面があれば容易に適用できますよね。
パーティーセットのお菓子って、パーティーのときにしか活躍できませんよね。
でも、その中の個包装のお菓子はパーティーに限らずさまざまな場面で活躍しますよね。
3. 依存関係を明確にできる
クラスが汎用的な大きなInterface
で実装されている場合、実装している全てのクラスが影響を受けます。
誰にでも、依存しているものって少なからずあるかと思います。
仕事とプライベートをきちんと分離できているのであれば、それぞれに影響を与えることって少ないかと思います。(いい影響も悪い影響も)
でも...。仕事とプライベートの分離ができず、切り替えが上手くできないことだってありますよね。
(だって人間だもん、心があるもん)
4. 不要な実装が減る & 重複を減らせる
単機能に特化したInterface
を使用することで、特定の機能に対する抽象クラスを作成しやすくなります。
interface Printable { // 印刷に特化したInterface
public function print(): void;
}
interface Saveable { // 保存に特化したInterface
public function save(): void;
}
abstract class Document implements Printable, Saveable {
public function print(): void {
// 印刷機能の実装
}
public function save(): void {
// 保存機能の実装
}
}
class Report extends Document {
// Report固有の機能を追加
}
class Invoice extends Document {
// Invoice固有の機能を追加
}
Document
クラスを継承する具体的なクラスは、print()
やsave()
の実装を再度行う必要がなくなります。(コードの重複を避けることができます。)
また、Animal Interface
の、Dog
に飛び方を教えなくてよくなります。(こちら)
5. その他
長くなってきたので、さっといきます。
保守性向上
メンテナンスが容易になります。
機能単位で分割することで、新機能/バグ対応等での変更な場合に、変更箇所の特定がしやすくなりますよね。
テストが容易になる
テストの対象が明確化されるので、テスト作業が減ります。
こんなクラスがあった場合、テストが大変そう...。
- 汎用的な
Interface
で多大な量のメソッドの実装が求められる - その他にも複数の
Interface
を実装している
設計が直感的になる
機能単位ごとにInterface
が設計されていると、コードが直感的に理解しやすくなります。
もちろん、Interface
自体の命名が直感的であることが重要ですが、機能単位のInterface
はコードを追いやすくし、理解を助けます。
逆に原則を守る欠点は?
欠点は思い浮かばないです。
ちょっとわかんないので、AIと壁打ちしました。
単純な設計
初期設計段階ではインターフェースが一つだけで済むため、設計がシンプルになります。
すべての動物クラスが同じインターフェースを実装するため、一貫性があるように見えることもあります。しかし、このメリットは長期的には大きなデメリットに転じることが多いです。
とのこと。
AIが、回りに回って「デメリットと判断している」のであれば、ISPは遵守したほうがいいんでしょうね。
まとめ
業界経験があまり長くないので、今回勉強できて良いきっかけになりました。
実装のアプローチに幅が広がった気がします。(気がするだけ)
原則…。とはいえ、完全に守るべきものかどうかは正直わかりません。
チームの開発方針、プロジェクトの運用想定、納期など、状況次第で重視するべき部分が変わってくることもありますよね。
ただ、
「インターフェースの分離の原則」、これを指針とすることは間違いない選択だと思います。
守れる強さがほしいです。
番外編
~きっかけをくれた同僚に向けて~
interface Battery {
/*
* 充電する.
*/
public function charge();
}
interface Stand {
/*
* 傾ける.
*/
public function tilt();
/*
* 折りたたむ.
*/
public function foldUp();
}
パターン①
class BatteryStand implements Battery, Stand {
public function charge() {
}
public function tilt() {
}
public function foldUp() {
}
}
パターン②
class Battery implements Battery {
public function charge() {
}
}
class Stand implements Stand {
public function tilt() {
}
public function foldUp() {
}
}
どっちかというと...。
interface BatteryStand {
/*
* 充電する.
*/
public function charge();
/*
* 傾ける.
*/
public function tilt();
/*
* 折りたたむ.
*/
public function foldUp();
}