LSP――Copyに「開くURL」を要求すると何が壊れるか
作成: / 公開: / 内容更新:
執筆:tamito0201 / 掲載・運営:プロマリ
丸い共有ボタンの裏側を掘る連載の第6回。SOLID の3つ目の原則 LSP を、長方形と正方形の例と、共有ボタンの実物で確かめます。コピーのボタンに「開く URL」を求める書き換えを実際に作ると、型検査は通り、テストが5件落ちました。事前条件・事後条件・不変条件の3つの約束を、図15点で読み解きます。

こんにちは、プロマリの紫です。連載「丸いボタンの裏側」の第6回、第2章の3回目です。SOLID は、変更に強いコードを書くための5つの原則を、頭文字でまとめた呼び名です。前回は2つ目の原則 OCP を読み、「足すときに、すでに動いているコードを直さずに済むか」を、本物の SNS を1つ足して確かめました。今回は3つ目の原則、LSPです。テーマは共有先を取り替えても、使う側が困らないかということ。コピーのボタンに「開く URL」を求めると何が壊れるのかを、実際に書き換えて確かめます。
点線の付いた用語は、その言葉を押すと詳しい説明が開きます。意味、身近なたとえ、この実装での使い方を順に読めます。キーボードではTabで用語へ移動し、EnterまたはSpaceで開き、Escapeで本文へ戻れます。各ページで最初に出る用語から参照できるようにしました。
この記事のコードと図の土台は、promari-sns-share 4.0.0の公開ソースです。既存の実験の件数・差分は執筆時の記録として残し、今回加えた場面は「説明例」「練習例」と区別しています。現在の4.2.2にはComposeや下書きの対応もあるため、ここに載せた操作の種類や行数を、現在版の一覧としては読まないでください。各原則を、同じ実装を土台にして一つずつ確かめるための版の固定です。
この記事は連載「丸いボタンの裏側」の第6回(2.3)です。前回の2.2「OCP――共有先を一つ足したときの差分を追う」で、押したときの動き(開く・コピー・端末の共有)で分かれる場所を数えました。今回はその続きなので、先に読んでおくと話がつながりやすくなります。連載全体の地図は、本編5ページ目の連載の目次からどうぞ。
この回は6ページあります。1ページ目で LSP の意味と「3つの約束」を確かめ、2ページ目で長方形と正方形という有名な例で原則をつかみます。3ページ目で共有ボタンの実物に戻り、4ページ目で「どの共有先も URL を開けば共有できる」と思い込んだコードを実際に書いて、何が壊れるかを確かめます。5ページ目では、テスト用の代わりの部品を作るときの約束を、6ページ目では OCP とのつながりと、やりすぎの見分け方、宿題に進みます。図は19点です。既存の実験の件数や出力は執筆時に手元で動かした記録で、追加の模式図は、約束を読むための説明例です。
取り替えても、使う側が困らないか
記事の上に並んでいる丸いボタンを思い出してください。X のボタンも、コピーのボタンも、見た目は同じ丸です。押し方も同じで、どちらもクリックするだけです。けれど、押したあとに起きることは違います。X のボタンは X の投稿画面を開きます。コピーのボタンは、記事の URL をクリップボードに書き込みます。スマートフォンで出る「共有」のボタンは、端末の共有メニューを開きます。
見た目が同じだと、つい「どのボタンも、押したら何かの画面を開くもの」と考えたくなります。けれど、コピーのボタンには開く画面がありません。もし、ボタンを押したときの処理を「開く画面の URL を受け取って、それを開く」という1つの手順で書いてしまうと、コピーのボタンは正しく動きません。この回の LSP は、こうした取り違えを防ぐための原則です。
ここで大事なのは、どちらのボタンも悪くないことです。X のボタンは開くのが仕事で、コピーのボタンは書き込むのが仕事です。壊れるのは、使う側が「どれも同じはずだ」と思い込んで、中身の違う部品を同じ手順で動かしたときです。取り替えて壊れるかどうかは、使う側が何に頼っているかで決まります。
ここでは、部品を使う側の席に座って図を見てみましょう。使う側が知っているのは、丸い見た目なのか、返り値が文字列ということなのか、それとも「コピーが終わった」という結果なのか。頼っているものを一段ずつ具体的にすると、同じところと、同じにしてはいけないところが分かれてきます。見た目がそろっているだけでは、押したあとの仕事まで一つにはできません。
この違いを見落とすと、共通化したコードは短くなっても、利用者の目的が消えてしまいます。コピーを押した人が欲しいのは、次の画面へ貼り付けられるURLです。記事自身を開き直すだけでは、エラーが出なくても目的を満たしません。「最後まで止まらずに動いたか」と「頼んだ仕事が済んだか」を分けると、今回の実験で見るべき結果も決まります。
Liskov の定義:使う側から見て、振る舞いが変わらない
この原則の名前は、計算機科学者の Barbara Liskov(バーバラ・リスコフ)から来ています。Liskov は1987年の講演「Data Abstraction and Hierarchy」で、ある型を別の型の代わりに使えるための条件を、次のように書きました。「T を使って書かれたどんなプログラム P でも、T の値の代わりに S の値を置いたとき P の振る舞いが変わらないなら、S は T の部分型(サブタイプ)である」。原文は What is wanted here is something like the following substitution property で始まる一文です。
1994年には、Jeannette Wing(ジャネット・ウィング)との共著論文で、この考えがもう少し分かりやすく言い直されています。部分型のオブジェクトは、元の型のオブジェクトを使う人やプログラムから見分けがつかないほど、同じように振る舞うべきだ(the objects of the subtype ought to behave the same as those of the supertype as far as anyone or any program using supertype objects can tell)、という言い方です。
身近なもので言えば、スマートフォンの充電器です。純正の充電器が壊れたので、同じ形のプラグの充電器を買ってきたとします。差し込めることは大事ですが、それだけでは足りません。スマートフォンが頼っているのは「決まった電圧で電気が来ること」です。形が同じでも、約束と違う電圧を出す充電器なら、スマートフォンは困ります。LSP が見ているのは、この「形が合うこと」の先にある「出てくるものまで約束どおりか」です。
プログラムに当てはめると、差し込み口の形にあたるのがインターフェースです。メソッドの名前や、受け取る値と返す値の型のことです。TypeScript では、形が合わなければコンパイルの時点で止まります。けれど、形が合っていても、中身の振る舞いが約束と違うことはあります。そこは、形を確かめる仕組みでは見つかりません。
ただし、「同じように振る舞う」は、内部の計算手順や戻り値を、すべて一字一句そろえるという意味ではありません。面積を答える図形なら、長方形が50、正方形が25を返しても、その図形の面積として正しければ約束を守れます。使う側が約束から期待できる性質を保つことが大事です。何が観測できて、何に頼ってよいかを決めずに、単に「違いが無い」と読まないようにします。
充電器の絵も、その区別をつかむための模型です。色や製造元が違うことは、この例の約束には含めていません。一方、必要な電力を安全に届けることは外せません。実物の充電器には電圧以外の条件もありますが、図では「差し込める形だけでは十分でない」という一点を見ます。コードでも、型の名前と、実際に守る必要のある振る舞いを別々に書き出すところから始めます。
約束を3つに分ける:事前条件・事後条件・不変条件
では、「約束どおり」とは何を指すのでしょうか。約束は、3つに分けて考えると確かめやすくなります。
1つ目は事前条件です。処理を呼ぶ前に満たしておくべき条件で、「受け付ける入力の範囲」と言いかえられます。充電器なら「日本の家庭用コンセントにつなげば動く」です。2つ目は事後条件です。処理が終わったときに守ると約束する結果で、充電器なら「決まった電圧で電気を出す」です。3つ目は不変条件です。いつ見ても成り立っているべき条件で、充電器なら「本体が熱くなりすぎない」です。
取り替えた部品が約束を守るとは、この3つについて、元の部品より約束を悪くしないことです。事前条件なら、受け付ける範囲を狭めない。事後条件なら、約束する結果を減らさない。不変条件なら、崩さない。入口は狭めない、出口の約束は減らさない、いつもの性質は崩さない。この三つを出発点として、使う側が頼る約束を具体的に確かめます。
考えてみる:同じ名前のメソッドを持っていれば、取り替えられるの?
名前と型がそろっているのは、差し込み口の形が合っているだけです。取り替えられるかどうかは、その先の3つの約束で決まります。たとえば同じ write という名前でも、片方は書き込みが終わってから返り、もう片方は何もせずに返るなら、使う側は違いに気づけず、壊れた結果を受け取ります。名前と型は必要な条件ですが、それだけでは足りません。
観察窓から見える結果で、交換できる範囲を決める
同じ約束を、次の観察窓で考えてみます。窓の向こうにどんな部品が置かれているかは見えません。こちらから紙を一枚渡すと、仕事が終わったことを示す返事と、実際に残った結果だけが見えます。今回は、その二つを見比べます。
「約束を守る部品」の窓では、渡したURLが書き込まれてから、完了の返事が戻ってきます。「何もしない代役」の窓では、同じ返事だけが戻り、書き込み先は空です。呼び出しの形も返事の型も同じなのに、後で貼り付けようとする人は困ります。図の赤い空欄が、型の検査だけでは拾えなかった違いです。
この窓に何を映すかは、使う側の約束から決めます。内部で何個の変数を使ったか、どの補助関数を通ったかまで映す必要はありません。そこを固定すると、約束を保った改善までテストが妨げてしまいます。一方、完了の返事だけに窓を狭めると、空の書き込みを見逃します。使う側が次の作業で必要とする結果が、ちょうど窓の中に入るようにします。
失敗する可能性も、窓の外へ捨ててはいけません。ブラウザから書き込みを拒否されたら、成功したふりをせず、失敗として返す必要があります。使う側が代わりの案内を出せるのは、成功と失敗を見分けられるからです。いつでも成功するという約束と、成功したと返したなら仕事が済んでいるという約束は、違うものです。
観察窓の練習として、保存する値をA、次にBへ変える二回の呼び出しを考えてみます。一回目にAが残り、二回目にBが残るなら、少なくともその順序では渡した値を書けています。いつも最初のAだけを保存する代役は、一回だけのテストには通っても、二回目で違いが見えます。状態を持つ部品では、単発の結果だけでなく、続けて使ったときの約束も大切です。
反対に、書き込むたびに内部の配列を作り直すか、同じ変数を上書きするかは、この窓からは見えなくてもかまいません。使う側が必要とする結果を保てば、内部の工夫は変えられます。テストを内部の変数名へ結び付けすぎると、契約を守った変更でもテストを直すことになり、何を守る検査かがぼやけます。
期待する値も、代役自身が返した値から作らないようにします。「保存したと言っている値」と「保存された値」を同じ関数から読むだけでは、両方が同じ間違いをしている可能性があります。渡した入力と、観測した保存先を比較するなど、確かめたい関係が独立して見えるようにすると、窓の意味がはっきりします。
今回の空欄の図なら、入力のURLと、書き込み先に残った文字列の二つを並べます。完了の返事は三つ目の観測です。入力、結果、返事を別々のものとして記録すると、「返事は来たが結果が無い」「違うURLが残った」「結果は正しいが失敗として返した」といった違いを、同じ物差しで整理できます。
三つの条件は、この窓を整理する出発点です。状態を持つ部品では、呼び出す順序や、前に行った操作との関係も調べます。一度読んだ値が、読むだけの操作で勝手に変わらないか、といった約束です。表の三欄を埋めただけで、あらゆる使い方を証明できたとは考えず、実際の利用手順までたどって確かめましょう。











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