PROMARI JOURNAL

ISP――コピーの代役に、いいねの保存まで求めない

作成: / 公開: / 内容更新:

執筆:tamito0201 / 掲載・運営:プロマリ

丸い共有ボタンの裏側を掘る連載の第7回。SOLID の4つ目の原則 ISP を、飛べないペンギンの例から、プラグインの5つの小さな口(ポート)とテストの代役まで読み進めます。全部入りのインターフェースにすると、コピーの代役に何が要るのか。作業コピーでの実験と図15点で確かめます。

PASS IT ON

ひとつの発見を、次の会話へ。
自然光の差す木の机で、5つに仕切った木のトレイから青緑の小さなはさみを1つだけ取り上げている手元。奥には多機能ツールが閉じたまま置かれている写真風のイメージ

こんにちは、プロマリの紫です。連載「丸いボタンの裏側」の第7回、第2章の4回目です。SOLID は、変更に強いコードを書くための5つの原則を、頭文字でまとめた呼び名です。前回はその3つ目の原則 LSP を読み、「取り替えても、利用側が頼っていた約束が崩れないか」を確かめました。今回は4つ目の原則、ISPです。テーマはテストでコピーの代役を作るとき、いいねの保存まで書かされないかということ。小さな例で原則をつかんでから、プラグインの実物と、一時的な作業コピーでの実験で確かめます。

点線の付いた用語は、その言葉を押すと詳しい説明が開きます。意味、身近なたとえ、この実装での使い方を順に読めます。キーボードではTabで用語へ移動し、EnterまたはSpaceで開き、Escapeで本文へ戻れます。各ページで最初に出る用語から参照できるようにしました。

この記事は連載「丸いボタンの裏側」の第7回(2.4)です。前回の2.3「LSP――Copyに「開くURL」を要求すると何が壊れるか」で、取り替えても約束が崩れない形を先に読むと、今回の話がつながりやすくなります。連載全体の地図は、本編5ページ目の連載の目次からどうぞ。

この回は6ページあります。1ページ目で ISP の意味を、鳥とペンギンの短い例でつかみます。2ページ目でプラグインの実物を開き、ブラウザの機能を使うための口が5つの小さなインターフェースに分かれていることを確かめます。3ページ目でテストの代役を読み、もし全部入りの1つにまとめていたら代役に何が要るかを、一時的な作業コピーで試します。4ページ目では、いいねの保存がどの口にも入っていない理由を、5ページ目では分ける単位と分けすぎの代償を考え、6ページ目で前回の LSP とのつながりと宿題に進みます。図は15点です。実験の結果として載せるエラーの文面や件数は、すべて実際に手元で動かして取ったものです。

使わない能力まで、利用側に求めない

ISP は、日本語では「インターフェース分離の原則」と訳されます。最初に言葉にしたのは Robert C. Martin で、1996年に雑誌 The C++ Report の連載コラムに書いた記事では、次の一文で定義しています。「クライアントは、自分が使わないインターフェースへの依存を強いられるべきではない(Clients should not be forced to depend upon interfaces that they do not use.)」。

ここで言うクライアントは、お客さんのことではありません。あるインターフェースを使って仕事をする側のコード、この記事の言葉でいう利用側のことです。Martin は2020年の記事で、同じ原則をもっと短く言い直しています。「インターフェースを小さく保ち、使う人が必要のないものに依存しないようにする(Keep interfaces small so that users don’t end up depending on things they don’t need.)」。

図1-1 印刷だけ頼みたいのに、配送先まで必須
図1-1 印刷だけ頼みたいのに、配送先まで必須。説明(たとえ)。頼む人に要るのは、自分の用事の欄だけ。ISP は、使わない欄を必須にしないようにインターフェースを分けます。図1-1 印刷だけ頼みたいのに、配送先まで必須。説明(たとえ)。頼む人に要るのは、自分の用事の欄だけ。ISP は、使わない欄を必須にしないようにインターフェースを分けます。

身近なもので言えば、申込書です。コピー店で文書を1枚印刷してもらいたいだけなのに、申込書に「配送先の住所」「支払い方法」「会員番号」の欄があり、どれも必須だったらどうでしょう。印刷には関係ないのに、全部を埋めないと受け付けてもらえません。しかも、配送のルールが変わって欄が1つ増えたら、印刷しか頼まない人まで、新しい申込書に書き直すことになります。ISP が避けたいのは、この「使わない欄の必須」です。

共有ボタンでいえば、コピーのボタンです。コピーの処理に要るのは、「文字列をクリップボードに書き込む」能力だけです。ところが、いいねの保存やアクセスの記録まで1つのインターフェースに入れてしまうと、テストでコピーの代役を作るだけで、いいねの保存まで書かされることになります。この回の題名は、ここから来ています。

なぜ、使わない能力に頼ってはいけないのか

Martin は1996年の記事で、理由をこう説明しています。使わないインターフェースに頼らされている利用側は、そのインターフェースが変わるたびに影響を受ける。ほかの利用側の都合で変わった部分に、自分まで巻き込まれる、ということです。本来は無関係なはずの利用側どうしが、同じインターフェースを通して、知らないうちにつながってしまいます。

守らないと、具体的には3つのことが起きます。1つ目に、テストで代役を作るたびに、使わない能力の分まで中身を書くことになります。2つ目に、使わない能力が変わるたびに、関係のない利用側まで直すことになります。申込書の欄が増えて、印刷の人まで書き直すのと同じです。3つ目に、使わない能力のために「例外を投げるだけ」や「何もしない」の中身が増えます。そうした中身は、取り替えたときに壊れる実装の温床になります。これは、このページの最後で前回の LSP とつなげて読みます。

まず一般的な例:飛べない鳥に fly を求める

原則を実物に当てる前に、よく使われる例で形をつかんでおきます。鳥を表すインターフェース Bird に、「飛ぶ(fly)」と「泳ぐ(swim)」の2つのメソッドを持たせた例です。この連載で使っている TypeScript で書きました。

TypeScriptfat.ts(一般的な例・守っていない形。説明用に書いたコード)
interface Bird {
  fly(): void;
  swim(): void;
}

class Duck implements Bird {
  fly(): void { console.log('カモが飛ぶ'); }
  swim(): void { console.log('カモが泳ぐ'); }
}

class Penguin implements Bird {
  fly(): void { throw new Error('ペンギンは飛べません'); }
  swim(): void { console.log('ペンギンが泳ぐ'); }
}

const birds: Bird[] = [new Duck(), new Penguin()];
birds.forEach((bird) => bird.fly());

上から読んでいきます。最初の4行が interface Bird で、「鳥なら fly と swim の両方を持つ」という約束です。次のカモ(Duck)は、飛ぶことも泳ぐこともできるので、両方を素直に書けます。困るのはその次のペンギン(Penguin)です。ペンギンは飛べませんが、Bird の約束がある以上、fly を書かないと型チェックを通りません。しかたなく、fly の中身は「ペンギンは飛べません」という例外を投げるだけにしています。最後の2行では、カモとペンギンを Bird の並びに入れ、まとめて飛ばしています。

図1.2-1 飛べない鳥に、fly を強いる
図1.2-1 飛べない鳥に、fly を強いる。一般的な例(守っていない形)。約束が大きすぎると、飛べない鳥にも fly を書かせます。壊れていると分かるのは、飛ばした瞬間です。図1.2-1 飛べない鳥に、fly を強いる。一般的な例(守っていない形)。約束が大きすぎると、飛べない鳥にも fly を書かせます。壊れていると分かるのは、飛ばした瞬間です。

このファイルを、型チェックと実行の両方にかけてみました。型チェック(tsc --noEmit --strict --target ES2022 --lib ES2022,DOM fat.ts)は、何も言わずに通りました。約束の上では、ペンギンも「飛べる鳥」だからです。ところが実際に動かす(node fat.ts)と、「カモが飛ぶ」と表示したあと、ペンギンの番で Error: ペンギンは飛べません と例外が出て止まりました。壊れていることが分かるのは、飛ばした瞬間です。もしこれがボタンの処理なら、利用者が押したときに初めて分かることになります。

約束を、能力ごとに分ける

ISP に沿って直すと、「飛ぶ」と「泳ぐ」を、Flyer と Swimmer という2つの小さなインターフェースに分けます。

TypeScriptsplit.ts(一般的な例・守っている形。説明用に書いたコード)
interface Flyer {
  fly(): void;
}

interface Swimmer {
  swim(): void;
}

class Duck implements Flyer, Swimmer {
  fly(): void { console.log('カモが飛ぶ'); }
  swim(): void { console.log('カモが泳ぐ'); }
}

class Penguin implements Swimmer {
  swim(): void { console.log('ペンギンが泳ぐ'); }
}

function flyAll(birds: Flyer[]): void {
  birds.forEach((bird) => bird.fly());
}

flyAll([new Duck()]);
flyAll([new Penguin()]);

最初の2つのかたまりが、飛ぶ能力の約束 Flyer と、泳ぐ能力の約束 Swimmer です。カモは両方を名乗り(implements Flyer, Swimmer)、ペンギンは Swimmer だけを名乗ります。ペンギンに、意味のない fly を書く必要はもうありません。flyAll は「飛ぶ能力を持つものの並び」だけを受け取る関数です。最後の行で、わざとペンギンを flyAll に渡してみました。

今度は動かす前に、型チェック(tsc --noEmit --strict --target ES2022 split.ts)が split.ts(23,9): error TS2741: Property 'fly' is missing in type 'Penguin' but required in type 'Flyer'. と言って止めました。「Penguin には fly が無いが、Flyer では必要だ」という意味です。分ける前と比べると、壊れていることに気づく時点が、動かしたあとから、動かす前へ移りました。

図1.3-1 飛ぶと泳ぐを、別々の約束にする
図1.3-1 飛ぶと泳ぐを、別々の約束にする。一般的な例(守っている形)。ペンギンは Swimmer だけを名乗ります。飛ばせない鳥を渡す誤りは、動かす前に型チェックが見つけます。図1.3-1 飛ぶと泳ぐを、別々の約束にする。一般的な例(守っている形)。ペンギンは Swimmer だけを名乗ります。飛ばせない鳥を渡す誤りは、動かす前に型チェックが見つけます。

例外を投げるだけの実装は、分けどきの合図

ペンギンの fly のように、「例外を投げるだけ」「中身が空」「何もしないで決まった値を返すだけ」のメソッドを見つけたら、インターフェースが大きすぎる合図です。書かされているのは、その実装にとって要らない約束だからです。何でも入ったインターフェースにはFat インターフェースという呼び名があり、避けるべき形とされています。Martin の1996年の記事も、結論で fat interface(太ったインターフェース)の欠点を挙げ、本来は切り離されているべき利用側どうしを結び付けてしまう、と書いています。

ペンギンの fly は、もう1つの原則にも反しています。前回読んだLSPです。Bird の並びを受け取った利用側は、「鳥なら飛べる」という約束に頼っています。そこへペンギンを渡すと、同じ Bird なのに例外が出て、約束が崩れます。例外を投げるだけの実装や空の実装は、ISP の問題であると同時に、LSP の問題でもあるのです。ISP で約束を分けておけば、ペンギンにそもそも fly が求められないので、LSP を破る余地も生まれません。

考えてみる:ペンギンの fly を、例外ではなく「何もしない」空のメソッドにすれば、問題は無くなる?

エラーは出なくなりますが、問題はむしろ見えにくくなります。flyAll はペンギンを「飛ばした」つもりで処理を終え、実際には何も起きていないのに、誰も気づきません。空の実装は、壊れていることを知らせる手がかり(例外)まで消してしまいます。直すべきは中身ではなく約束の大きさで、ペンギンには最初から fly を求めない形にするのが筋です。

COMMENTS
コメント…

このページ(1ページ目)の感想・質問・設計へのコメント

PASS IT ON

この気づきを、誰かにも。

この記事を書いた人

Takaomi Murasaki

Promari SNS Shareの開発を通して、Webの仕組みや設計の選び方を紹介しています。動く実装と、その判断に至るまでを記事に残しています。

公開記事 10 件

プロフィールを見る
THANK YOU FOR READING.すべての記事へ ↗

FROM INSIGHT TO IMPACT

「できたらいいな」を、
動く仕組みに。

記事で見つけたヒントを、あなたの事業へ。
新しいサービスも、手間のかかる業務も。
いまの課題から、つくるべきものを一緒に考えます。

開発・AI・研修の実績を見る

まだ、仕様書はいりません。

「何から始める?」から、ご一緒に。

課題がまとまっていなくても大丈夫。テーマを選ぶと相談文をご用意します。
連絡先の必須入力は、お名前とメールアドレスだけ。

まずは課題の整理から相談する

ご相談後の流れ

  1. 01 内容を確認
  2. 02 メールでご連絡
  3. 03 課題・進め方をご相談
送信だけで契約やお申込みが確定することはありません。