OCP――共有先を一つ足したときの差分を追う
作成: / 公開: / 内容更新:
執筆:tamito0201 / 掲載・運営:プロマリ
丸い共有ボタンの裏側を掘る連載の第5回。SOLID の2つ目の原則 OCP を、本物の SNS(Bluesky)の追加を通じて確かめます。名札カード1枚で済む変化と、共通処理のコードを変更することになる変化。差分を1行ずつ追いかけ、閉じる向きは変化の来やすさで選ぶことを、図22点で読み解きます。
何に対して閉じているのか
2ページ目と3ページ目では、Bluesky を足す作業を2つの段階に分けて行いました。ここで、その結果をまとめて眺めてみます。すると、「共有先を足す」と一口に言っても、どんな変化かによって、直す場所が大きく変わることが分かります。OCPの目で、変化を3つの段階に並べました。
段階1は、X や LINE と同じやり方で表せる新しい SNS です。名札カードを1枚書き、設定に1行足すだけで済み、共通処理のコード(web/src)は1行も変えませんでした。段階2は、今ある6つの値では足りない共有先です。Bluesky がそうで、「タイトル+記事の URL」という新しい値のために、共通処理(ドメイン層)の3ファイルを書き換えました。
段階3は、ボタンを押したときの「動き」そのものが新しい共有先です。今のプラグインの動きは、名札カードの action に書く3つだけです。open は共有画面を開く、copy は記事の URL をコピーする、native はスマートフォンの共有メニューを出す、という動きです。たとえば「その場で QR コードを表示する」のように、4つ目の動きが要る共有先がこれに当たります。段階3は実際には作らず、どこを直すことになるかを数えました。
| 段階 | どんな変化か | 直した場所(実測) |
|---|---|---|
| 段階1 | 新しい SNS(今ある動き・今ある値) | 名札カード1枚(10行)+設定1行+在庫表3行。web/src は0行 |
| 段階2 | 新しい値(共有文+記事の URL) | ドメイン層3ファイル(+7/−1)+生成器1行+説明書1行+テスト1件 |
| 段階3 | 新しい動き | 動きの種類で分かれている8か所(実装はせず、数えた見込み) |
表を見ると、段階が上がるほど直す場所が増えていきます。つまり、共通処理が「閉じている」、言いかえると「書き換えずに済む」のは、あらかじめ想定した種類の変化に対してだけなのです。段階1は想定の内側にありました。段階2と段階3は想定の外側にあり、来たら共通処理を書き換えることになります。OCP は「どんな変化にも閉じている」ことではなく、「どの変化に対して閉じるかを決めている」ことなのです。
段階の図は、上へ行くほど良い、下へ行くほど悪い、という順位表ではありません。どの種類の違いを表そうとしているかを並べたものです。必要な機能が段階3にあるなら、そこを実装すること自体は正しい仕事です。段階1へ無理に押し込めて、設定値に不自然な意味を持たせるほうが、あとで読みにくくなる場合もあります。
たとえば、QRコードを出したいのに、既存のopenとして特別なURL文字列を渡し、その文字列を見た表示側だけがQRコードへ変えるとします。見かけのactionは増えなくても、意味の違いが別の場所へ隠れただけです。新しい動きだと認めて、型、判断、実行、表示の対応を揃えるほうが、変更の意図を説明しやすくなります。これは設計の比較例で、この記事のプラグインに入れた処理ではありません。
反対に、新しいSNSの色が違うだけなのに、専用クラスと専用のクリック処理を作るのは、既存の口で表せる違いを大きく扱いすぎています。図のどの段へ置くかを考えると、追加の大きさに合った変更を選べます。小さく済ませることと、必要な違いを隠すことを、混同しないようにします。
この三段をチームの会話で使うなら、「新しい名前」「新しい値」「新しい動き」のどれかを先に言ってみてください。そのあとでファイル一覧を見ると、想定より広い変更になっている場所に気づけます。同じ『共有先追加』というチケット名でも、中身が違うことを説明する共通の目印になります。
動きの種類で分かれている8か所
段階3を実装しなかった代わりに、3つの動き(open・copy・native)がコードのどこで使い分けられているかを数えました。数えたのは、コードの中で「もし動きが copy なら…」のように、動きの種類を名指しで比べている場所です。
こうした分岐は、全部で8か所ありました。共通処理の中では、ルールを置くドメイン層に3か所、処理の手順を置くアプリケーション層に2か所、画面の表示を受け持つ見た目の層に2か所です。それに、名札カードを読む生成器にも1か所あります。たとえば、ボタンが押されたときに何をするかを決める部分は、次のように書かれています。
static decide({ action, popup }: { action: ShareAction; popup: unknown }): ShareActionDecision {
return action === ShareAction.Copy ? 'copy'
: action === ShareAction.Native ? 'native'
: popup ? 'popup' : 'follow';
}上から順に読みます。action(名札カードに書いた動き)が copy なら、’copy’(URL をコピーする)を返します。そうでなく native なら、’native’(スマートフォンの共有メニューを出す)を返します。どちらでもなければ open なので、小窓で開く設定(popup)なら ‘popup’、そうでなければ ‘follow’(ふつうのリンクとして開く)を返します。
新しい4つ目の動きを足すなら、この場所を含む8か所を確認し、「その動きのときはどうするか」を必要な箇所へ書き足すことになります。動きについては、名札カードのような差し込み口を作っていないのです。
八か所という数字は、将来の実装の変更行数を保証する数字ではありません。当時のコードで、動きの種類を比較していた箇所を調べた結果です。新しい動きの性質によっては、既存の分岐をそのまま使えるところも、分岐以外に追加が必要なところもあります。図の点は、まず読み直す候補として見てください。
この図を実際の変更へ使うときは、点を一つずつ「判断」「実行」「表示」「入力の検証」へ対応させると、抜けを見つけやすくなります。実行するコードだけを追加しても、入力として認める名前が増えていなければ届きません。逆に、入力だけ認めて実行の分岐が無ければ、何もしないまま通過してしまうおそれがあります。
閉じた値の一覧を使う設計では、種類を増やしたときに型検査が教えてくれる箇所もあります。しかし、型が扱わない設定ファイルや、文字列を調べる処理まで自動で網羅できるとは限りません。「一か所を直せば全部教えてもらえるはず」と期待せず、図の範囲と実際の検査が対応しているかを確認します。
これは作りの手抜きではありません。3つの動きは、それぞれブラウザの機能(新しい画面を開く・クリップボードに書き込む・端末の共有メニューを呼び出す)と1対1で結びついています。当時は、共有先の追加に比べて動きの追加は少ないと見立てていました。既存の機能を組み合わせた新しい動きが必要になる場合もあります。めったに起きない変化のために差し込み口を作っても、5ページ目で見る「差し込み口を持つための手間」だけが残ってしまいます。
もし、SNS の名前で分岐していたら
8か所と聞くと、多く感じるかもしれません。そこで、もしこのプラグインが「SNS の名前」で分岐していたら、どうなっていたかを比べてみます。「X ならこの URL、LINE ならこの URL、Facebook なら…」という分岐が、URL の組み立て・ボタンの色・ボタンに出す文字・アイコンのそれぞれに書かれている作りです。
URL の組み立てだけを取り出すと、たとえば次のような書き方です。
function shareUrl(sns: string, page: { url: string; title: string }): string {
const u = encodeURIComponent(page.url), t = encodeURIComponent(page.title);
if (sns === 'x') return `https://twitter.com/intent/tweet?url=${u}&text=${t}`;
if (sns === 'line') return `https://social-plugins.line.me/lineit/share?url=${u}&text=${t}`;
if (sns === 'facebook') return `https://www.facebook.com/sharer/sharer.php?u=${u}`;
// Bluesky を足すなら、ここに1行足すことになる
throw new Error(`知らない SNS です: ${sns}`);
}1行に1つの SNS があり、SNS の名前(sns)で比べて、それぞれの URL を返しています。短くて分かりやすい書き方ですが、SNS が1つ増えるたびに、この関数そのものを開いて1行足すことになります。1行足すだけでも、書き間違えれば、すでに動いている X や LINE の行まで壊すかもしれません。だから、足すたびに X や LINE も確かめ直す必要が出てきます。
この形は、OCP を説明するときの定番の例でもあります。よく使われるのは、会員のランク(ゴールド・シルバーなど)ごとにポイントの倍率を if で分ける例です。定番の直し方は、ランクごとにクラスを分けて「倍率を返す」という共通の約束(インターフェース)を持たせ、使う側はその約束だけを呼ぶ形にすることです。新しいランクは、新しいクラスを1つ足すだけで済みます。このように、変わる部分を差し替えられる部品として外に出す形は、Strategy パターンと呼ばれます。このプラグインの名札カードは、クラスの代わりに設定ファイルを使って、同じ形を作っているのです。
この作りだと、Bluesky を1つ足すたびに、そのすべての分岐に「Bluesky ならこう」を書き足すことになります。新しい SNS は、新しい動きよりずっと頻繁に増えます。そのたびに共通処理を書き換えることになってしまいます。
このプラグインは、よく増えるもの(SNS)の違いを分岐から追い出し、名札カードの値として外に出しました。そのうえで、めったに増えないもの(動き)の分岐だけを共通処理に残しています。分岐を全部なくしたのではなく、どの分岐を共通処理に残すかを選んだのです。分岐で書いていた場合の姿は、連載1.2の「もし、分岐で書いていたら」でも詳しく見ました。
SNS名で分岐する例と、actionで分岐する実物を比べるときは、if文の数だけに目を奪われないようにします。大事なのは、その条件へ新しい項目を足す頻度です。毎月増える共有先の名前を条件に持つのか、少数の動きの種類を条件に持つのかで、同じ一つのifでも書き換える回数が違います。
一方で、「分岐なら悪い、クラスにすれば良い」という読み方もできません。共有先ごとにクラスを作っても、使う側で全クラスを名指ししてnewする長い分岐が残れば、そこは追加のたびに変わります。登録や組み立てをどこへ集めるかまで見て、保ちたい手順が本当に新しい種類を知らずに済むかを確かめます。
このプラグインはデータを選びましたが、追加する振る舞い自体が共有先ごとに大きく違うなら、実行する部品を差し替える方式が合うかもしれません。設定だけで表すために複雑な条件式を増やし続けるより、コードとして別に持つほうが読みやすい場合もあります。どちらが正しいかは、何が増えるのかという問いへ戻って決めます。
閉じる向きは、選ぶもの
すべての変化に対して「書き換えずに済む」ようにすることは、できません。では、どの変化に備えればよいのでしょうか。答えは、変化の来やすさで選ぶ、です。
このプラグインで、いちばんよく来る変化は「新しい SNS を足したい」です。だから、その変化に備えて、名札カードという差し込み口を用意しました。一方、新しい動きのような、めったに来ない変化には備えていません。来たときに共通処理の8か所を書き換えれば十分だからです。よく来る変化の側にだけ差し込み口を開け、めったに来ない変化は来たときに直します。どの変化に備えるかを選ぶこと。これが、OCP を実際の設計に使うということです。
考えてみる:動きの種類にも、名札カードのような差し込み口を作ればよいのでは?
作ることはできます。たとえば「動きカード」を用意して、ボタンを押したときの処理を外から足せるようにする形です。けれど、動きはブラウザの機能(新しい画面を開く・クリップボードに書き込む・端末の共有メニューを呼ぶ)と1対1で結びついていて、ブラウザに新しい機能が増えなくても、既存の機能の組合せで新しい動きが必要になる場合があります。めったに来ない変化のために差し込み口を作ると、書き方の決まり・検査・作り直しの手順が増えるのに、それを使う日はほとんど来ません。来たときに8か所を直すほうが、全体としての手間は小さいのです。
一度広げた口を、次の追加で使えるか
想定の外の変化が来たら、OCPは役に立たなかったのでしょうか。そうとも限りません。次のフィルムは、一度だけ値の種類を広げ、その後は同じ値を別の共有先でも使う、という設計の育て方を描いた説明例です。
最初のコマでは、今ある値を選んで共有先を足しています。次のコマでは、その値では要件を満たせないので、共通の仕組みを広げています。最後のコマでは、広げた値の意味に合う別の共有先が来て、再び名札カードの追加へ戻っています。真ん中だけに工具があるのは、毎回同じ工事をする形から抜けられるかを見るためです。
ここで足した値が、最初の共有先の名前や細かな画面構成にべったり結び付いていたら、最後のコマでは再利用できないかもしれません。textWithUrlという名前なら何でも再利用できるわけでもなく、実際に「共有文とURLを空白でつなぐ」という同じ意味が必要かを確かめます。名前が一般的なことと、意味が共通であることは別です。
逆に、二つ目の利用例が来る前に、あらゆる区切り文字や省略規則を選べる仕組みへ広げる必要もありません。最初の要求に足りる小さな意味を定義し、その意味が次の要求にも合うかを見ます。合わなければ、無理に同じ名前へ押し込まず、どこが違うかを整理します。フィルムは将来の保証ではなく、変更を受けたあとの見直し方です。
この連載の土台である4.0.0のあとには、コピーして投稿画面を開くComposeという動きも加わりました。新しいブラウザAPIが現れなくても、既存の機能を組み合わせた新しい動きは必要になります。だから、この記事の「動きは増えにくい」は当時の見立てとして読み、永久の前提にはしません。実際の追加が見立てを外れたら、閉じる向きを考え直せます。
原則が役に立つのは、最初に未来を当てたときだけではありません。外れたあとに、何を想定していたか、どこを広げたか、次の追加はどこから入るかを説明できることにも価値があります。差分を段階ごとに残しておくと、この説明を印象ではなく、触った場所から始められます。











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