OCP――共有先を一つ足したときの差分を追う
作成: / 公開: / 内容更新:
執筆:tamito0201 / 掲載・運営:プロマリ
丸い共有ボタンの裏側を掘る連載の第5回。SOLID の2つ目の原則 OCP を、本物の SNS(Bluesky)の追加を通じて確かめます。名札カード1枚で済む変化と、共通処理のコードを変更することになる変化。差分を1行ずつ追いかけ、閉じる向きは変化の来やすさで選ぶことを、図22点で読み解きます。
検証:本物の SNS を1つ足してみる
OCPにかなっているかどうかは、実際に1つ足してみるのがいちばん確かです。1ページ目で見たとおり、このプラグインでは、共有先ごとに名札カードという設定ファイルを1枚書きます。OCP にかなった作りなら、新しい SNS も名札カードを1枚足すだけで済むはずです。連載1.2では架空の共有先で数えましたが、今回は本物の SNS、Bluesky を足してみます。Bluesky には、ほかのサイトから投稿画面を開くための公式の入口があり、共有ボタンの題材にちょうどよいのです。
検証は、次の順で進めました。まず、プラグインのリポジトリをgit worktreeで作業用のコピーに写しました。公開しているプラグインには何も入れていません。次に、何も足していない状態でテストを流し、結果を記録しました。Node のテストが63件、Python のテストが19件で、どちらもすべて通りました。Node のテストは、ブラウザで動く部分(URL の組み立て、ボタンの並び、クリックしたときの動き)を確かめるものです。Python のテストは、名札カードを読む生成器を確かめるものです。そのうえで Bluesky の名札カードを1枚足し、設定に1行足して、もう一度テストを流します。最後に、変わったファイルと行数を数えます。
足す前の結果を先に取っておくのには、理由があります。足したあとにテストが落ちても、それが足したせいなのか、もともと落ちていたのかが分からなければ、差分を正しく数えられないからです。変更前の状態を記録しておくことで、追加の影響を切り分けて確かめられます。
実験の順番も、図を一段ずつ読んでみましょう。最初の結果は、あとで「もともと動いていたもの」と比べるためのものです。次の追加では、まず名札カードと設定だけを変えます。そこで共通処理まで一緒に直すと、「今の口だけで足せたのか」を試せなくなるからです。足りないところが見つかってから、別の段階として仕組みを広げます。
この順番を保つと、失敗も説明の材料になります。最初からtextWithUrlまで作ってしまえば、「Blueskyを追加できた」という結果だけが残り、既存の六つの値ではどこが足りなかったのかが見えません。小さい追加で一度止めることで、表現できる範囲とできない範囲を実際の出力から見つけられます。
手元で再現する場合も、公開中のプラグインへ直接書かず、版を固定した作業コピーで試します。既存の設定を退避し、どの設定ファイルを読むコマンドなのかを先に確かめてください。この連載の件数は当時の版の記録なので、別の版で件数が違うこと自体を失敗とはしません。比較する前後が同じ土台であることを優先します。
Bluesky の名札カードを書く
Bluesky の公式ドキュメントによると、投稿画面の入口は https://bsky.app/intent/compose で、渡せる値は text の1つだけです。これに合わせて、名札カードを1枚書きました。
同じドキュメントには、共有ボタンを作るうえで知っておきたいことが、ほかにも書いてあります。投稿は300字まで(正確には、人の目に1文字に見えるまとまりで300個まで)であること。ログインしていない人は、まずサインインを求められること。そして、入口を開いても投稿はすぐには行われず、本人が内容を確かめてから投稿することです。共有ボタンは投稿欄を埋めるところまでを受け持ち、投稿するかどうかは、押した人が決めます。これは X や LINE の共有画面と同じ分担です。
# Bluesky の投稿画面(検証用)。
key = 'bluesky'
label = '投稿'
brand_color = '#1185FE'
action = 'open'
endpoint = 'https://bsky.app/intent/compose'
icon = '<svg viewBox="0 0 24 24" …>…</svg>'
[params]
text = 'text'X の名札カードと並べると、違いがよく分かります。違うのは値だけです。key は bluesky、色は Bluesky の青、endpoint は Bluesky の投稿画面、[params] は text = ‘text’ の1行だけです。Bluesky が受け取る名前は text しかないので、そこに共有文を入れる、という意味になります。項目の種類は、X と同じ7つのままです。名札カードの書き方の型そのものは、1文字も変えていません。
宛名と中身は、同じtextでも別の役目
名札カードは短いぶん、同じ名前が並ぶと、かえって読み違えやすくなります。text = 'text'もその一つです。次の図は、SNSへ送る荷物にたとえて、宛名の欄と中身を選ぶ札を離して描きました。
箱の表に付くtextは、Blueskyが受け取る項目名です。箱へ何を入れるかを決めるtextは、このプラグインの値の名前です。たまたま文字が同じでも、片方は相手の呼び方、片方は送り手の呼び方です。Xのurl = 'url'も同じ二役を持ち、via = 'via'にも同じ読み方を当てられます。
図でタイトルの札だけが箱へ入っているのは、この段階のtextが記事のURLを含まないからです。URLの札は手元にあっても、自動では箱へ入ってくれません。設定ファイルにtextという文字があるだけで、「タイトルもURLもよい感じに送ってくれる」と読むと、ここを見落とします。相手の欄へ、実際にどんな一つの文字列が入るかまで追う必要があります。
それでは右側をurlへ変えればどうでしょうか。text = 'url'なら、相手のtext欄へ記事のURLを送る形は表せます。ただし、今度はタイトルを同じ欄へ一緒に入れられません。「URLだけでよい」という要件なら候補になりますが、今回確かめたいのは「共有文とURLの両方」です。何が不足しているかを、要件まで戻って説明します。
左右を意識して書く練習として、対応表を日本語で読み上げてみてください。「Blueskyのtext欄に、プラグインが持つtextの値を入れる」。右側に架空の名前を書いたときは、その値を作る場所がありません。左側に相手の知らない名前を書けば、プラグインがURLを作れても、相手が意図どおり受け取るとは限りません。それぞれ別の確認が必要です。
この図が分かると、あとで出てくるtext = 'textWithUrl'の変更も読みやすくなります。相手の宛名はtextのままです。変えるのは、そこへ詰める中身の選び方だけ。その新しい中身を作るために、送り手の側でどこを広げるかが、次の段階の話になります。
説明書どおりの3つの手順
このプラグインの説明書(docs/customization.md)には、共有先を足す手順が書いてあります。やったことは、この手順どおりです。
説明書の手順は、図2.3-1 のとおり3つです。
① 名札カードを書く:destinations フォルダに bluesky.toml を1枚作ります。中身は、上で見た10行です。
② 設定に並べる:サイトの設定ファイルに、bluesky を表示する共有先として書き足します。設定ファイルには、大きなボタンで並べる共有先の一覧(destinations)と、その後ろに小さなアイコンで並べる共有先の一覧(secondary)の2つがあります。今回は secondary に bluesky = true の1行を足しました。
③ 作り直す:生成器の python3 tools/config.py –write を実行します。このコマンドは名札カードと設定ファイルを読み、ブラウザが読み込む JavaScript のファイル(promari-sns-share.min.js)を作り直します。名札カードを足しても、この作り直しをしないとボタンには出ません。
3つの手順で触るのは、名札カード、設定ファイル、自動で作られる JavaScript のファイルだけです。URL を組み立てる共通処理のコードは、開くことすらありません。「共有先を1つ足す」という作業が、共通処理を書き換えずに終わるように作られている。これが、このプラグインが共有先の追加に対して「閉じている」ということです。
三つの手順を、完成するものから逆向きにたどることもできます。ブラウザで新しいボタンが見えなければ、まず作り直した配布物を読んでいるかを見ます。配布物に新しい共有先が無ければ、表示対象の設定と名札カードを見ます。カードを書いたという事実だけで、読者の画面へ届いたと判断しない順番です。
手順が複数あることは、OCPへの違反ではありません。保ちたいのは共通の手順であって、追加作業を一回のクリックにすることではないからです。名札カードを作る、採用する設定を足す、生成する、出力を確かめる。この一連の作業を説明書に残しておくことも、差し込み口を使える状態で保つための仕事です。
また、生成と型検査とテストは、確認するものが違います。生成できたことは、設定が読めたことや出力を作れたことを示します。型検査はコードの型の整合を見ます。テストは書いた具体例について結果を見ます。一つのコマンドが通っただけで、三つをすべて確認したつもりにならないように、実行したコマンドと分かったことを対にして記録します。
1枚足したときの差分
では、実際に差分はどこに出たのでしょうか。図2.4-1 では、変わった場所を、その変更を頼んでくる人(依頼主)ごとに分けて並べています。新しい SNS の情報は destinations、サイトの設定は config、テストは tools/tests、共通処理は web/src というフォルダにあります。
名札カードを足して作り直した時点で、変わったのは2つのファイルだけでした。git diff の結果を、ファイルの場所と位置を示す行だけ短くして載せます。行頭の + が足した行、- が消した行、何も付いていない行は、前後がどこかを示すための変わっていない行です。
--- a/config/share_config.example.toml
+++ b/config/share_config.example.toml
@@ [share.secondary] @@
email = true # Send by email (mailto).
copy = true # Copy the URL to the clipboard; falls back to a link without JavaScript.
+bluesky = true # Bluesky(検証用)。
native = true # Native share sheet (Web Share API); shown only where navigator.share is available.
--- /dev/null
+++ b/destinations/bluesky.toml
@@ 新しいファイル @@
+# Bluesky の投稿画面(検証用)。
+key = 'bluesky'
+label = '投稿'
+brand_color = '#1185FE'
+action = 'open'
+endpoint = 'https://bsky.app/intent/compose'
+icon = '<svg viewBox="0 0 24 24" …>…</svg>'
+
+[params]
+text = 'text'設定ファイルには bluesky = true の1行が足され、新しい名札カード bluesky.toml は10行がまるごと足されました。消した行(-)は1つもありません。共通処理を含むブラウザ側のコード(web/src)には、1行の差分も出ていません。そして Node のテスト63件は、1件も書き換えずにすべて通りました。X や LINE のボタンが正しい URL を作ることも、並び順も、クリックしたときの動きも、変わっていないということです。
落ちたテストは、在庫表
ただし、Python のテストは3件落ちました。テストの出力を見てみます。
$ python3 -m unittest discover -s tools/tests
FAIL: test_catalog_order_follows_file_names
FAIL: test_extracts_all_bundled_destinations
FAIL: test_example_is_valid
AssertionError: Items in the first set but not the second:
'bluesky'FAIL: のあとに並んでいるのが、落ちた3件のテストの名前です。最後の2行は、「比べた2つの一覧のうち、片方にしか無いものがある。それは ‘bluesky’ だ」という意味です。つまり、テストに書いてある共有先の一覧には bluesky が無いのに、実際の共有先には bluesky が増えていた、ということです。
3件とも、共有先の一覧を丸ごと期待値に書いたテストでした。「共有先は copy・email・facebook・hatena・line・linkedin・native・x の8つ」と書いてあるところに、9つ目の bluesky が現れたので、「一覧に無いものがある」と教えてくれたのです。この記事では、この種のテストを在庫表と呼びます。
在庫表に1行足すのは、閉じた共通処理を変更したことにはなりません。お店で新しい商品を仕入れたら、棚卸し表に1行足すのと同じです。むしろ、在庫表が落ちたことは、「共有先が1つ増えた」という事実を機械がきちんと見ていた証拠です。3件の期待値に bluesky を1行ずつ足すと、Python のテスト19件もすべて通りました。
考えてみる:在庫表のテストは、足すたびに直すことになる。OCP に反していない?
反していません。OCP が閉じておきたいのは「すでに動いているコードの振る舞い」です。在庫表は「いま何が並んでいるか」を書いた一覧で、並んでいるものが増えれば一覧も増えるのが正しい動きです。むしろ一覧のテストがあることで、名札カードを足し忘れたり、消してしまったりしたときに気づけます。直すのは一覧の1行だけで、X や LINE の URL を確かめるテストのような、振る舞いの期待値は1つも書き換えていないことが大事なのです。
在庫表の図では、新しい札が棚に増えたことと、前からある札の中身が変わっていないことを、別々に見ています。追加が目的の作業では、一覧が増えることは期待どおりです。一方、Xの共有URLが変わることは、今回の目的に含まれていません。どちらも「期待値との差」ではありますが、変更してよい理由が違います。
テストが落ちたら、まずテスト名と比較している値を読みます。「共有先の集合」なのか、「特定の共有先が作るURL」なのか。「新しいものが一つ増えた」ことを伝えているのか、「以前と同じ入力なのに結果が変わった」ことを伝えているのか。この読み分けができると、赤い表示を消すためだけの修正を避けられます。
ここで、在庫表を生成物からそのまま作る案にも注意が要ります。出力と同じ一覧を期待値として読み込めば、二つはいつも一致しやすくなります。しかし、間違った共有先を追加したことや、必要なものを削除したことに気づけなくなるかもしれません。在庫表が何を守るためにあるかを確かめてから、保守の手間を減らす方法を選びます。
「テストを書き換えたらOCP違反」「既存テストが緑なら追加成功」という二択にはしません。意図して増やしたものは、その意図に合わせて期待値を更新します。保つと決めた振る舞いの期待値は維持し、新しい機能の目的を確かめるテストを追加します。この三つを分けると、テストの変更も設計の説明の一部になります。















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