先日慶應義塾大学日吉キャンパスで行われた builderscon2018、最高のカンファレンスでしたね。わたしも「開発現場で役立たせるための設計原則とパターン」というタイトルで発表させていただきました。今回は恒例「実況中継シリーズ」として、プレゼンの再現をブログで行いたいと思います。
なお、過去の実況中継シリーズは前職の技術ブログにまとまっていますので、そちらからご覧ください。
それでは本編を開始したいと思います。
開発現場で役立たせるための設計原則とパターン
アバンパート
よろしくお願いします。
まず最初に簡単に自己紹介をさせていただきます。
先月転職をしまして、8/1からClassiという会社で働いています。妻と息子がおります。Scalaが好きですが、仕事ではRubyメインという感じです。
Web+DB PressやSoftware Designで何度か特集を書かせていただきました。とくにこの真ん中のデータ構造のやつは、ありがたいことになかなか良い評判をいただけて、自分でも気に入っているやつです。C言語でいうところの配列がどのようにメモリ上に展開されているかから始まり、Rubyを使って各種基本的なデータ構造をオーバービューし、最後にはなぜかMySQLのカバリングインデックスについてまで話が進む特集となっておりますので、気になる方はぜひバックナンバーを購入してみてください。原稿料はすでにいただいているので、いまから買っても追加でぼくにお金は流れないんですけど(笑)、それでも読んでくださったらうれしいです。
まず最初になんですけど、あの、お手持ちの名札にですね、QRコードが印刷されていて、そこからベストスピーカー賞ってやつに投票できるようになっているんですね。これには投票しなくていいんで、フィードバックのほう、要するにアンケートですね、これにぜひ答えてください。どこがわかりやすくてどこがわかりにくかったかなどフィードバックをいただければ、今後の参考にさせていただきます。あの、ベストスピーカー賞に投票しなくていいって言ったんですけど、これは建前でして、よかったらぜひ投票してほしいです……
それはそれとしてですね
ぼくは今までもけっこうカンファレンスやブログでデザインパターンや設計原則についてアウトプットしてきたんですけど、だいたい結論が「ケースバイケース」みたいなところに落ち着いちゃうんですよね。それはまあ実際そうだなと今でも思ってるんですけど、一方で、「ケースバイケース」ってなにも言ってないに等しいみたいなところあると思うんです。だから「ケースバイケース」だけ言うのでは片手落ちだなっていう気持ちが自分にはずっとあって、じゃあどうやって「このケースだとこの構造でいいね」あるいは「ダメだね」を判断するんだ、その方法まで一緒に言わないとダメなんじゃないかって思うんです。
なので、今日はその点について、設計原則とデザインパターンを使って考えていきたいなと思っています。
というわけで、本題に入っていきますが、ところでみなさん、こういうことってありませんか?
設計原則の話とかパターンの話って、どうも具体性が乏しいというか、「ビジネスロジック」って具体的になに?って思ったりしたことないですか?あるいは、「抽象化」って言われるけど、AさんとBさんの言ってる「抽象化」はなんか意味が違う気がする……?みたいな。あるいは、「このクラスの責務はXXだから」みたいな話をされても、責務なんて考え方次第やんけみたいなところありませんかね。たとえば「このクラスの責務はシステム全体の挙動に責任を持つことです」って言われて、「それ責務でかすぎやろ」って言ったとして、「でかい」とか「小さい」とかふわっとした物言いでちゃんと議論噛み合うんですかね……?そんな感じで設計とかパターンの話って空中戦になりがちだと思うんですよ。
そういう気持ち。ぼくはあります。わかりまくる。圧倒的わかりがある。
あるいはですね、こういう思い出があるプログラマも結構いるんじゃないかなって思うんです。
ある日Qiitaで「XXパターンについて解説するよ!」って記事が上がっていて、それを読んで思うわけです。「XXパターン完全に理解したわ」。それでそのあとはてぶ見にいったら、なんか強そうなひとたちが強そうなことばで議論始めてるんですよね。それを見て思うわけです。「XXパターン完全にわからん」。
わかりまくる。圧倒的わかりがある。
あるいはこういう思い出はありませんか?
なんか強いひとが「DRY原則大事!コピペ禁止!」って言うわけです。そこでがんばって「共通化」(この時点でちょっといやな予感はする)するわけです。そして爆誕するUtilクラス!!!!そして発生する怒られ!!!慈悲はない!!!帰りの電車で泣きながら思うわけです。「お前が共通化しろって言ったんちゃうんか!!!!!!」
わかりみがすごい。
ぼくはけっこうこういうのって悲しい話だなあと思っていまして。どうしてこういうことがおこっちゃうんだろうって考えて見て、ふと思い当たる節があったんですね。もしかしたら、それぞれのパターンや原則についての話がバラバラに語られてしまっていて、「全体としてどのようにそれらが繋がっていて、どのように現場で活かすことができるのか」についての話があまりなされないことが原因なんじゃないかって。なので今日は、そのあたりの話ができたらなと思っています。
というわけで、まずは「そもそも設計ってどういう活動なんだっけ」ってことをまずは見ていきたいです。この前PHPの現場っていうポッドキャストを聞いていたら @polidogさん という方がおっしゃっていたんですけど、マルチパラダイムデザインっていう本でこういうことが書かれているそうなんですよ。それでぼくは慌てて会社にこの本買ってもらって読んだんですけど、たしかにこういういいことが書かれているんですね。
ただちょっとこれ難しい言い回しだなあと思っていて、雑にぼくの言葉で言い換えると、「問題を分割して構造化すること」だと思うんです。
そこでですね、「よっしゃ分割やな!!やるぞ!!」となるんですけど、
なんか「分割すればええんやな」って適当に分割してもいいんですかね。あるいは、知ってるデザインパターンが使えそう!って思って闇雲にデザインパターン適用するのって設計ですかね。多分「違うよね」ってそれはみんな思ってくれると思うんです。
そこに欠けてるのは多分「どう分割するか」って話だと思うんですよね。たとえばRDB触るシステムのその部分だけ考えたとしても、分割の方法って複数考えることが可能だと思うんですよ。テーブルごとにモジュール作るのか、それとも、Create,Read,Update,Deleteで責務分割してモジュール作るのか。あるいはまったく別の方法で分割するのか。良し悪しはいまは置いておいて、選択肢として考えることは可能ですよね。
そう考えると、設計って「複数ある選択肢の中から最適な構造をしている選択肢を選び出すこと」とも言えるんじゃないかなと思うんです。で、ぼくはデザインパターンと設計原則って、そういうときに役に立つと思っています。
デザインパターンって、「問題をこう分割することができるよ」っていうパターンのカタログですよね。たくさん手法を知っていればたくさん手法を知っているほど、ひとつの問題に対して多くの選択肢を思いつくことができると思います。
そして、設計原則は「今ここにあるこの構造が、良い設計になっているかどうか」を確かめるために指針になります。たくさんある選択肢の中からどれを選べばいいのか考えるときには、設計原則を使って考えることができる、ということですね。
これは図にするとこういうことで、設計って「一回デザインパターンとか使って構造作っておわり」じゃなくて、そのあと設計原則を使って「この構造は良いものになってるかな?」ってセルフレビューして、ダメなところがあったらそこに対してまた構造を作り直して見て、それでもう一回設計原則でレビューして、っていうこういうイテレーションを繰り返していくことで「より良い構造を削り出していく」って行為だと思うんです。
ものすごい能力があったりセンスがあるひとたちって、一発でいい構造が描き出されているように見えるかもしれないけど、おそらく彼らの脳の中では無意識でこれをやっていたり、高速にこれをやっているはずです。
ここでちょっとお詫びなんですけど、最初に「設計の話とかってふわっとした話になって空中戦になりがちだよね〜」って言っておきながら、ぼくも今日ひとつもコードとか書かずに抽象的な話に終始してるんですねいまのところ。
ただちょっと許して欲しくて、これ、アニメとかドラマで言うところのアバンパートなんですよ。導入パート。あの、OPムービーの前に入るドラマパートのやつ。具体的な話はこのあとやっていくので、許してください。
というわけで、アバンパートのあとはOPに入るわけですが、今日は歌う余裕がないので、息子の写真にて返させていただきます。
OP〜Aパート
はい、OPのあとはAパートです。
Aパートでは、より具体的な例を見ていきたいとおもいます。
たとえばこういう例題を考えましょう。サービスの一機能として掲示板があります。この掲示板のポストをユーザーはwatchすることができ、watchしているポストにコメントがついた場合は、たとえばマイページみたいなところの「お知らせ」欄に「XXさんがYYのポストにコメントを書きました」みたいな通知が出るみたいな感じです。ついでに、アプリ持ってて、アプリのpush通知機能をonにしているユーザーにはpush通知も送りましょう。
もちろん、お知らせ欄には新しいコメントがついたことだけじゃなくて、たとえば自分のコメントにスターがついたよとかそういうものも出てくるとします。
「ふーん、通知機能ね。じゃあ通知クラス作ってそれでやろ」って適当に書き始めてみました。
まず、通知種別が「コメントがついたよ」ってやつだったら、コメントをwatchしてるユーザーを引いてきて、通知本文作って、ユーザーごとにお知らせテーブルに書き込んで〜。
通知種別が「スターがついたよ」だったらうんぬんかんぬんで〜
会場におられる強いひとたちはすでに「あかん!!!目も当てられない!!」ってなりつつあるのではないでしょうか。
けど、ここで考えて欲しいのは、「じゃあなんであかんの?」って話なんですよね。その理由を言語化できるでしょうか。もしそれができないとしたらそれは暗黙知で、おそらく「センス」と呼ばれるようなものだと思うんです。自分が初学者だったときに「このコードセンスない」って言われて納得できますか?できましたか?レビューするためにも、みんなで良いものを作っていくためにも、ぼくたちはこの暗黙知を言語化していくべきだと思うんです。
ここで改めて「設計原則は、いま目の前にある構造をレビューするときの指針になる」って話を思い出してください。「なんか臭う」「なんか嫌な予感がする」「センスない」じゃなくて、「このコードだとこういう理由でこういう設計原則に反しちゃってるよね」って言語化できたら、定量的とまでは言わないけど、少なくとも論拠を持ってこのコードの良し悪しを判断できますよね。それは他人に説明するときだけではなく、自分自身が書いたコードを見直すときにも役に立つはずです。
なのでまずは、ぼくが重要だと思っている設計原則をみっつ覚えて欲しいんですけど
そのみっつは
- 単一責任原則
- 開放閉鎖原則
- 凝集度と結合度
です。
まずは単一責任原則について見ていきましょう。単一責任原則って字面からも「ひとつのモジュールはひとつの責務だけ持とう」って意味がわかると思うんですけど、冒頭で言ったように、「責務」ってなんやねんってのは結構ガバガバですよね。
だから、より具体的な話として、「Aについての変更でもBについての変更でも影響を受けるようなモジュールがあれば、それは責務持ちすぎなので適切に分割されるべき」という言い方にしてみます。これは別にぼくが考えたわけじゃなくて、そもそも、単一責任原則はもともと「モジュールが変更される理由がふたつ以上あってはいけない」という原則なんですね。
さらに具体的に、コードで見てみると、たとえばさっきの「あかん」コードについては、まず、コメントが書かれたときの挙動に変更が入った場合、ここを書き換えることになりますよね。
で、スターに関する挙動に変更が入ったら、こんどはここを書き換えることになります。
これは、このモジュールが仕事をしすぎていることを示しています。こうなってしまうと、「ある機能に対しておこなった変更が、べつの機能に対して影響を与えかねない」って感じで、メンテナビリティが悪いですよね。
じゃあどうなれば単一責任原則を満たせるのか、って話ですが、これはBパートに譲ることにします。
とりあえず今回は、「単一責任原則」という視点を持ち込んだことで、「なんかあかん」とか「センスない」じゃなくて「これこれこう言う理由で単一責任原則に反しているため、こういうふうにメンテナビリティが悪い」と言えるようになった、ということが重要です。
続いて開放閉鎖原則を見て見ましょう。ぼくはじめてこの原則の名前見たときに思ったのが「開放すんのか閉鎖すんのかどっちだよ!!!!」ってことでした。これ名前悪いっていうかわかりにくい名前ですよね。
何かっていうと、「拡張に対して開いておけ」「修正に対して閉じておけ」ってことらしいんです。なるほどわからん。もっと具体的にいうと、「機能拡張する、つまり新しい機能を作るときに、既存のモジュールに手をいれなくてもいいようにしようね」ってのが、「拡張に対して開いておこう」という意味です。
で、「なにか、あるモジュールを修正するときに、別のモジュールもいっしょに修正しないといけない」みたいなのはやめようね、それは「修正に対して閉じていない」状態だよって話です。
これちょっと名前のせいでむずかしい原則に感じられるんですけど、例を見て見ましょう。
さっきの「あかん」例なんですけど、これが拡張に対して開いているかどうかをまずは見ていきます。「新しい機能を追加するときに、既存のクラスに手を入れなくてもできる状態」が「拡張に対して開いている状態」でしたよね。これはどうでしょう。たとえば「あしあとのお知らせ」みたいな機能を追加するときって、おそらくここにあたらしい分岐を入れて、みたいな感じになりますよね。それって、すでにあるNotificationクラスに手を入れちゃってますよね。これ拡張に対して開いてません。
じゃあこれ拡張に対して開かせるにはどうしますかって話なんですけど、それもちょっとBパートに譲ろうと思います。
次に、修正に対して閉じているってどういうこと?ってことについても見ていきたいんですけど、この例だとちょっとモジュールがいっこしかなくてわかりにくいんで、別の例を出させてください。
たとえばなんか引き落とし処理かなんか書いてるとして、引き落とし失敗したユーザーはサスペンドするみたいなやつがあるとしましょう。単にサスペンドするだけじゃなくて、3回サスペンドされたひとはもう永久にBANですみたいな処理もしてると考えてください。このCustomerSuspenderさんがバッチ処理みたいなイメージです。このコードなんですけど、CustomerSuspenderがCustomerのいろんなプロパティいじって、いろんなメソッド呼んでますよね。このとき、もしCustomerのプロパティに変更が入ったら、その影響がCustomerSuspender側にも出てきてしまいますね。これは「修正に対して閉じていない」と言えそうです。
もし、サスペンド処理の詳細がCustomer側に書かれていたらどうでしょうか。CustomerSuspenderはCustomerのsuspend!メソッドを呼ぶだけなので、Customerのプロパティになにか変更があっても、その変更の影響はCustomerに閉じ、CustomerSuspenderは書き換えないでよさそうです。これが、「修正に対して閉じた」状態です。
さて、修正に対して閉じる例をみた上で、改めてさきほどの「あかん例」を見てみると、そもそもモジュールが一個しかないので、修正に対して閉じるもなにもない(というか、ある意味どんな修正もこのクラスに対して閉じているは言える……)という感じですね。
というわけで、さっきのあかん例に対して、開放閉鎖原則という視点からみることで、「どんな修正がきてもこのNotificationクラスしか変更しないで済むという意味では修正に対して閉じているけど、新しい通知種別が追加されるときに既存のNotificationに手をいれないといけなくて他の機能に影響与えちゃうから、この構造はよくないね」と言えるようになりました。「センスない」「保守性よくないと思う」とかふわっとじゃなくて、論拠ができました。設計原則を利用することで、議論を開始することができますし、自分の作った構造を勘ではなくきちんとした論拠でセルフレビューできるようになりました。
覚えて帰って欲しいみっつの原則のうち最後のは、凝集度と結合度です。
凝集度は高くしよう、って話がまずあって、これは関連するものはなるべくひとつのモジュールにまとめようねって話です。これでもまだふわっとした言い方になってしまっているのでさらに具体的にいうと、関連性の高いものがいろんなところに分散して書かれていると、ある機能に仕様変更が入ったときに、いろんなところを書き換えないとダメになりますよね。それって変更漏れが起きたりとかしそうだし、そもそもめんどくさいし、メンテナビリティ低いですよね、という話です。
で、結合度は低くしようっていうやつです。凝集度とも関係するんですけど、関連するものがひとつのところにきちんとまとまってないと結合度は高くなりがちで、「片方のモジュールを変更したときに必ずもうひとつのモジュールも書き換えないとダメ」みたいな状況を「モジュールとモジュールが密結合してる」って言うんですけど、片方変えたらかならずもう片方変えないとだめなの、これもやっぱり変更漏れ起こしそうだしそもそもめんどくさくてメンテナビリティ低いですよね。
これも実例を見ていきたいんですけど、例の「あかんやつ」だとモジュールいっこしかなくて説明しにくいんで、またさっきのCustomerの例で考えさせてください。
Customerのやつであかん例みると、Customerに関する操作がCustomer以外のクラスに書かれちゃってますよね。これって、凝集度が低いと言えそうです。凝集度が低くなった結果、修正に対して閉じてない状況が生まれていると言えそうです。
仮にこの操作がCustomerに書かれていたら、「CustomerのことはCustomerにやらせている」状態で、凝集度が高まります。凝集度が高まった結果、修正に対して閉じることができました。
次に結合度を見て見ましょう。同じくモジュールがひとつしかないこの例だとわかりにくいので、Customerの例で見ます。
CustomerSuspenderクラスが、Customerクラスのプロパティをいじりまくってます。CustomerSuspenderはCustomerがどんなプロパティ持っててどんな操作が可能なのかの知識をたくさん持ってる状態です。これは結合度が高い。
じゃあ直しましょう。CustomerのことCustomerにやらせるようになった結果、CustomerSuspenderはCustomerのsuspend!メソッド呼ぶだけになりました。これで、Customerの内部が変わって、持つプロパティが変わったりしても、CustomerとCustomerSuspenderが疎結合になっているのでCustomerSuspenderには影響がありません!
さて、凝集度と結合度について見たところで、その視点であらためて今回のあかん例を見てみましょう。「お知らせ」についてはここに全部まとまっているので、凝集度は高いと言えるかもしれませんが、結合度については、それ以前の問題というか「本来はバラバラに書かれるべきものが、すべてここに書かれてて密結合しちゃってる」とみることができるかと思います。
凝集度と結合度の視点からみても「あかん例」がなぜあかんのかを説明することができました。
という感じで、Aパートのまとめです。
- みっつの設計原則について見てきました。
- 設計原則を使うことで、「なんかあかん」が言語化できること、論拠を持って自分の作った構造をセルフレビューできること、そうして「今ここにある構造が良いものかどうかを見分ける視点」を手に入れることができたことが確認できればいいな、と思っています。
以上でAパートが終わりです、Aパートが終わるとCMが始まるわけですが、だいたいその前には「アイキャッチ」と呼ばれる一枚絵が入りますよね?もちろん息子の写真がアイキャッチです。
続いてCMに行かせてください
CM
さて、今回の発表ですが、業務扱いとして参加させていただいております。また、同僚には業務時間中に発表のリハーサルにも付き合っていただいたり、そこでフィードバックをいただいたりと、要するにこのトークはClassi株式会社にサポートしてもらってる状況です。
じゃあClassiってどういう会社なのって話なんですけど、ベネッセさんとソフトバンクさんのジョイントベンチャーで、Classiっていうプロダクトを作ってます。これは学校向けのグループウェアみたいなやつで、先生はこれを使って校務を楽にできたり、先生と保護者、先生と先生、先生と生徒などでチャットができたり、あるいは卒業生の方が「大学ではこんなことをやっているよ」っていう情報を在学生と共有したり、テストがこれで受けられたり、自分の学びのあしあとが確認できたり、先生も生徒の学習状況やポートフォリオを確認できたりと、いろんなことができます。学校教育を支える、あるいはリードするプラットフォームを目指しています。
今学校教育ってかなりの変革期を迎えていて、現場はなかなか「新しい学び」に対応するのが大変なんですけど、そこをベネッセさんが蓄積した教育のノウハウや現場との強いつながりと、ソフトバンクさんが得意とするプラットフォームビジネスの力を合わせてサポート、リードしていく仕事です。まじめに、子供達の未来をよくするためにみんなでがんばっているので、興味がある方がいらっしゃったらぜひお気軽にお声がけください。
Bパート
さて、本編に戻りBパートを始めるまえに、アイキャッチですね。当然息子の写真です。
はい、Bパートすすめていきます。
Aパートでは、設計原則を利用することで「このコードのなにがいけないのか」をレビューできることを見てきました。それでは、もう一回構造を作るところに立ち返って見ましょう。
重要な図なので繰り返し出しますが、構造を作っておしまいでもなければこの構造をレビューしておしまいでもなく、このふたつを行ったり来たりしながら良い構造を削り出していくのでしたね。さっきはレビューしたので、今度は構造を作り直します。
やってみましょう。
まずは、ユースケースを見直して見ます。
なにかが起こったタイミングで何かやりたい……。
ここでオブザーバーパターンとか知ってると、「あっこれ進研ゼミでやったところだ!!!」ってなるわけです。パターンを知ってるとこんなふうに「取れる選択肢」が増えるんですね。
というわけで、オブザーバーパターンを検討してみましょう。
素直にやるとこんな感じでしょうか。まずコメントが書き込まれると、
CommentObserverがCommentを監視していて、Commentから通知がきます。
で、その通知を受け取ったら、
- WatchingUserからこのコメントをwatchしてるユーザーを引いてきて
- 通知の本文とか作りつつ
- ユーザーごとにお知らせテーブルに書き込む
と。
なかなかかっこよくできたんじゃないでしょうか。
しかしですね!!!
かっこよさで構造を決めていいのはSandboxまでですよね。
大切なことなのでなんどもこの図を出しますが、一回構造を作ったら、設計原則を使ってレビューする必要があります。レビューしたらそこで出てきた問題点を鑑みて構造を作り直す。この円環をぐるぐると回すことで、良い構造を削り出していくのでしたね。
というわけで、次は設計原則を使っていまの構造をレビューしてみましょう。
まずは単一責任原則です。
さきほどのあかん例では、Notificationクラスが「コメントの通知」も「スターの通知」もやっていて、単一責任原則に違反してしまっていましたが、今回はその処理が「CommentObserver」に切り出され、Notificationが多重責務に陥っていた状況が改善しました。また、このCommentObservertは、コメント以外の通知(Starがついたよとか)については一切知ったこっちゃありません。コメントのことだけやっています。
単一責任原則については、改善したと言ってよさそうです。
次に、開放閉鎖原則について見て見ましょう。
開放閉鎖原則の「開放」のほうは、「機能追加するときに既存のクラスに手を入れなくてもよいようにしよう」という意味でしたね。さっきの「コメント通知」の他に「スター通知」機能が追加されたときのことを考えて見ましょう。StarクラスをObserveするStarObserverが、Notificationを作成するようにしました。注目してほしいのは、今回の機能追加でCommentクラスもCommentObserverクラスもNotificationクラスもいじっていないところです。これは拡張に対して開いていると言えるでしょう。
つづいて、修正に対して閉じているかどうかみてみましょう。例えば、「通知の本文を変更したい」という例を考えてみましょう。このとき、CommentObserverの修正するだけで、さらにこの修正がどこか他のクラスに影響を与えるということもなさそうですね。修正に対して閉じていると言えそうです。
拡張に対して開いているし修正に対して閉じている。良さそうです。
最後に、凝集度と結合度について見てみましょう。
ちょっと話の流れの都合で結合度から先に見たいのですが、
まずはそれぞれのクラスがどんなクラスに依存してるかを見て、その依存が密結合になっていないか見て見ましょう。
CommentObserverがCommentを監視しているので、Commentに依存しています。しかしCommentからは通知を受けているだけなので、なんどもCommentにアクセスしたり、Commentのプロパティを読んだりもしていません。
また、CommentがWatchingUserを呼び出していますが、これもユーザーを引いてきているだけで、なんども呼び出したりプロパティにアクセスしたりしていませんね。
Notificationにも依存してますが、これもcreateしているだけです。特に密結合しているところはなさそうです。よかったですね。
続いて凝集度を見てみましょう。
気になるのはここです。CommentObserverはCommentに関することをやっていますよね。オブザーバーパターンであえてこのふたつが切り離されてるわけですが、これでいいんでしょうか。
それを検討するために、まずはObserverパターンってどんなパターンだっけ?ってのを見みます。
Observerパターンと言えば、「ボタン自体の振る舞いと、そのボタンが押された時になにが起こるかのふるまいを分離する」みたいなやつです。というわけで見てみましょう。
もしObserverパターンを使わずに素朴に書くと、Buttonクラスのpushが呼ばれた時にHandlerAとHandlerBを読んで、というようなコードになるでしょう。これなにが問題かというと、ButtonがHandlerに依存しているので、Handlerを入れ替えたいとか増やしたいってなったときに、Button側を書き換えないとダメなんですよね。ボタンに対する反応を増やしたい、あるいは変更したいときに、ボタン側を書き換えないといけない。修正に対して閉じてないし、拡張に対しても開いていません。
ここで、Observerパターンを使うとどうなるか見てみましょう。Rubyの場合は標準モジュールにobserverってのがあるんで、それ使いましょう。Observableをincludeすると「add_observer」ってメソッドが生えるんで、それを使ってハンドラが自分をボタンにobserverとして登録します。
Button側は、pushされたら、これまたObservableが生やしてくれてる「notify_observers」というメソッドを呼びます。すると、observerのupdateメソッドが呼ばれる、という感じですね。
このようにしたことによって、さっきはボタンが押されたときの挙動を変えたいときにはボタンそのものを書き換える必要がありましたが、ボタンを書き換えずに挙動を増やしたり修正したりすることができるようになりました。
ボタン自体の振る舞いと、それが押されたあと何が起こるかの振る舞いを分離できたわけですね。
改めてObserverパターンのメリットを考えると、パターンを使うことによって、普通に書くときと依存の方向が逆になり、それによってObserverを増やしたりぽこぽこ入れ替えたりでき、また、Observeされる側を書き換えることなく、Observeしてる側の挙動を書き換えることができるようになったわけです。
では、今回のお知らせ機能の例について、Observerパターンのメリットが効くかどうか検討してみましょう。
今回は、CommentがObserveされる側、CommentObserverがObserveする側です。ここにデザインパターンを適用することによって、CommentObserverを入れ替えたり書き換えたりを、Comment側に影響なくできるようになっているわけですね。
ところで、これ本当に嬉しいんですかね?コメントが書き込まれたことを監視してなにかするみたいな挙動、「お知らせ機能」以外にも増えるんですかね。いや、スターがついたときにお知らせするとか、あしあと機能とかは今後増えるかもしれないけど、それは「コメントがついたときにやること」ではないですよね。コメントがついたときにやることって今後増えたり変わったりするんですかね?
コードの側で言えば、ここのObserver増やしたり入れ替えたりすることって今後考えられるんですかね?
あるいはですね。CommentとObserverの振る舞い分離したのほんとに嬉しいんですかね。通知の仕様が変わるときって、コメントに関する仕様変更あったときなのでは?コメントに関する仕様が変わったときには多分通知も一緒に変わりますよね……?
これも図でみると、ここのふたつってもしかして、だいたい「同じタイミングで書き換える」ってことになるんじゃないですかね
そういうことを考えると、「もしかしてこれ、分けなくていいものを分けちゃってるんじゃ……?」って気持ちになってきますね
つまり、「関連の強いものがバラバラのところに書かれている = 凝集度が低い」ということなのでは!?
凝集度が低くなった結果なにが起こるかというとですね、コメントの仕様変更に対応したときに、別ファイルになってるObserverのメンテが漏れます。事故ります。
事故らないようにするためには、必ずふたつのファイルを変更しなければなりません。それって保守性上がってるんですかね?
そう考えると、この構造は凝集度の点で問題がありそうです。
改めて振り返ってみると、最初の例と比べるとだいぶましになったけど、まだ問題があるコードだと言えそうです(ほら、「なんとなく」じゃなくて、最初の例と今の例で「これは満たしてるけどこれは満たしてない」とか、ちゃんと比べられるようになってるでしょ!!これが設計原則を学ぶ意義です)
というわけで、Observerパターンを捨ててもう一度構造を考え直して見ます。なんども言いますが、「この円環を繰り返し回す」んです。
Observerパターンを捨てて、CommentObserverとCommentをがっちゃんこしてみました。
構造を作ったら設計原則でレビューしますよ。
単一責任原則です。
パッと見Commentが責務を持ちすぎに見えますが、「責務」ってことばだとふわっとしちゃうんでしたね。複数の仕様変更の影響を受けるかどうか考えてみましょう。「コメント通知の仕様が変わるときって、だいたいコメントそのものが変わるときだよね」「コメントが変わるときに通知が変わらないなんてことある?」って考えがあってこうなったわけで、これは単一責任原則を満たしていると言えそうです。
いいですね
続いて開放閉鎖原則で見てみましょう。
Starがついたときの機能拡張では、CommentにもNotiicationにもWatchingUserにも手を入れずに拡張できそうです。開いている。
また、Commentに対する修正が入ったときも、Commentの内部に修正が閉じており、ほかのクラスに影響は与えずにすみそうです。閉じている。
開放閉鎖原則も満たせていそうです。
最後に凝集度と結合度についても見てみましょう。
Observerパターンのバージョンだと、凝集度に問題がありましたが、今回はそこを解決しました。
凝集度と結合度についてもよいでしょう。
というわけで、これでだいぶよさそうです!これでいきましょう!
というところでBパートのまとめに入りたいと思います。
Bパートでは、Aパートで得た「いまある構造をレビューする力」を使いながら、なんども構造を作り直すことでより良い構造を削り出していくのを実践してみました。「1回構造を与えておわり」ではなく、設計原則とデザインパターンを利用してなんども繰り返すことが大切です。
というわけでBパートはおわりです。Bパートが終わると何があるか?そうです。EDです。
ED〜Cパート
もちろんもうわかりますね。歌う余裕がないので、息子の写真です。
さて、ときにはですね、EDが終わったあとにドラマパートが挿入されることがありますよね。通称Cパートです。今日もCパートあります
Bパートで一見よさそうな終わりを迎えましたが、実は未解決問題があります。
push通知にまつわる部分ですね。ここについての考慮が漏れていました。
Cパートではこれどうするか考えていきたいと思います。
ここに関して見ていきながら、「問題次第だよね」って部分について考えたいと思います。たぶん問題の性質によって答えが変わると思っているんですね。このpush通知の仕様って、「コメントがついたってときにはpush通知送るけど、starつくたびに通知送ってたらうざいから送らない」なんでしょうか。それとも、「お知らせがあるときには、それがどんな種類のお知らせでもあってもpush通知送る」なんでしょうか。
もしこれが前者、つまり「コメントの場合は送るけどStarの場合は送らない」とかがある場合、Notificationでpush通知送るんじゃなくて、CommentでやったりStarでやったりしたほうがいいと思うんです。仮にNotificaitonでやっちゃうと、Notificationクラスに「もしも通知がコメント通知の場合は云々」みたいなif文が生えてきて、最初に見た例みたいな単一責任原則を破ったものが出来上がってしまいますよね。
その場合は、push通知を送るか送らないかを知る責務をCommentクラスだったりStarクラスに負わせて、「CommentがNotificaiton作ってさらにpush通知も送る」「StarはNotification作るだけ」ってしたほうが、単一責任原則を満たせてるし開放閉鎖原則も満たせてるし凝集度と結合度も良い状態になりますよね。
一方で、後者、つまり、「お知らせに書き込まれるときは必ずpush通知送る」って仕様の場合、Notificationがpush通知もやったほうがいいですよね。というのも、もしCommentやStarがpush通知もやる場合、push通知の仕様が変わったら両方書き換えないといけなくて、凝集度下がっちゃいますよね。
Notifcationがやってくれたほうが、凝集度が高くてうれしいはずです。
今の話で何が言いたかったかっていうと、実は「設計原則を使って構造をレビューする」って行動自体はかわらなくても「問題が変われば、設計原則を使った結果は変わる」ってことが言いたかったんです。マルチパラダイムデザインに出てきた「問題に対して」ってのが重要で、問題を無視してはいけないわけです。
たとえば単一責任原則について見直してみると、「変更Aでも変更Bでも影響を受ける場合は」って言ってるんですよね。これってつまりどういうことかというと「どこが変更されやすいポイントなのか」がわかってないと、単一責任原則って本来使えないはずなんですよ。今後変更されないところに対して「ここが変更された場合は」とか考えても無意味ですよね。今後変更されうるであろうところに対して「ここが変更された場合はどこが影響受けるかな」「別のここが変更された場合はここ影響受けるかな」というふうに考えるときに初めて単一責任原則が役にたつわけです。それは他の原則についても同じことが言えるでしょう。今日見た設計原則は、どれも「変更があるときに」というような言葉が入っていたはずです。
よく言われる「問題によって適切な構造は変わる」っていう話のポイントはここだと思っていて、「どこが変わりやすい部分なのか」が変われば、設計原則を適用した結果は変わります。今Cパートで見たpush通知の例なんか完全にそうですよね。しかし、同時に重要なことは、それでも「設計原則によって今の構造をレビューして、再度構造を作り直して」 というやり方自体は変わらない、ということです。
というわけでCパートのまとめですが、「設計原則を適切に使うためには、問題をよく吟味して、"どこが変わりやすいポイントなのか"を見極める必要がある」というあたりでしょうか。
実はBパートでやったObserverパターンの例も、「Commentが書き込まれたあとにどんな振る舞いをするか」という点はべつに変化しやすいポイントじゃなかったのに、そこに対する問題解決であるObserverパターンを適用してしまった例なんですよね。あれは「問題を見誤っていたせいでまずい分割になっていた」とも言えると思います。
ちょっと本題から逸れるので、落ち穂拾いとしてですが、重要な補足をしたいと思います。それは、「いや、問題を吟味しろっていったってさ、俺たちは未知の問題に立ち向かってるんだよ」っていうときの話です。たとえば、元号だったらいつかは変わることがわかっていますが、スタートアップとかで「問題そのものを発見しながら新しい問題に取り組む」みたいなときに、未来の変化がどこで起こるかなんてわからないですよね?そういうときには、「KISS原則」と「YAGNI原則」を思い出してください。
KISS原則は、「Keep It Simple, Stupid」の略で、シンプルに書こう、というやつです。「どこが変わるかわかんない」なら、変に「ここに拡張ポイント作っておこう」とかせずに、とにかく目の前の問題をシンプルに記述できるように書いておこう、という話ですね。YAGNIは You Ain't Gonna Need Itの略で、「どうせそれは必要にならないよ」くらいの意味でしょうか。すでに変わることがわかっているのでなければ、「念のためここは変わっていいようにしておこう」と考えるのではなく、「今わかっていることだけをうまく解決しよう」という話です。
ぼくの大好きなリポジトリに「FizzBuzzEnterpirseEdition」っていうのがあるんですけど、これすごくて、FizzBizzのあらゆるところに拡張ポイントを入れたものなんですね。ループすら抽象化されている。これは考えうる限り最悪のFizzBuzzで、そもそもそういうジョークなんですけど、これってまさに「問題を見誤った例」だと思うんです。FizzBuzzっていう本来シンプルな問題に対して、「ここも変わるかも」「ここも変わるかも」を適用しまくった例なんですよね。でも、逆にいうと、本当にその「ここも変わる」が高い確率で起こるのであれば、そこに対して準備しておく、つまり、それを元に設計原則と照らし合わせていったら、このリポジトリは大変に良いコードになるんです。このリポジトリは、そんな「問題を吟味することの大切さ」と「どこが変わりやすいポイントなのかによって設計原則を適用した結果は変わる」を体現するリポジトリで、本当にいいリポジトリっていうか、めちゃめちゃ面白いのでぜひ見てみてください。
また、これは今日の最後の枠にホールで発表されるruiさんのツイートですが、まさにruiさんもこのことを言っているのだと思って、「問題を捉え損ねたままXX原則とか言っても意味ないよね。それより、今ある問題に対して適切な構造を選びとろうよ」ってことを言っているのだとぼくは解釈しています。
結局このメッセージに落ち着くのですが、やはり、コードだけではなくて、問題そのものにも向き合いつつ設計を考えていく必要があると思います。しかし、今日話したのは単に「ケースバイケース」って話ではなくて、「じゃあ個別のケースを検討するときにはどうやってやればいいの?」という話まで射程を深めたつもりです。
というわけで、全体のまとめですが、今日は
- まず、設計とは、複数の選択肢から、問題に対してより良い構造を選ぶことだということができるでしょう。
- デザインパターンをたくさん学ぶことで、自分の取れる選択肢を増やすことが可能になるでしょう
- そして増えた選択肢のひとつひとつを検討するときに、設計原則を使うと根拠を持って選ぶことができるようになるでしょう
- しかし、その設計原則を使うためには、まずは問題を吟味して、「どこが変わりそうで、どこが変わらなそうか」を知る必要があります。
- 未知の問題に対しては、「KISS」「YAGNI」を思い出して、「今わかってること」に集中しましょう
という話でした。
ここで、最初の問いに話を戻して見ましょう。
「ケースバイケースというけど、じゃあどうやって"今回のケースに適切かどうか"を見極めるの?」という問いでした。
その答えとしては、
まず、複数の選択肢を用意する必要があります。デザインパターンを学ぶことで、選択肢をたくさん持つことができるようになります。
その選択肢に対して、設計原則を使ってレビューします。そうすることで、よりよい選択肢がどれなのか、確信を持って選ぶことができるようになるでしょう。
これを繰り返して、より良い構造を削り出していくこと。これが「どうやって適切な構造を見極めるの?」に対するこたえではないでしょうか。
えー、最後になりますが、みなさんお手元の名札にQRコ(ry
質問ありますでしょうか
質問
- 単一責任原則の話なんですけど、最後に作った構造も、「通知の本文が変更された場合」と「コメント欄はもともとタイトルなかったけど、タイトルつけられるようにしましょうみたいな変更がされる場合」で複数の責務もってませんか?
- めっちゃいい話ですね。これはまさに「どこが変更されるか」の話で、より具体的にどこが変更されるかを吟味、検討した結果、単一責任原則の適用結果が変わった例です。そういった変更が予期されている場合、通知の本文作るところを「NotificationBodyBuilder」に切り出しちゃったりするといいかもしれませんね。
- 設計原則っていろんなフェーズがあると思っていて、今回のみっつの原則は構造レビューに使えたけど、KISSとかYAGNIってもうちょっと上のレイヤーだったりしますよね
- めっっっっっちゃいい話ですね!!!まさにそうで、たとえば今回あげたうち単一責任原則と開放閉鎖原則はSOLID原則と呼ばれる原則集団(?)のうちのふたつなんですよ。Sは "S"ingle Responsibility Principle(単一責任原則), Oが"O"pen-Closed Principle(開放閉鎖原則)、Lが"L"iskov Substitution Principle(リスコフの置換原則)、Iが"I"nterface Segregation Principle(インターフェイス分離の原則)、Dが"D"ependency Inversion Principle(依存性逆転の法則)ってやつなんですけど、このうちLとIとDをなんで今回取り上げなかったかっていうと、これはより抽象度が低くて実装よりの話になってて、つまり「一回引いてレビューする」みたいなときよりもむしろ実装時に都度参照したりするといいような性質があるかなって思ってるんですよね。そういう感じで、設計原則にも抽象度の上下があったりして、どういうときに使えるかは異なると思います。それでも、原則は「レビューするときに使える」ということは変わらないと思います。
おわりに
というわけで、プレゼン再現エントリを書いて見ました。なにか質問等ある方は、9/29日に開催されるNDSというイベントでぼくと握手するか、Classiで一緒に働きましょう。ジョイナス!!!! ※宗教上の理由でWantedlyが使えない方は、「ジョイナス」のほうじゃなくて「!」のほうのリンクからぼくにDMを送ってくれたら対応いたします。