LSP――Copyに「開くURL」を要求すると何が壊れるか
作成: / 公開: / 内容更新:
執筆:tamito0201 / 掲載・運営:プロマリ
丸い共有ボタンの裏側を掘る連載の第6回。SOLID の3つ目の原則 LSP を、長方形と正方形の例と、共有ボタンの実物で確かめます。コピーのボタンに「開く URL」を求める書き換えを実際に作ると、型検査は通り、テストが5件落ちました。事前条件・事後条件・不変条件の3つの約束を、図15点で読み解きます。
LSP と OCP:違いを隠すか、見て選ぶか
ここまでで、LSPが求める3つの約束と、それを破ると何が起きるかを見てきました。最後のページでは、前回のOCPとのつながりから始めます。
Martin は、1996年の記事で、LSP と OCP を結びつけています。LSP を守らない関数は、親の型を受け取りながら、実は子の型をすべて知っていなければならない。そうなると、新しい子が増えるたびに、その関数を書き換えることになる。だから、LSP を破ると OCP も破ることになる、という説明です。4ページ目の書き換えた処理は、まさにこの形でした。開く共有先しか想定していなかったので、コピーの共有先が入ってきたら、処理を書き換えるしかありません。
違いのある部品を、取り替えても壊れない形にする方法は、大きく2つあります。1つは、違いを部品の中に隠すことです。共有先ごとに「押されたら何をするか」を実行するメソッドを持たせ、使う側は、どの共有先かを知らずにそのメソッドを呼ぶ。同じ呼び方で中身の違う部品を動かせる、この性質をポリモーフィズムと呼びます。もう1つは、違いを札として見せて、1か所で見て選ぶことです。このプラグインは、後者を選んでいます。共有先は action という札を持ち、ShareActionPolicy.decide が1か所でその札を見て、実行方法を選びます。
このプラグインが後者を選んだのには理由があります。共有先は名札カード、つまりデータとして書かれていて、コードを持たないからです。名札カードにコードを持たせると、共有先を足すたびにコードを書くことになり、前回の OCP で確かめた「名札カードを1枚足すだけ」が崩れます。代わりに、実行方法の数は増えにくいと考え、札で分けることにしました。札で分ける形では、実行方法を足すときに decide と #perform を開くことになります。前回数えた「動きの種類で分かれている8か所」が、その代償です。
札で分ける代わりに、札の書き忘れは型検査で止める。#perform の最後にある default の分岐は、残った decision を never という型の変数に入れています。never は「ここには何も来ない」という意味の型です。新しい実行方法を足して分岐を書き忘れると、ここに値が残るので、型検査が止まります。LSP の違反は型検査で見つからないと書きましたが、「分岐の書き忘れ」のように、型に書ける約束は、型で守ることもできます。
やりすぎと、LSP の限界
LSP は大切な原則ですが、当てはめすぎると、かえってコードが読みにくくなります。気をつけたい点を3つ挙げます。
1つ目は、取り替えないものにまで、共通の約束を作らないことです。LSP が役に立つのは、同じ場所で、中身の違う部品を取り替えて使うときです。取り替える予定のない部品のために、先回りして共通の約束を作ると、読む場所が増えるだけです。
2つ目は、共通の約束を大きくしすぎないことです。長方形と正方形の例では、共通の約束を「面積を答えられる」だけに絞ったので、どちらも守れました。もし「幅と高さを別々に変えられる」まで約束に入れていたら、正方形は守れません。約束を大きくするほど、守れる部品は減ります。使う側が本当に頼っていることだけを約束に入れるのが、ちょうどよい大きさです。この「約束の大きさ」の話は、次回の ISP でくわしく扱います。
3つ目は、LSP を守っていることを、型検査だけで確かめたつもりにならないことです。この記事の実験では、正方形も、書き換えたクリックの処理も、約束を破った代役も、すべて型検査を通りました。型が確かめるのは形の合い方で、振る舞いの約束は、テストで確かめる必要があります。一方で、テストも、書いた約束しか確かめられません。4ページ目で違反が見つかったのは、コピーの約束がテストに書いてあったからです。書いていない約束は、テストでも見つかりません。
考えてみる:共有先を「開く」「コピー」「端末の共有」の3つのクラスに分けて、それぞれに実行のメソッドを持たせたら、LSP はもっと守りやすくなる?
使う側が共通のメソッドだけを呼ぶ形にすれば、使う側は共有先の違いを知らずに済むので、守りやすくはなります。けれど、このプラグインでは共有先を名札カードというデータで書いているので、クラスに分けるなら、名札カードとクラスを結びつける仕組みが別に要ります。実行方法が増えにくいなら、札で分けて1か所で選ぶほうが、読む場所も少なく済みます。どちらを選ぶかは、何がよく増えるか(共有先か、実行方法か)で決めます。
LSP を確かめる4つの問い
ここまでの話を、コードの変更を見直すときに使える4つの問いにまとめます。同じ4つの問いで、このプラグインの今のクリックの処理と、4ページ目で書き換えた処理を採点してみました。
問いは次の4つです。使う側は、並びの中のどの部品にも通用する約束だけに頼っているか。取り替えた部品は、元より狭い入力しか受け付けない、ということがないか。取り替えた部品は、元より少ない結果しか約束しない、ということがないか。約束は、テストに書いてあるか。4つとも「はい」なら、取り替えても使う側は困りません。今のクリックの処理は4つとも「はい」、書き換えた処理は「はい」が1つだけでした。書き換えた処理でも、約束はテストに書いてあったので、そこだけは「はい」です。だから違反を捕まえられました。
手を動かして確かめる
今回の練習は、手元の代役を1つ選んで、約束を守っているかを確かめることです。テストで使っている代役(本物の代わりに渡しているオブジェクト)を1つ選んでください。そして、本物の部品の事前条件・事後条件・不変条件を、それぞれ1行ずつ書き出します。
書き出したら、代役がその3つを弱めていないかを、1行ずつ見ていきます。本物より狭い入力しか受け付けていないか。本物が約束している結果を、代役が省いていないか。できれば、5ページ目の clipboardContract のように、約束を1つのテストにまとめ、本物と代役の両方に流してみてください。本物がブラウザの中でしか動かないときは、代役どうし(たとえば、成功する代役と、失敗する代役)に同じテストを流すだけでも、約束の書き忘れが見つかります。宿題:手元の代役を1つ選び、本物の3つの約束を弱めていないか確かめる。確かめた結果は、このラベルからコメントに残せます。
LSP の元になった定義は、Barbara Liskov の講演「Data Abstraction and Hierarchy」(1987年の講演・SIGPLAN Notices 23巻5号、1988年)です。部分型の振る舞いの考え方は、Liskov と Wing の論文A Behavioral Notion of Subtyping(1994)で読めます。長方形と正方形の例と、事前条件・事後条件の決まりは、Robert C. Martin のThe Liskov Substitution Principle(1996・Web Archive の写し)にあります。原則の見方(どういうものか・守らないとどうなるか・守るための方法)は、NakuRei さんの本『SOLID原則完全に理解した!になるための本』のリスコフの置換原則とSOLID原則とはの章を参考にし、例のコードはこの記事のために TypeScript で書き直しました。
まとめと、次回
第6回のまとめです。LSP は、同じ約束を満たす部品へ取り替えても、使う側が頼っていた条件が崩れないことを求める原則でした。約束は、事前条件(受け付ける入力)・事後条件(約束する結果)・不変条件(いつも成り立つこと)の3つに分けて考えます。取り替えた部品は、事前条件を強めず、事後条件を弱めず、不変条件を崩さない。長方形と正方形の例でも、共有ボタンの実物でも、崩れたのは型ではなく、この約束でした。
| 問い | 答え | 確かめ方 |
|---|---|---|
| shareUrl は、どの共有先でも開く先か | いいえ。コピーと端末の共有では記事の URL を返す | ShareDestination の1行目(endpoint が空なら記事の URL) |
| action を見ずに開くと何が壊れるか | コピーと端末の共有が、何もせずに follow を返す | 一時作業コピーで書き換え、テスト63件中5件が落ちた |
| 型検査は違反を見つけるか | 見つけない(終了コード0) | 正方形・書き換えた処理・3つの代役のどれも通った |
| 代役の約束はどう確かめるか | 同じ約束のテストを、本物にも代役にも流す | 契約テストで、約束を破った代役だけが落ちた |
| 名札カードの組み合わせは守られているか | endpoint と params は検査あり。action と endpoint は検査なし | 崩した名札カードを生成器に読ませた |
取り替えられるかどうかは、形ではなく約束で決まる。このプラグインは、「共有 URL を開けば共有できる」という、コピーには守れない約束を立てず、action を見て実行方法を選ぶことで、取り替えても壊れない形にしていました。そして、その約束がテストに書いてあったから、約束を破った書き換えを、実行する前に捕まえられました。
なお、この記事で試した書き換え・代役・契約テスト・名札カードは、すべて一時作業コピーの中だけのもので、プラグインの本体には入れていません。action と endpoint の組み合わせの検査も、提案にとどめています。
次回は連載2.4「ISP――コピーの代役に、いいねの保存まで求めない」です。今回は、代役が約束を弱めないことを見ました。次回は、その約束そのものの大きさを見ます。コピーの代役を作るだけなのに、いいねの保存やアクセス解析の知らせまで求められたら、何が困るのか。約束を、使う側が一緒に必要とする能力ごとに分ける ISP(インターフェース分離の原則)を、一緒に見ていきましょう。それでは、次回もお楽しみに!
連載の全体像は本編の連載目次へ。前回の2.2「OCP――共有先を一つ足したときの差分を追う」、前々回の2.1「SRP――一つの共有先の定義は、一つの理由で変わるか」とあわせて読むと、名札カードとクリックの処理が、どんな約束の上に立っているかが一続きで見えてきます。
プロマリでは、Webサイトの制作に加えて、共有や計測のような「裏側のしくみ」の設計・導入、それらを運用できる人材の育成に取り組んでいます。部品を取り替えても壊れない作りをチームでそろえたい方も、お気軽にご相談ください。
記事で読んだ約束を、実際のリポジトリで確かめるための資料です。









この記事全体の感想・質問・設計へのコメント