OCP――共有先を一つ足したときの差分を追う
作成: / 公開: / 内容更新:
執筆:tamito0201 / 掲載・運営:プロマリ
丸い共有ボタンの裏側を掘る連載の第5回。SOLID の2つ目の原則 OCP を、本物の SNS(Bluesky)の追加を通じて確かめます。名札カード1枚で済む変化と、共通処理のコードを変更することになる変化。差分を1行ずつ追いかけ、閉じる向きは変化の来やすさで選ぶことを、図22点で読み解きます。

こんにちは、プロマリの紫です。連載「丸いボタンの裏側」の第5回、第2章の2回目です。SOLID は、変更に強いコードを書くための5つの原則を、頭文字でまとめた呼び名です。前回はその最初の原則 SRP を読み、「責任とは、変更を頼んでくる人の数」だと確かめました。今回は2つ目の原則、OCPです。テーマは足すときに、すでに動いているコードを直さずに済むかということ。本物の SNS を1つ実際に足し、そのときの差分を1行ずつ追いかけます。
点線の付いた用語は、その言葉を押すと詳しい説明が開きます。意味、身近なたとえ、この実装での使い方を順に読めます。キーボードではTabで用語へ移動し、EnterまたはSpaceで開き、Escapeで本文へ戻れます。各ページで最初に出る用語から参照できるようにしました。
この記事のコードと図の土台は、promari-sns-share 4.0.0の公開ソースです。既存の実験の件数・差分は執筆時の記録として残し、今回加えた場面は「説明例」「練習例」と区別しています。現在の4.2.2にはComposeや下書きの対応もあるため、ここに載せた操作の種類や行数を、現在版の一覧としては読まないでください。各原則を、同じ実装を土台にして一つずつ確かめるための版の固定です。
この記事は連載「丸いボタンの裏側」の第5回(2.2)です。前回の2.1「SRP――一つの共有先の定義は、一つの理由で変わるか」で、依頼主ごとに置き場所を分けた形を先に読むと、今回の話がつながりやすくなります。連載全体の地図は、本編5ページ目の連載の目次からどうぞ。
この回は6ページあります。1ページ目で OCP の意味を確かめ、2〜3ページ目で Bluesky を実際に足して差分を追い、4ページ目で「どんな変化なら、今のコードを書き換えずに足せるのか」を整理し、5ページ目で、それをテストで確かめる方法と、備えるための手間を考え、6ページ目で前回の SRP とのつながりと宿題に進みます。図は26点です。既存の実測と、今回加えた読み方の説明例を区別して載せています。段階3の変更箇所は、実装せずに調べた見込みです。
足すときに、すでにあるコードは直さない
OCP は、日本語では「開放閉鎖の原則」と訳されます。開いているのに閉じている、という言い方は、最初は矛盾して聞こえます。この原則が言っているのは、機能を足すことには開いていて、すでに動いているコードを書き換えることには閉じている、という形です。
身近なもので言えば、延長タップです。新しい家電を買ってきたら、空いた口に差せば使えます。壁の中の配線を開け直したり、ブレーカーを付け替えたりはしません。壁の配線は「閉じて」いて、延長タップの口は「開いて」います。
共有ボタンでいえば、X・LINE・Facebook が差さった延長タップに、新しく Bluesky を差す場面です。OCP にかなった作りなら、既存の値と動きで表せる範囲ならBluesky のプラグを差す形で済み、X や LINE のために書いたコードは書き換えません。この回は、この「差すだけで済むか」を、実際に Bluesky を足して確かめていきます。
延長タップの図は、差し込む品物だけでなく、差し込み口の形も見てください。口に合う機器なら、壁の配線を変えずに使えます。けれど、形や電気の条件が違う機器まで、そのまま受け入れるわけではありません。OCPも同じで、「足せるものの範囲」が先にあります。どんな機能でも無条件に足せる、という約束ではないのです。
共有先に置き換えると、「今ある値を、決まった名前の欄へ入れ、今ある方法で開く」が口の形です。新しいSNSの名前は初めてでも、その形へ収まるなら追加できます。反対に、既存のSNSからの依頼でも、新しい入力方式や操作が必要なら、口の形を広げる検討が要ります。新しい名前かどうかと、新しい仕組みかどうかを、まず分けておきましょう。
この順で図を見ると、調べるものが決まります。「追加するのは何か」「変えずに保ちたいのはどこか」「その間を何がつなぐか」の三つです。新しいファイルの数だけを数えるより先に、守る範囲を決めます。これから追うBlueskyの例は、この範囲が一度では足りなかったので、OCPを読むのに役立つ題材なのです。
ここで、このプラグインが共有ボタンをどう作っているかを、先に確かめておきます。共有先ごとに、この連載で名札カードと呼んでいる小さな設定ファイルが1枚ずつあります。置き場所は destinations フォルダで、X なら x.toml、LINE なら line.toml です。次が X の名札カードです。
# X(旧Twitter)のポスト画面。
key = 'x'
label = 'ポスト'
brand_color = '#000000'
action = 'open'
endpoint = 'https://twitter.com/intent/tweet'
[params]
url = 'url'
text = 'text'
hashtags = 'hashtagsCsv'
via = 'via'上から、共有先の名前(key)、ボタンに出す文字(label)、ボタンの色(brand_color)、押したときの動き(action)、開く共有画面の住所(endpoint)です。ここでは省いたアイコン(icon)を合わせて、名札カードの項目は7つあります。いちばん大事なのは、最後の [params] です。これは「SNS に渡す値の対応表」で、左側が SNS の受け取る名前、右側がこのプラグインの用意した値の名前です。たとえば url = ‘url’ は、「X が url という名前で受け取る欄に、いま読まれている記事の URL を入れる」という意味です。
ボタンが押されると、プラグインはまず、いま開いているページのタイトルや URL を1つにまとめます。コードでは ShareRequest という部品です。次に、名札カードの endpoint の後ろに、[params] の対応表どおりに値を並べて、共有画面の URL を作ります。こちらは ShareDestination という部品です。X なら、https://twitter.com/intent/tweet?url=(記事の URL)&text=(共有文)… という URL ができ上がります。この手順は、どの SNS でも同じです。SNS ごとに違うのは、名札カードの中身だけです。この記事では、どの SNS でも同じ手順で動くこの部分を、共通処理と呼びます。
名札カードの行は、実行するプログラムを好きに書く場所ではありません。text = 'text'の右側へ計算式を書いても、その式を評価する仕組みはありません。「この欄には、この用意済みの値を入れる」という対応を選ぶだけです。機能が制限されているから、カードを追加した人が共通処理の手順まで勝手に増やすことはできません。
その制限は、足す人にとって窮屈さにもなります。ただ、何ができるかが決まっているため、読む人は全カードについて同じ手順を当てられます。あるカードだけが通信したり、別のカードの設定を変更したりする隠れた処理を探さずに済みます。自由度と調べる範囲を交換している、と見ると、データとして差し込む方法の意味が分かりやすくなります。
同じ手順で作ることは、すべてのSNSが同じ仕様だという意味でもありません。URLの入口、受け取る項目名、必要な文字列はSNSごとに違います。その違いを、用意した対応表で表せる範囲だけ外へ出しています。対応表の外にある違いが来たら、どこで止まるか。今回の後半は、その境目を確かめる話です。
なぜ、動いているコードを直したくないのか
そもそも、どうして「すでに動いているコードを直さない」ことが大事なのでしょうか。理由は、直した瞬間に、そのコードに頼っていたすべてのものを確かめ直す必要が出てくるからです。X のボタンのために書いたコードを、Bluesky を足すついでに1行でも書き換えれば、X のボタンが今までどおり動くかを、もう一度確かめなければなりません。LINE も、Facebook も同じです。拡張のたびに、それまでに作ったすべてのボタンを確かめ直すことになります。
足すものが新しいファイルとして独立していれば、重点的に調べる共通処理の変更を減らせます。ただし、設定と生成物を含む組合せの確認は必要です。すでにある X や LINE のボタンは、今までのテストをそのまま流し、確認できた範囲を確かめます。OCP が目指しているのは、この「共通処理の変更を抑え、追加の影響を調べやすい」形です。
閉じる範囲を、図の囲いで先に決める
「コードを直さない」と言ったとき、リポジトリの何もかもが変わらないと受け取ると、早い段階で行き詰まります。名札カードも設定も生成物も変わるからです。そこで、次の図では、保ちたい共通処理の周りだけに囲いを描きました。
囲いの外には、追加する名札カードと、表示する共有先を選ぶ設定があります。囲いの中には、対応表を読み、文字を整えて、URLを作る共通の手順があります。外側の値が増えても、中の手順が同じままなら、この種類の追加に対して共通処理を保てたと言えます。囲いの位置を最初に決めることで、「どこが変わったか」の議論が揃います。
図では、生成された配布物を囲いの外へ出しました。名札カードを追加すれば、配布するJavaScriptの中身やハッシュは変わります。これは共通の手順を人が書き換えたことと同じではありません。出荷するものが同一か、設計上保ちたい手順が同一かを分けて数えます。前回のSRPで見た、原稿と印刷物の違いにもつながるところです。
ただし、囲いを都合よく小さくして、触ったファイルを全部外へ追い出しては検証になりません。追加前に「ここを保つ」と決め、追加後も同じ範囲で差分を見ます。たとえば、URLの組み立てを保つと決めていたのに、その計算を別ファイルへ移して書き換えたなら、ファイル名が変わっても共通の手順を変更しています。
実務で短く記録するなら、「既存の動きと値で表せる共有先の追加に対して、URLの組み立てとクリック処理を変更しない」と書けます。対象の変化と、保つ範囲が一文に入っています。「OCPを守った」という言葉だけより、次に依頼が来た人が、自分の変更をその範囲へ当てやすくなります。
囲いの外を変えた場合でも、組合せのテストは必要です。設定へ重複した名前を入れたり、既存の共有先を表示対象から外したりすれば、共通処理を一行も変えずに利用者の画面が変わります。囲いは変更の整理に使うもので、テストを免除する札ではありません。この違いを持ったまま、次の実験へ進みます。
「閉じている」は、「もう触れない」ではない
OCP を最初に言葉にしたのは、Bertrand Meyerです。1988年の本『Object-Oriented Software Construction』で、よいモジュールは「開いていて、かつ閉じている」べきだと書きました。
Meyer の定義では、「開いている」とは、あとから機能を足せることです。データに項目を足したり、できることを増やしたりできる状態を指します。一方の「閉じている」は、ほかのモジュールから使ってよい状態として仕上がっていることです。具体的には、そのモジュールのインターフェース、つまり外から使うときの名前・渡す値・返ってくる値が決まっていて、もう変わらないことを指します。
ここが、よく誤解されるところです。「閉じている」は「もう誰も触れない」という意味ではありません。「閉じている」とは、ほかが安心して頼れるほど、使い方の約束が安定していることです。使い方が変わらなければ、それを使うコードは書き換えずに済みます。機能を足せることと、ほかのコードが安心して頼れること。この2つを1つのモジュールが同時に満たしている状態が、Meyer の言う「開いていて、閉じている」です。
ここで言う安定した約束は、「過去の不具合まで永久に残す」という意味ではありません。URLの文字変換に不具合があれば直しますし、新しい要件が今の形へ収まらなければ、約束を見直すこともあります。OCPが減らしたいのは、よく来る追加のたびに、関係のない既存の手順を開き直すことです。
たとえば、入力として受け付ける値を増やす変更と、既存の値の意味を変える変更では、使う側への影響が違います。「タイトル付きURL」を新しい名前で足せば、その名前を選んだ共有先だけが使えます。一方、今までのtextの意味を突然「タイトルとURL」に変えると、textを使う既存の共有先も変わります。名前を増やすか、意味を変えるかという選択にも、閉じておきたい範囲が表れます。
共通処理はプラグインを知らない。プラグインが共通処理を知っている
Meyer から25年あまりのちの2014年、Robert C. Martinは、OCP を今の言葉で説明し直しました。Martin の記事の中心にあるのは、次の一文です。「システムを変更せずに、その振る舞いを拡張できるべきだ(You should be able to extend the behavior of a system without having to modify that system.)」。そして、その形の手本として、テキストエディタやゲームのプラグインを挙げています。
プラグインで拡張できる仕組みには、決まった向きがあります。Martin は「システムはプラグインを知らない。プラグインがシステムを知っている(The system doesn’t know about the plugins. The plugins know about the system.)」と書いています。この記事のプラグインに当てはめると、名札カードの1枚1枚がプラグインで、共通処理がシステムです。名札カードは、共通処理が決めた7つの項目の書き方に従って書かれます。これが「プラグインがシステムを知っている」側です。一方で、共通処理のコードには、x や bluesky といった SNS の名前が1つも書かれていません。これが「システムはプラグインを知らない」側です。だから、名札カードを何枚足しても、共通処理は変わらないのです。
考えてみる:共通処理が SNS の名前を知らないなら、X のボタンと LINE のボタンはどうやって見分けているの?
見分けていません。共通処理が知っているのは「名札カードには key・action・endpoint・params がある」という書き方だけです。X の名札カードには X の共有画面の URL が、LINE の名札カードには LINE の URL が書いてあるので、共通処理は同じ手順で URL を組み立てるだけで、それぞれ正しい共有画面へたどり着きます。違いはすべて名札カードの値に入っていて、共通処理の手順は1つです。連載1.2で読んだ「URL を組み立てるのは1か所だけ」も、この形のことです。











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