PROMARI JOURNAL

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

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

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

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

PASS IT ON

ひとつの発見を、次の会話へ。

分ける単位は、メソッドの数ではない

ここまでで、ISPは「利用側に、使わない能力を求めない」原則で、このプラグインではコピー・端末の共有・共有の窓・知らせ・ページの読み取りが別々のポート(口)になっていること、いいねの保存は口の外にあることを見ました。では、口はどこまで細かく分ければよいのでしょうか。

「メソッドを1つずつに分ければ、いちばん ISP にかなう」と考えたくなりますが、そうではありません。手がかりになるのが、端末の共有の口 NativeShareGateway です。

TypeScriptweb/src/domain/gateway/NativeShareGateway.ts(全体)
/** The device share sheet. Infrastructure implements it. */
export interface NativeShareGateway {
  readonly available: boolean;
  share(data: { title: string; url: string }): Promise<void>;
}

この口には、available(この端末で共有メニューが使えるか)と share(共有メニューを出す)の2つがあります。そして2ページ目で見たとおり、使う側が分かれています。available を読むのは画面を組み立てる部品で、share を呼ぶのはクリックの処理です。それぞれの利用側から見ると、もう片方は使わない項目です。

図5-1 2つの利用側が、1つの口の別々の項目を使う
図5-1 2つの利用側が、1つの口の別々の項目を使う。現在のしくみ。使う側が分かれていても、一緒に変わる1つの能力なら、分けないほうが読みやすいこともあります。図5-1 2つの利用側が、1つの口の別々の項目を使う。現在のしくみ。使う側が分かれていても、一緒に変わる1つの能力なら、分けないほうが読みやすいこともあります。

それでも、この口は分けていません。理由は3つあります。1つ目は、2つの項目が同じブラウザの機能(Web Share API)の表と裏だということです。「使えるか」と「使う」は、関連する能力として同じ実装にまとまっています。2つ目は、実装がどちらも1つのクラス(BrowserNativeShare)の数行で、片方だけを差し替えたい場面が無いこと。3つ目は、代役の手間がほとんど増えないことです。テストの代役は { available: true, share: async (d) => { … } } の1行で書けています。

分ける単位は、メソッドの数ではなく、利用側が一緒に必要とする能力と、一緒に変わる理由です。「一緒に変わる理由」は、連載2.1で読んだSRPの「依頼主」と同じ見方です。Web Share API の仕様という1つの依頼主が変えるものなら、1つの口にまとめておくほうが、読む人にも分かりやすくなります。

同じ根から分かれる二つの能力を、どこで切るか

NativeShareGatewayは、利用側が二つあるのに、口を二つへ分けていません。次の木の図では、availableとshareを別々の枝にし、どちらもWeb Share APIという根へつながる形にしました。

図5.1-1 二つの利用側と、同じブラウザ機能の根
図5.1-1 二つの利用側と、同じブラウザ機能の根。4.0.0で分けない判断の説明。同じ機能の確認と実行を、今は一つの口に保ちます。別々に差し替える負担が出たら、分ける理由を見直します。図5.1-1 二つの利用側と、同じブラウザ機能の根。4.0.0で分けない判断の説明。同じ機能の確認と実行を、今は一つの口に保ちます。別々に差し替える負担が出たら、分ける理由を見直します。

片方の枝では、画面を組み立てる人が「共有の機能があるか」を読みます。もう片方では、クリックの処理が「共有を実行して」と頼みます。使う人は違いますが、今の実装では同じブラウザ機能を薄く包んだ二つの能力です。枝の違いだけで切り離すか、共通の根まで一緒に理解するかを比べます。

ただし、「同じブラウザ機能だから、必ず同時に変わる」とまでは言えません。availableの判定だけを変えたくなる日も、shareの失敗の扱いだけを変えたくなる日もあります。図の根は、今のまとまりを説明するものです。二つが永遠に同時変更されるという、履歴の証明を描いているわけではありません。

また、availableがtrueでも、共有の呼び出しが必ず成功するわけではありません。これはブラウザに機能があるかを調べる側と、その機能を使って結果を受け取る側の違いです。ボタンを出す条件と、クリック後の成功を混ぜると、小さな口でも約束を誤解します。前回のLSPの目で、口の各項目がどこまでを約束するかも確認します。

木の図へ、新しい利用側を一つ描き足してみましょう。共有ボタンを表示せず、対応機能の一覧だけを作るページなら、availableだけを読みたいかもしれません。そこでshareの代役を毎回用意する負担が増えたら、確認だけの小さな口を見せる理由になります。

逆に、今ある二つの利用側では、共通の実装と短い代役で十分なら、枝を増やす前にその理由を残せます。どちらの答えも、図の枝の本数だけでは決まりません。利用側を一人ずつ置いて、必要な能力へ線を引くことが、口を分ける判断の出発点です。

今は、二つを一緒にした代役が短く書けて、別々に差し替えたい要求も出ていないため、分けない判断ができます。将来、画面の組み立てだけを別環境で使い、共有の実行を用意する負担が増えたら、そのときは枝を分ける理由が生まれます。図の切れ目を固定せず、具体的な負担が変わったときの見直し先として残しておきます。

分けすぎの代償

口を1つ分けるたびに、増えるものがあります。このプラグインで、コピーの口1つのために書かれている場所を数えてみました。

図5.2-1 1つの口は、4か所に書かれる
図5.2-1 1つの口は、4か所に書かれる。現在のしくみ(数えた結果)。口は、分けるほどつなぐ場所が増えます。分けすぎると、どこで何がつながっているかを追うのが大変になります。図5.2-1 1つの口は、4か所に書かれる。現在のしくみ(数えた結果)。口は、分けるほどつなぐ場所が増えます。分けすぎると、どこで何がつながっているかを追うのが大変になります。

口のインターフェース(domain/gateway/ClipboardGateway.ts)、ブラウザの実装(infrastructure/BrowserClipboard.ts)、DI コンテナで使う名札(composition/InjectionTokens.ts の37行目)、実装を結び付ける行(composition/ShareContainer.ts の48行目)の4か所です。テストの代役を入れると、もう1か所増えます。

もし NativeShareGateway を available と share の2つの口に分けたら、同じ方式で別々に登録するなら、名札や結び付ける行などが増え、渡すものも見直します。実装クラスまで必ず二つに増やす必要はありません。細かく分けすぎると、「どの口がどこにつながっているか」を追うだけで手間がかかり、初めて読む人が迷います。分けることは、それ自体が目的ではありません。使わない能力に巻き込まれる困りごとが、つなぐ手間より大きいときに分けるのです。

四か所の図は、このリポジトリで一つの口を組み立てている場所を数えたものです。すべてのプログラムで、口を分けるたびに四ファイルが必ず増えるという公式ではありません。同じ実装クラスで二つの口を満たしたり、コンストラクタで直接渡したりする設計なら、増える場所は違います。数を使うときは、どの組み立て方を前提にしたかも残します。

分割の費用は、ファイル数だけでも決まりません。名前を覚える手間、接続を探す手間、テストで渡す値をそろえる手間もあります。一方、口を広いままにした費用には、使わない項目を埋める手間や、無関係な変更に追従する手間があります。図の四つの場所と、前のかばんの余分な六項目を並べると、何と何を比べる判断なのかが見えます。

最初に試す変更は、大がかりな分割でなくてもかまいません。ある利用側の引数の型を、本当に使う能力だけへ絞るだけで、不要な実装義務を外せることもあります。ただし、この回の実物はその変更を行っていません。分ける案を考えるときに、実装のクラスまで増やす以外の選択肢もある、という検討の順番です。

いつ口を分けるか

分けるかどうかは、次の3つの問いを順にたどって決めます。

図5.3-1 いつ口を分けるか
図5.3-1 いつ口を分けるか。判断の目安。分けるきっかけは、使わない能力が代役や変更に現れたとき。分ける前に、つなぐ手間とも比べます。図5.3-1 いつ口を分けるか。判断の目安。分けるきっかけは、使わない能力が代役や変更に現れたとき。分ける前に、つなぐ手間とも比べます。

1つ目は、ある利用側のために代役を書くとき、「例外を投げるだけ」や「空」の項目が並んでいないか。2つ目は、使わない能力の変更で、関係のないテストや実装を直すことになっていないか。この2つは、「分ける必要がある」という合図です。3つ目は、分けても、登録・接続・代役を書く負担が増えすぎないか。これはブレーキです。1つ目か2つ目のどちらかに負担があれば、3つ目で、つなぐ手間より巻き込まれずに済む利点が大きいかを比べます。大きいなら分ける候補にします。困りごとがまだ起きていないのに、先回りして口を細かくしないためです。

SOLID の原則は、いつでも最適な答えをくれるわけではありません。参考にした Zenn の本も、原則を守ることに固執すると、かえって読みにくくなったり開発の効率が落ちたりすることがある、と注意しています。ISP で言えば、「分けること」ではなく「利用側が要らないものに頼らされていないこと」が目的です。分けるかどうかは、困りごととつなぐ手間を比べて決める。

三つの問いは、順番に機械判定するルールというより、困りごとと費用を言葉にするための目安です。空の実装が無くても、無関係な変更に何度も巻き込まれているなら、分ける候補になります。反対に、使わない項目が少しあっても、そのままのほうが約束を理解しやすい場合はあります。最初の二つの合図が必ず両方そろうまで、待つ必要はありません。

今すぐ分けない場合も、理由を残せます。「同じ能力の確認と実行で、代役は一行。別々に変更する要求が出たら再検討する」と書けば、保留は具体的な判断になります。「SOLIDだから細かくする」「今は動くから考えない」の二択にせず、どんな変化が来たら判断を変えるかまで決めておくと、次の人が続きを考えられます。

考えてみる:ClipboardGateway の fallback?(無くてもよい)も、別の口に分けるべき?

今は分けなくてよいと考えます。fallback を使うのは、write を使うのと同じクリックの処理で、しかも「コピーに失敗したとき」という同じ場面です。一緒に必要とされ、一緒に変わる能力です。そのうえ ? を付けて「無くてもよい」にしてあるので、fallback を試さない代役は write だけを書けば済み、使わない中身を書かされることもありません。無くてもよい項目にしておくのは、口を分けるよりも軽い、ISP の小さな当て方の1つです。

任意にすることが役立つのは、その項目が無くても契約として意味が通る場合です。fallbackが無いときは、クリックの処理はそれを呼ばず、copy-fallbackという結果を返せます。一方、必ず必要なwriteまで任意にしてしまうと、コピーの処理は書き込みの能力があるかを毎回調べなければなりません。全部に?を付けることは、分離の代わりにはなりません。

項目を任意にするときは、「省略されたとき、使う側は何をするか」を一緒に書きます。今回は引用したfallback?.という呼び出しが、その答えの一部です。口の見た目を小さくするために省略可能にするのではなく、省略した状態も正しく扱える約束にする。この順番なら、型の都合で曖昧な実装を増やさずに済みます。

COMMENTS
コメント…

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

PASS IT ON

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

この記事を書いた人

Takaomi Murasaki

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

公開記事 11 件

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

FROM INSIGHT TO IMPACT

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

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

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

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

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

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

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

ご相談後の流れ

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