PROMARI JOURNAL

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

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

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

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

PASS IT ON

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

いいねの保存は、どの口にも入っていない

前のページの実験では、いいねの保存(saveLike)を全部入りのインターフェースに入れたせいで、コピーの代役にまで中身が要りました。では、実物のプラグインでは、いいねの保存はどのポート(口)にあるのでしょうか。答えは「どの口にも無く、プラグインの中には無い」です。

共有欄の丸いボタンの隣には、いいねのボタンを出せます(variant=”circle” と like の属性を付けたとき)。けれど、いいねの件数をサーバーに保存する処理は、プラグインに入っていません。プラグインが受け持つのは、ボタンを表示して、押されたことを知らせるところまでです。いいねのボタンが押されたときのコードは、次のとおりです。

TypeScriptweb/src/presentation/PromariSnsShareElement.ts(118〜123行目)
root.querySelector('.like')?.addEventListener('click', () => {
  if (this.#likeState.busy) return;
  this.#likeState.busy = true;
  this.#updateLike();
  this.dispatchEvent(new CustomEvent('promari-sns-share-like', { bubbles: true, composed: true, detail: { liked: !this.#likeState.liked } }));
});

1行目で、いいねのボタン(.like)が押されたときの処理を登録します。2行目で、保存の返事を待っている間(busy)は何もしません。3〜4行目で、待っている状態にして、ボタンを押せなくします。5行目で、promari-sns-share-like という名前のカスタムイベントを出します。detail.liked に入るのは、「いいねにしたい」「外したい」という希望の状態です。保存が済んだという知らせではありません。

知らせを受け取ったページ側のプログラムが、サーバーへ保存し、返ってきた件数を element.setLikeState({ liked, count, busy: false, message: ” }) でボタンに戻します。この分担は、プラグインの説明書(README.md の Circular sharing with likes)に書かれています。同じ箇所には、公開するライブラリには保存先の住所も認証情報も入れていない、ともあります。

図4-1 いいねは知らせを出し、保存はページ側が受け持つ
図4-1 いいねは知らせを出し、保存はページ側が受け持つ。現在のしくみ。プラグインは、ボタンを出して知らせるところまで。保存はページ側の仕事なので、保存の口は要りません。図4-1 いいねは知らせを出し、保存はページ側が受け持つ。現在のしくみ。プラグインは、ボタンを出して知らせるところまで。保存はページ側の仕事なので、保存の口は要りません。

この形だと、いいねの保存のための口は、プラグインのどこにも要りません。だから、コピーの代役にも、クリックの処理のテストにも、いいねの話は出てきません。保存の仕方(どのサーバーに、どんな認証で保存するか)が変わっても、プラグインのコードは変わりません。いいねは知らせを出すところまで。保存はページ側に任せる。

「保存してほしい」という依頼と、保存済みの返事を分ける

いいねの図は、送る知らせと、返ってくる状態の二つを分けると読みやすくなります。次の図では、押した直後に出すものを依頼の封筒、保存後に戻すものを結果の受領票として描きました。

図4.1-1 保存の依頼と、確定した結果の往復
図4.1-1 保存の依頼と、確定した結果の往復。4.0.0のいいねの分担を表す模型。押した直後は希望を送り、保存の結果をページ側から戻します。コピーの口は、この往復を担当しません。図4.1-1 保存の依頼と、確定した結果の往復。4.0.0のいいねの分担を表す模型。押した直後は希望を送り、保存の結果をページ側から戻します。コピーの口は、この往復を担当しません。

封筒に入っているlikedは、利用者が次にしたい状態です。いいねを付けたいのか、外したいのかをページ側へ伝えています。この封筒を送った時点では、サーバーの保存が成功したとは限りません。だから、図の受領票にある確定件数は、まだ封筒へは書かれていません。

ページ側は保存を行い、返ってきた結果をsetLikeStateでボタンへ戻します。そこでlikedとcountを更新し、busyをfalseへ戻せます。図の往復は、単に同じbooleanを送り返す話ではありません。希望を送る段階と、結果を受け取る段階に、それぞれ別の意味があります。この区別があるから、待っている間の見た目も説明できます。

保存に失敗した場合も、ページ側が待機状態を解き、利用者へ結果を伝える必要があります。この記事の引用部分は、保存に成功したときの分担を説明するものです。失敗の表示や再試行の方針まで、プラグインが自動で引き受けるわけではありません。必要な案内をサイト側で実装するという、境目の外に残る仕事も忘れずに見ます。

保存先のURLや認証方法をページ側へ置けるのは、この分担の利点です。ただし、イベント名やdetailの形、setLikeStateの受け取り方まで変えたら、ページ側との接続も見直します。「保存方法を変えてもプラグインは変わらない」は、受け渡しの約束を保てる範囲での話です。境界を置いたことと、境界の契約が不要になったことは違います。

封筒の例で、いいねを外す場面もたどってみます。ボタンを押した人の希望はliked:falseです。これを受けたページ側が保存し、確定した状態と件数を戻します。図の封筒にfalseが入ったことは、失敗の意味ではありません。何を希望したかという値と、保存が成功したかという結果は、別々に読む必要があります。

同じbooleanでも、意味を省くと混乱します。liked:falseとbusy:falseは、どちらもfalseですが、一方はいいねの状態、もう一方は待機中ではないことです。口の項目名と、その項目を誰が決めるかを一緒に見ると、ページ側とプラグイン側の分担がはっきりします。

この往復を一つずつ説明できれば、保存をどこへ置くかの議論も具体的になります。サーバーを替えても、希望を受け取り、確定状態を返す約束を保てるなら、ボタン側の仕事は変わりません。変えたいのが保存先なのか、希望や結果の意味なのかを先に分けると、口を広げる必要がある変更だけを選べます。

この図をコピーの口へ戻して考えると、いいねの保存を足さない理由がはっきりします。コピーの利用側は、この封筒も受領票も扱いません。機能が同じ共有欄に並んでいることと、同じ約束へ入れることは別です。画面の近さではなく、どの処理がどの往復を担当するかで、口の範囲を決めています。

もし、知らせの口にいいねの保存を足したら

反対に、実物の口にいいねの保存を入れたらどうなるかも試しました。押されたことを知らせる ShareActivityPublisher は、いいねと同じく「ページ側へ伝える」口なので、ついでにここへ入れたくなる場所です。作業用のコピーで、ShareActivityPublisher の publish の下に saveLike(liked: boolean): Promise<void>; の1行を足し、型チェックにかけました。

Shell型チェック(tsc)の結果(知らせの口に saveLike を1行足した作業コピー)
$ npx tsc -p tsconfig.json
web/src/composition/ShareContainer.ts(41,72): error TS2741: Property 'saveLike' is missing in type 'CustomEventShareActivityPublisher' but required in type 'ShareActivityPublisher'.
web/src/infrastructure/CustomEventShareActivityPublisher.ts(7,14): error TS2420: Class 'CustomEventShareActivityPublisher' incorrectly implements interface 'ShareActivityPublisher'.
  Property 'saveLike' is missing in type 'CustomEventShareActivityPublisher' but required in type 'ShareActivityPublisher'.
web/test/application.test.ts(60,7): error TS2741: Property 'saveLike' is missing in type '{ publish: (d: ShareActivity) => number; }' but required in type 'ShareActivityPublisher'.

型チェックが止めたのは3か所です。1つ目は、組み立てを受け持つ ShareContainer.ts の41行目で、知らせのブラウザの実装を作る場所です。2つ目は、その実装 CustomEventShareActivityPublisher で、どちらも「saveLike を実装していない」というエラーです。3つ目が、前のページで見たテストの代役の束の60行目、知らせの代役です。この束は、コピーのテストでも使われます。いいねのために1行足しただけで、コピーのテストの土台を直すことになりました。一方、コピーの代役(57・79・89行目)は、1つもエラーになっていません。

図4.2-1 知らせの口に保存を足すと、3か所が止まる
図4.2-1 知らせの口に保存を足すと、3か所が止まる。実測(一時作業コピーでの実験)。知らせといいねの保存は、一緒に使われない能力です。同じ口に入れると、関係のない代役まで直すことになります。図4.2-1 知らせの口に保存を足すと、3か所が止まる。実測(一時作業コピーでの実験)。知らせといいねの保存は、一緒に使われない能力です。同じ口に入れると、関係のない代役まで直すことになります。

ここから分かるのは、同じ「ページ側へ伝える」でも、知らせといいねの保存は別の能力だということです。知らせは、押されたことを伝えて終わりで、返事は待ちません。いいねの保存は、サーバーに書き込み、確定した件数の返事を待ちます。そして、クリックの処理は知らせを必ず使い、いいねの保存は一度も使いません。一緒に必要とされない能力は、同じ口に入れない。これがISPの分け方です。

知らせの口へsaveLikeを足した実験では、コピーの代役そのものと、コピーのテストが使う束を分けて見ます。ClipboardGatewayのwriteには新しい項目は要りませんでした。しかし、束に入っているactivityの代役が、新しい約束を満たさなくなりました。影響が届く道は、直接の実装先だけではないことが分かります。

三つのエラーは、どれも同じ追加から始まっています。組み立てる場所、ブラウザの実装、テストの代役という三つの面で、契約が広がった影響を見ています。「エラーが三つだから修正も簡単」と片付けず、なぜ保存と関係のないテストの準備まで変わるのかを読むと、ISPの問題が具体的になります。

考えてみる:いつか、いいねの保存をプラグインの中に入れる日が来たら?

そのときは、いいねのボタンの処理だけが使う、別の小さな口を作るのが ISP にかなった形です。クリックの処理と、その代役の束には何も足しません。そうすれば、3ページ目の実験の「分けた形」のように、いいねの口のメソッドが増えても、コピーのテストには届きません。ただし、これは今のプラグインには無い、将来の選択肢の話です。保存先をサイトごとに選べるようにする必要が出たときに、改めて考えます。

将来、保存をプラグインへ入れるなら、口を一枚足せば終わりではありません。保存の成功と失敗、待っている間の状態、二度押しの扱い、件数をどこから受け取るかなど、使う側が頼る約束を決める必要があります。これは実装済みの機能の説明ではなく、別の口を設計するときの検討例です。

その検討でコピーが登場しないなら、コピーの契約へ保存を入れる理由もありません。反対に、複数の機能が同じ結果を一緒に必要とする場面が見つかったら、その利用側に渡すまとめを考えます。先に「一機能一口」と数を決めず、必要な呼び出しと返事を並べてから、まとめる範囲を選びましょう。

COMMENTS
コメント…

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

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 課題・進め方をご相談
送信だけで契約やお申込みが確定することはありません。