#yapc8oji 得票数4位トーク「あの日見たM V WhateverのModelを僕たちはまだ知らない」実況中継
丸山です。ヤパチー、最高に楽しかったですね。スライドの公開はしたのですが、正直重要なことは全部口頭で説明していて、スライドには情報が少ない。しかし、動画を見るのは正直だるい。40分もPCの前で集中して映像を観れないですよね。知ってる。というわけで、受験参考書の「実況中継」シリーズ(知ってます?)方式で、プレゼンをブログで再現します。長いぞ。でも多分動画見るよりははやく読み終われる。ぜひ読んでいってください。
アバンパート
今日はこういうトークをします。
簡単に自己紹介すると、こういうものです。
息子紹介です。かわいいですね。
WEB+DB Press 91号では特集を、92号ではPerlHackersHubに記事を書かせてもらいました。もう原稿料いただいてるので、もうこれ買ってもらってもわたしには一銭も入らないんですけど、いい雑誌なので買ってないひとはぜひ買って読んでください。
まずMVWってなに?って話からしていきたいんですけど、
MVCとかMVPとかMVVMとかみなさん聞いたことがあると思います。これらを総称してM V Whateverと呼んだりします。
違う名前ってことはそれぞれに違いがある一方、MVWとひとくくりにされるのだから共通点もあるわけですね。
共通点としては、これらは全部PresentationDomainSeparation(以下PDS)を実現するためのパターンである、ということが挙げられます。一方、それをどうやって実現するのか、という部分についてはそれぞれに違いがあります。しかし今回はその違いについては立ち入りません。
今さらっと「PDS」といいましたが、ではそもそもPDSとはなんでしょうか?
Martin Fowler先生が書いてるんですけど、
要するにプレゼンテーションとドメインの分離ですね。
「いやそれじゃあそのままじゃん」って感じなんですけど、プレゼンテーションとドメインとはなにかの前に、
Martin Fowler先生が「PDSってのはMVCの最も重要パートなんだよ」と言っていることを確認しておきましょう。そう、MVCはPDSと切っても切れない関係である、ということをここでは確認しておきましょう。
で、じゃあプレゼンテーションとドメインってなに?って話になるんですけど、まあプレゼンテーションはなんとなくイメージがつきそうですね。ユーザーが見れて触れる部分、みたいな感じだよな、って思いませんか。まあだいたいそんなもんです。
一方、ドメインって聞くと、「あっDDDでやったところだ!」ってなりがちなんですけど、PDSの文脈におけるDomainとDDDにおけるDomainってのはたまたま名前が同じなだけで、指してるものは違います。
じゃあPDSにおけるドメインってなに?って話になるんですけど、
MVWのModelに相当するのがドメインです。
とはいえですね、これって言葉を言い換えただけで、「じゃあMVWにおけるモデルってなんなんだよ」という問題は依然として残るわけですね。
よく言われる説明としてはこんなのがありますね。ただですね、
そうすると「ビジネスロジックって結局なんなのだ、宇宙とは、人生とは」みたいな哲学に迷い込むわけです。この会場で「ビジネスロジック」の定義言えるひとっています?……あ、いないですよね。わたしも言えません。
死って感じですね。お疲れ様でした。
でもこれみなさんが悪いわけじゃないと思うんですよ。そもそもビジネスロジックということばで指してるものがみんな違ったりするんですよね。そんな状態で、「ビジネスロジック」って言葉を使っちゃって名前がついちゃうと「なんだかわかったつもり」になったまま実はなにもわかってない、みたいなことになりがちなんですよね。もうだから今日は「ビジネスロジック」って言葉忘れちゃった方がいいと思います。
何が言いたいかっていうと、「モデルとは何か」ってのを言い表すのはすごい難しいんですよ。
というわけでですね、わからないときは分かりやすいところから考えましょう。
まあViewはわかりやすいですよね。iOSだったらUIViewがあるし、AndroidにもViewがある。webAppだったらHTMLとCSSで表現されるのがViewですよね。
実はWの部分もわかりやすくて、こういうやつらです。これらが何を担っているかっていうと、ViewをレンダリングしたりViewに値突っ込んだり、あるいはユーザーからの入力を受け取るみたいなこともやりますよね。
で、PDSに立ち返ってみると、ファウラー先生は「(the user interface)」っていってますよね。UIって見た目だけじゃなくてユーザーがボタン押したら云々みたいなユーザーインタラクションも含むものだってのは開発者のみなさんはよくご存知だと思うんですけど、それと照らし合わせて考えると、
VとWの担ってる責務こそが「プレゼンテーション」であるってことなんですよね。で、VとWでプレゼンテーションを実現するわけです。で、WにはCとかPとかVMとかいろんなものが入ってくるんですけど、これは要するに「どうやってプレゼンテーション層を書くか」のやり方をいろいろパターン化してくれてるわけですね。
じゃあ残りの、MVWでいうところModel、PDSでいうところのDomainっていうのはどんな責務を担当するんだろう、って話についに切り込んでいきます。
PDSにみたび登場してもらうとですね、赤字で書いたところ注目してください。「the rest」って書いてありますよね。
まあこういうことです。
ちーん、結局なんなのかわかんない!って感じですよね。
でもそれは当たり前なんですよ。そもそもPDSってのは、「プレゼンテーション層とそのほかを分けよう」って考え方だし、この「その他」が何を担うべきなのかってのはアプリケーションによって変わるわけです。つまり、これ大事なことなので何度も強調しますが、
そもそもMVWは「プレゼンテーション層をどう設計するか」のパターンであって、Modelをどう設計するべきかについては何も語っていないパターンなんですね。
はい、ここまでがアニメとかでいうところのアバンパートです。というわけでオープニング、タイトルコールどん!
Aパート
はい、こういうタイトルでしたね。MVWはModelの設計指針についてなにも語っていないことは確認できました。そうすると次の疑問として「じゃあModelはどうすんのよ、なんの指針もなしにどうやって設計すんの」って話が出てきます。そういうときに力になるのが様々なデザインパターンです。デザインって設計のことですよね。そういうパターンをたくさん知ることで、「ははーんこれはxxパターンを適用すべき事案ですね?」とか見えてくるようになるわけです。
今日はそんな中でみっつほどパターンを紹介しましょう。
まずはActiveRecordパターンです。これRailsのおかげでみなさんも結構よく知ってると思うんですけど、
こういうパターンですよね。
ただ、Railsで使われてるActiveRecordライブラリを使えば自動的にARパターンが実現できるわけではないんですよ。
たとえばこういうコード見てみましょう。Controller で AR::Base 継承したNotificationから、unreadなものを引いてきて、それを全部readに更新してます。いわゆる「mark all as read」ってやつですね。
ところで、ControllerってPDSでいうとPとDどっちに属すやつでしたっけ?プレゼンテーション層ですよね。でも、この「DBからレコード引いてきて更新する」って、全然ユーザーインタラクションじゃないですよね。そこで、ARパターンを適用してみます。
はい、テーブルに対する操作なのでクラスメソッドとして実装しました。いいですね。(あっ、これActiveRecord::Base継承しわすれてますね、してると思って読んでください)
注目すべきはcontrollerです。これ、ユーザーからリクエスト、つまり入力を受け取って、Modelに「あとはまかせた!」ってやってますよね。これはユーザインタラクションの実装で、プレゼンテーション層の責務の範囲に収まってると言っていいと思います。
こうしたことによって、なにが嬉しいかというとですね、実はプレゼンテーション層ってのはだいたいテストしにくいんですよね。RailsだったらRack経由でテストしないとダメだし、iOSアプリとかだとViewとかViewControllerが絡むと一気にテストしにくくなりますよね。でも、Cの責務をユーザーインタラクションだけに絞ったことで、あとはRackとかそういうテストしにくいところ経由しないでテストが書けるようになりました。嬉しいですね。
あと、もう一個いいことがあります。Cにいろいろ書かれちゃうとそこ再利用するの難しいんですけど、こうやってmodelに書いておけば、controllerからだけじゃなくてrakeタスクとかからも呼べるようになりますよね。これって、「プレゼンテーション層を入れ替えてる」ってことなんですよ。このアプリケーションを使うクライアントからみると、RailsのViewとかControllerで実現してるのってHTTPを喋るプレゼンテーション層なんですよね。一方で、CLIのインターフェイスを実現するrakeタスクなんかからこのModelを使えば、rakeタスクの層が「CLIを喋るプレゼンテーション層」になるってわけです。こうやってプレゼンテーション層をぽこぽこ入れ替えることができるのも、PDSに沿うことで得られる強みですね。
とはいえ、現実は厳しくて、「これでめでたしめでたしやで!」とはなかなか行かないんですね。よくある課題としては、複数のテーブル触るようなユースケースをどっちに書くのか問題ってのがありますよね。あるいは、ARを継承したモデルの中でトランザクション開始しちゃうと、そのメソッドをほかのところからも使おうとすると多重にトランザクションが開始されちゃってうわあみたいな感じになったり、みなさんも経験ありませんか?
そういうのむりしてActiveRecordパターンで扱おうとしていろんなところにぐちゃーっと詰め込んでいくと、
まあメンテ不可能なRailsアプリが出来上がるわけですね。はい、お疲れ様でした。
じゃあ、何がいけなかったのか考えてみましょう。そもそもActiveRecordパターンってのはどんなパターンでしたっけ?「テーブルと」クラスが1:1なんですよね。そもそもテーブルとクラスが1:1なんだから、複数テーブルにまたがった関心を扱うには「食い合わせのわるい」パターンであるってことが言えるんじゃないでしょうか。実際、実践DDDっていう最高の本があるんですけど、この中でも「単なるCRUDアプリならRailsでいいかもね」って言ってて、要するにActiveRecordパターンが対象にするのってCRUDの範囲だけなんですよね。つまり、
ARパターンだけでこの世のすべての問題を解決しよう、というのは無理があるわけです。
ちなみになんですけど、RubyのARライブラリってARパターンをもとにしたライブラリなんですけど、単なるARパターンじゃなくてもっといろんなことやってくれるんですよね。だから、RoRの界隈ではARライブラリベースでやっていこうという考え方もあるにはあるっぽいんですけど、あまりこれでうまくいったという話を聞かないような気がします。逆にARライブラリベースで上記みたいな問題を解決したぜっていうのは興味ある話なので、そういう話知ってる人はぜひ教えてください。皮肉とかじゃなくてまじで聞きたいです。
ええと、話を戻して、ARパターンですべての問題が解決できるわけではないということを見てきました。そこで、別のパターンとしてTransactionScript(以下TS)ってのを見てみましょう。
TSってのはユースケースと一対一になるメソッド定義して、そのメソッドの中に全部手続きを書いていくやつです。
たとえば、航空機の座席予約について考えてみましょう。まず、
- その席が誰かに先に予約されてないかチェックして
- ビジネスクラスの料金でファーストクラス予約しようとしてないかチェックして
- 場合によってはパスポートのvalidityチェックして
とか、いろいろとやるべきことはあるわけです。それを全部上から下に書き下していくスタイルですね。
これを適用すると、こんな感じのコードが出来上がってきます。
いろんなモデルを使って、ユースケースをがんがん書いていくわけですね。トランザクションもこのメソッドの中で開始してcommitすればいいです。
ちなみに、これはTSとARの合わせ技になってますね。
ここでもcontrollerに注目してください。入力を受け取ってTSのメソッドをdispatchしてるだけです。TransactionSctiptパターンで作ったこのNyanTransactionクラスもModelの一種であることに気をつけて下さいね。再度確認しますが、Modelというのは「プレゼンテーション以外全部」なのだから、当然ARを継承してないこのNyanTransactionもModel層のものです。だから、この場合もCは入力を受け取ってMの処理をdispatchしてるだけなわけです。
で、こういうふうにすると何がうれしいのかって話なんですけど、ARだけでは実現できてなかった複数の関心をうまくまとめられてるし、トランザクションもここに書いておけばバッチリ良さそうですね。
とはいえやっぱり現実は厳しくて、まあ一定以上複雑なアプリケーションをTSパターンでとこうと思うと、似たようなロジックがあちこちにコピペされて死んだり、ひとつのメソッドの行数数えたら10億行あってそれ読むのに5億年かかるしもはやテスト不能みたいな状態になるんですよね。
はいお疲れ様でした、という感じです。
とはいえですね、そこまで複雑でないアプリケーションならば、TSってすごく見通しがいいんですよ。どっかがバグったら、そのユースケースと1:1になってるメソッド読みに行けばいいって一瞬でわかるし、そもそもそのメソッドも「あれしてこれしてそれする」ってことしか書いてないから、複雑じゃなかったらむしろ読みやすいんですよ。あと、余計な設計しないで済むんで、初期開発速度も速いわけです。
そうなってくると、たとえば「とにかくバギーでもいいから早くリリースしてくれ!そんである程度までユーザー数伸びたらどっかに売ってあとメンテしてもらおう!そんで遊んでくらそう!」みたいなビジネス要請がある場合にはまあ倫理的にどうかって問題はあるにせよ、TSでドバーって書いちゃうほうがよかったりするんですよね。一方で、「複雑な内容だから急がないんだけど、ずっと直しながら使っていくシステムをしっかり作ってほしい」っていうようなやつをTSで設計したらちょっとまずいわけですよね。
そんな感じで、モデルをどう設計するのかってのは実は「どういうビジネス要請があるのか」みたいなところと深く関わっているわけですよ。だから、モデルを設計するときには各パターンのメリットデメリットを知って、そしてそのパターンが何を解決しようとしているのかを知って、その上で「こういう要請があるから今回はこのパターンを使っていこう」というように設計を進めていくべきなんですね。
だから「TSはクソ、時代は○○」みたいな話ではないんだよ、ってのを強調しておこうと思います。
まあとはいえですね、TSが適さない問題領域、ビジネス要請っていうのはあるわけですよね。複雑でなおかつ堅牢でなければならないみたいな。
そういう領域を解くときには、Layered Architcture(以下LA)を検討してみたらどうでしょうか。
どういうものかっていうと、こんな感じです。プレゼンテーション層はいいですよね、その下が全部モデルになるわけですけど、この層を、みっつに分けます、まずはCがdiapatchする窓口となるApplicationServiceっていう層があって、あとDBアクセスとかそういう汎用的な技術を実現するレイヤーをInfrastructureって定義して、それ以外をすべてDomainModelという層にやってもらう。そういうパターンです。
これ何がいいかっていうと、InfrastructureがDBアクセスとかやってくれるから、DomainModelはほんとに解きたい問題に集中できるんですよ。
たとえば、タスク管理システムを考えてみましょう。
そのタスク管理システムは、一回だれかがアサインされたタスクに関しては二度と「だれもアサインされた状態」には戻せないってことにしましょう。これはマネージャーはだれかをアサインしたと思って安心してたらだれかそれをさし戻しちゃって、誰も誰がボールを持っているのかわかんない宙ぶらりんのままのタスクができるのを避けるためです。あと、一回クローズされたらもう二度とオープンできないようにしちゃいましょう。「一回クローズしたのは終わったんだからそれは終わったものとして扱う!!!再オープンしたい?それは修正ではなくて変更です!新しいチケットを切ってください!」ってことですね。
まあそういう「このアプリケーションが解きたい問題領域」ってのをDomainModelにOOでコードでモデリングしていきます。
そんで、さっき定義したTaskStatusを利用するTaskクラスをこんな感じで定義してみましょう。
このシステムではタスクはマネージャしかオープンできません。権威主義的で最悪のサービスですけど、まあ例として許してください。そういう知識をTask.openメソッドの中にこうやって書いていきます。あとはまあ普通にオブジェクト作って返してますね。
あるいは、Taskをだれかにアサインするメソッドみてください。やっぱり権威主義的で最悪のサービスなんですけど、だれかにタスクをアサインできるのはマネージャだけです。あと、Taskの状態遷移はできる遷移とできない遷移がありましたよね?それをモデリングしたTaskStatusを利用して、「ちゃんとこれは許された遷移かな?」とかをチェックしたりしてます。
ここで注目してほしいのは、TaskとかTaskStautsみたいなDomainModelに定義されてるクラスは、自分がどこに永続化されてるとか気にしてないことです。そういう技術的な詳細を避け、「複雑な問題」に集中して丁寧にOOで設計して責務分割していくわけです。
そうやって丁寧に責務分割したとしても、オブジェクトがメモリに乗ってるだけではまああまり意味ないですね。永続化はどうすんの?という話があります。今回はリポジトリーパターンってのを使ってみましょう。リポジトリパターンってのはどういうやつかっていうと、なんちゃらリポジトリにDomainModelで定義したなんらかのオブジェクトを渡して永続化してもらったり、永続化されてるところから情報引っ張ってきてそれを使ってDomainModelで定義したクラスのインスタンスを返してくれたりするようにするパターンです。今回だったらTaskをRDBに保存するTaskRepositoryってのを定義してみました。
で、ApplicationServiceがDomainModelとInfrastructureを利用して、ユースケースを実現します。ここに定義したメソッドがControllerとかからdispatchされるわけですね。
こういうパターンを利用することで、TSのときは一枚岩でメンテ不可能だったような複雑な問題を適切に分割して統治することができそうです。
一枚岩のスクリプトをテストするのは非常に困難ですが、今回はDomainModelをきちんと設計しているので、単体テストも非常にしやすいですね。
あと、DBアクセスをInfrastructureに追いやったことで、複雑な問題を解く部分、すなわちDomainModelの層はDBなしでテストできるようになりました。最高ですね。
とはいえやっぱり銀の弾丸はないわけで、これって非常に難しいんですよね。そもそもLAに慣れてないプログラマにとっては「これどの層に書けばいいの?ApplicationService?DomainModel?」みたいな話になりがちです。
まあそれだけならともかく、OOって本質的に難しいと思うんですよ。さっき私が書いたTaskStatusとTaskの責務分割もほんとにあれでよかったんですかね?遷移先を渡すとそこに遷移できるかどうかをboolで返すんじゃなくて、TaskStatus#transit_to_assignedみたいなメソッドにして、そいつが繊維先の状態であるTask::Assingedのインスタンス返すようにする。そんで、もし行けないステータスに行こうとしてたら例外吐くみたいなほうが「チェック忘れ」とかなくなるしいい設計なのでは?
そんなわけで、とにかくやっぱりOOって難しいんですよ。そうするとドメイン層に関してはプログラマとしてのレベルを上げて物理で殴るとか、あるいは複雑な問題領域に対する深い理解とかが必要になってくるんですよね。
ほんとにつらいですね。はい、お疲れ様でした。
とはいえですよ、われわれはもともと「複雑な問題」に立ち向かうためにここまでやってきたわけです。そもそも難しい問題を相手にしているのだから、そこはがんばって、歯を食いしばってやっていくしかないんですよね。こういう複雑な問題を解くときにこそ、OOへの深い理解とか、あるいはDDDへの理解みたいなのが役にたつのだと私は思います。やっていきましょう。
ちなみにFAQなんですけど、聞きかじりでこういう話を「これって要するにDDDでしょ?」っていう向きもあるんですけど、DDDとLAは同一視しないほうがいいです。DDDの中でLAに触れることはありますが、DDDはそれだけではなくてもっといろんな概念を含んだ、より広い概念です。むしろLAはおまけというか、DDDを生かすための前提みたいなところがあると思います。単なるLAをDDDと呼んだりするとDDDの怖いひとにはてブとかTwitterで怒られるみたいな光景をよく目にしますので、気をつけてやっていきましょう。
はい、ようやくAパート終了です。というわけでアイキャッチどん!
あー、かわいいですね。
Bパート前のアイキャッチもどん!
あー、かわいいですね。不審なお兄さんもいますね。
Bパート
はい、つづいてBパートです。Aパート結構概念的な話多かったんで、GUIアプリを例に実際になにかを作ってみましょう。
こういう連打マシーンはどうでしょう。ボタンをタップすればカウンタが1進んでいくのでガンガン連打してくれ!!ってやつです。ただひとりでやってても面白くないので、ネットワーク越しにいろんな人が同時に連打します。で、手元のカウンタには自分の連打数じゃなくて、「みんなでどれだけ連打したか」が表示されます。
ちょっと実装方針を考えてみますが、ボタンを一回タップしたごとにHTTPリクエストとか飛んだらまじで目も当てられないという感じなので、クライアント側では連打をバッファリングしておいて、一定時間ごとにそれをflushするようにしましょう。
とはいえ、タップした瞬間にカウンタが動かないとUXとしては最悪なんで、タップすると、「実際にはまだサーバに送ってないんだけど、見た目の都合でカウンタは1増える」というようにしましょう。
あとは、サーバーから定期的に「現在のみんなの総連打数はこれだよ」って送られてきて、それを受け取ったらカウンタをその数字まで進める、という実装方針でいきましょう。
単なる連打マシンなんですけど、実装がちょっと複雑になりそうですね。そもそもRDB関係ないアプリなんでARパターンはマッチしなそうです。バッファリングとかいろいろあるからTSもマッチしなそうですね。LAっぽく考えてみましょう。
まずはModelを考えていきます。
なにはともあれ、カウンターを表すクラスが必要ですよね。タップされたときは1増えるのでincrementメソッド生やしておきましょう。あと、サーバーから数字送られてきたらそこまでいっきに飛ばないとだめなんで、setCountメソッドも生やしておきましょう。こいつらが内部に保持したcountを操作します。
はい、こんどはタップ数をバッファしてくれる君です。タップされたらカウントをインクリメントして、flushメソッドが呼ばれたらflushします。今回はプレゼンの時間の都合上サーバーにアクセスする部分は省略しますが、本来ならもっと真面目に書かないとダメです。
で、あとはサーバーから送られてくる値をさっき作ったカウンターにセットしてあげる君ですね。connectToServerすると、一定時間おきにサーバーから値が送られてきて、それをcounterにセットしてます。
これもプレゼンの時間の都合上バッサリ実装カットしてますが、もっとまじめに書かないと本来はダメです。
さて、あとは、これらを束ねてユースケースを実現するApplicationServiceを書きましょう。
プレゼンテーション層からconnectToServerが呼ばれたらfetcherを起動します。起動しておけばあとは勝手にcounterがアップデートされます。これはコンストラクタに書いてもいいかもですね。
ボタンがタップされたらこのtapメソッドを呼びます。そうすれば、カウンタは1増えるしバッファもされます。
あとは、プレゼンテーション層が一定時間おきにflushBufferメソッドを呼んであげれば、サーバーに値が送信されます。
さて、これでアプリケーションの挙動はモデリングできました。
こんどはプレゼンテーション層を書いていきましょう。
こんな感じで、Viewがロードされたら、さっき作ったapplicationServiceをセットアップします。あとはタイマーイベント仕込んで、定期的にapplicationServiceのメソッドを叩いてあげます。また、tapされたというイベントが起こったらapplicationServiceのtapメソッドをdispatchしてあげましょう。
タイマーイベントってここに書くの?という疑問も上がりそうですが、タイマーってアプリケーションに対する一種の入力だと考えることができると思いませんか?そう考えるとPresentation層にあるのが自然な気がしますね。あとこのほうがModelのテストしやすい。
まあこんな感じで、プレゼンテーションからイベントを受け取ってapplicaitonServieをinvokeするところまでできました。
ただですね、これひとつ問題があって、これだけだとタップしてもタイマーイベントが走ってもサーバーから値が降ってきても、Viewが描き変わらないんですね。内部でModelの状態が変わりまくっていくだけで、画面が一切動かない。これはあかんですね。
で、よく考えると、今回はCounterモデルの値が書き換わったときに、それに合わせてUILabelも書き換えればそれだけでよさそうです。
なんかの状態が変わったことを検知して、なにかをしたい。
Observerパターンでやったところですね!
というわけで、CounterをObservableにしてあげましょう。
値が書き換わったら、notifyメソッドを読んで、自分を監視してるひとたちに状態を通知してあげるわけです。
さて、それに対して、VC側ではこのCounterをobserveして、状態変化を検知して、その結果をUILabelに反映するようにします。
いいですね。これでひとまず完成です。
これ、Observerパターンを利用してModelの状態をViewControllerに開示したことが結構ポイントです。CounterモデルがViewControllerやUIViewを保持して、Counterの値が書き換わったときにそれらのメソッドを読んでViewControllerやViewに変更を伝える形にしてしまうと、CounterがViewModelやViewに依存してしまします。つまり、CounterをテストしようとしたときにViewControllerやViewが絡んできて、めっちゃテストしにくくなりますよね。
でもObserverパターンを使えば、「Modelの値や変更をPresentationに開示はするが、Presentationに依存しない」という状態を保つことができます。実際さっきのCounterクラスってimport Foundationしてるだけでしたよね。テスタブルです。
あとですね、モデル層がCocoaTouchフレームワークに依存しなくなったことで、モデル層は熟練のiOSアプリプログラマ以外にも読めるし書けるような内容になってますよね。これっていいことですよね。レビュー可能な人間が増えるし、分業もしやすいし。実際さっきの連打アプリのモデル層、iOSアプリ書けない人でも内容は理解できたと思います。
実際ファウラー先生も、冒頭で紹介した記事でこんなこと書いてて、「あー、PDSが実現できて最高だね、よかったよかった」って感じですね。
というわけで感動のエンドロールにいきましょう。このspecial thanksって書かれてるのはプレゼンの練習に付き合ってくれた友人たちです。あ、あと聴いてくださってるみなさんにもspecial thanksを。いやー、よかった、とか思ってたらですね。
ディレクターから割り込みが入るわけですよ。エンドロール止めましょう。最悪です。
なにかっていうと、今の実装だと、サーバーから数字が送られてきたときにカウンタの数字が4から10000とかにいきなり飛んだりするわけですよ。まあそれは「間違い」とか「バグ」とはいえないんだけど、最悪のUXだし、まあユーザはびっくりして「これバグじゃん」って思っちゃいますよね。たしかによくない、でもディレクタさん、あなたはプロなんだから「バグ」じゃなくて「よくないUI」くらいに言ってくれ、たのむ、という感じですね。
まあそんな感じで、現実はあまくないですね、エンドロールなんて見てないでまだ仕事しないとダメそうです。お疲れ様でした。
さて、とはいえ、やっぱりいいアプリ作りたいんで、UIを改善しましょう。これってUIの話なんで、プレゼンテーション層でやっていきますよ。
まず、UILabelをサブクラッシングしたカスタムなCounterLabelってのを定義します。これは内部に「ほんとのカウント」と「今表示してるカウント」を持ってて、ほんとのカウントがセットされたらタイマー起動して表示してるカウントのほうをタイマーで1ずつupdateしていきます。で、表示してるカウントがほんとのカウントにおいついたらタイマー止めます。
これ、ロジックですよね。「どう見せるか」っていう関心なんで、Presentationの関心ですよね。いわゆる「プレゼンテーションロジック」です。これはViewに閉じ込めてしまいましょう。
で、VCで直接UILabelに値をセットしてた部分も、この変更に合わせて変更しておきましょう。これで、サーバーから値が送られてきたときにはいきなり数字が飛ぶんじゃなくて、見た目上はすごい勢いで数字が増えてるみたいに見えるようになったでしょう。めでたしめでたしですね。
ちょっと話がずれますが、今回はViewにプレゼンテーションロジックを書きました。MVPならPresenterにこういうロジック書けば、そこがテスタブルになっていいですね。
さて、MVPにするのがさっきみたいな古典的MVCにするのかはともかく、今回、UI上の問題を解決するときにはPに手を入れただけで、アプリケーションの本質的な問題をモデリングしたDには一切手が入らなかったことに注目してください。こんな感じで、PDSを守っているといろいろいいことがありますね。
とはいえ、実はこのアプリもまだまだぜんぜん完璧ではありません。
たとえば、今回ってサーバーから送られてきた数字をカウンタにセットする君をModelに書いちゃいましたけど、これって本当にそれでよかったんですかね……?
タイマーイベントがアプリケーションへの入力だと見なすのが自然なように、サーバーからの入力もアプリケーションへの入力であると見なすのが自然なのではないでしょうか。そうするとこれはプレゼンテーション層で管理すべき関心の気がします。
それを考えると、プレゼンテーション層にもう一個、ActionProviderみたいな層があってもいいかもしれませんね。ここには、タイマーイベント発行してくれる君だとか、サーバーからpushしてくる値を受け取ってイベントを発行してくれる君みたいなやつを定義しておいて、VCがこいつらを利用してモデルに処理をdispatchしたほうがいいかもしれません。
あと、今回Observerパターンを結構重要な要素として使っていますが、実はObserverパターンってmutableな世界が前提のパターンなんですよね。それは当たり前で、変化しないものは監視する必要がないからです。しかし、よく言われるように状態のmutationは問題を複雑にします。最近はだから「なるべくimmtableに寄せていこうぜ」というのがトレンドになってきてますよね。それを考えるとObserverパターンを素朴に使ってるのはまだまだ改善の余地がありそうに見えます。
あと、今回API呼び出しダミー実装でDomainModelに置いちゃいましたが、API呼び出しって本来Infrastructureがもつべき関心ですよね。DBとのやりとりと同じく、外部システムとのやりとりっていう技術的な詳細ですし。そう考えるとやっぱりAPI呼び出しの部分はInfrastrucreに持つべきなんじゃないかな、とかいろいろ考えるわけです。
あと、これは今回のGUIアプリの例とはあまり関係ないんですけど、Repositoryパターンってクエリと相性最悪なんですよね。「いろんなテーブルをjoinした上で特定のカラムしか必要ない」みたいなやつって、DomainModelで定義したオブジェクトに綺麗にマッピングできないことが多いです。あと、クエリの種類によってRepositoryにファインダーメソッド定義しまくっていくと、異常な量のメソッドがRepositoryに生えてきて破滅します。
テーブルと1:1で設計できたARのときはまあよかったんですけど、Repositoryパターンを使ったことで、ここにきていわゆるOOとRDBの間のインピーダンスミスマッチ問題に悩まされるようになったわけです。つらいですね。
で、こういうことを考え始めると、いろんなことが気になってきます。さっきのActionProviderとか、immutableな設計とか考えると「おっこれってFluxアーキテクチャに近づいて行っていないか?いっそFluxにするべきか?」とか、「リポジトリパターンでうまくクエリが扱えないなら、コマンドとクエリを別にモデリングしちゃうか、あ、それってCQRSだよな」とか「じゃあいっそEventSourcingまで持って行ってしまうか?」とか、まあいろいろと本当に考えることが増えてくるんですね。これは完全に設計沼です。
そういうときには、「わたしたちはどんなビジネス要請によってアプリケーションを開発していて、このアプリケーションは何を解決するためのものなのか」ということに立ち返ってみましょう。単なるCRUDアプリケーションに対してCQRSを適用するなんて馬鹿げた話です。今からとても大事なことを言いますが、世の中には「最高の設計」なんてなくて、問題や課題に対する「最適な設計」があるだけなんです。だから、最初の話に戻りますが、「万能なモデルの正しい設計」なんてないんです。
そんなわけで、ここから先は、みなさんの作るアプリケーションがどのような課題に対するものなのかによって物語が変わってきます。「ここからさきは君自身の目で確かめる」しかないわけです。つらい。つらいですが、ここで唐突にゼルダの伝説に出てくるトライフォースの話をしましょう。
トライフォースはみっつに分かれているのですが、そのみっつとは、力のトライフォース、知恵のトライフォース、勇気のトライフォースです。
様々なパターンを知り、そのパターンのメリット、デメリット、そして何を解決するパターンなのかを知れば、それはあなたの力になります。力のトライフォースですね。
ただ、力があるだけではダメで、今度は課題に対してどのようにその力を使っていくのかを知恵を絞って考える必要があります。これが知恵のトライフォースです。
そして、正直いって、複雑な現実に立ち向かうのはかなり厳しいです。勇気を持って、力を知恵を運用することで初めて、良い問題解決が生まれるのではないでしょうか。
まとめます。
- PDSを心がけることでいいことがたくさんあります
- MVWはPについては設計指針をくれます
- Mについては設計指針をくれません
- Mについてはあなたがどのような課題に向き合っているのかによって適切な設計が変わってきます。
- 大変な旅になりますが、トライフォースがあなたの力になるでしょう。
最後に宣伝させてください。わたしが働いている株式会社リラクでは、普段からこういう話をプログラマ同士でやりながら開発を進めています。みなさんの会社もそうかもしれませんが、正直いって無限にやるべき仕事あるので、仲間が欲しいです。一緒に、課題に対して最も適切な設計を追求していきたい仲間を募集しますので、「あっなんだか雇用が流動化しそうになってきた」みたいな方は是非声をかけてください。一緒に悩みながらやっていきましょう。
宣伝で最後終わるのちょっといやなんで、またまとめのスライド写して、トークを終えさせていただきます。ご清聴ありがとうございました。
Q & A
- Q「ActiveRecordパターン使ってるとViewで複数テーブルから必要な値だけ引いてきてみたいなときにつらいことがあるんだけど相性悪いの?」
- A 「そのクエリとARパターンの相性がわるいと言っていいと思います。わたしはそういうときは、クエリに関してはSQL直接書くレイヤーである「ReadLayer」っての作っちゃって、そこでSQL組み立ててそのViewに特化したDTOにレコードぶっ込んでPresentation層に渡しちゃったりしてますね。
結び
以上です。どうだったでしょうか。最後にちょろっと出てきましたが、弊社ではプログラマを募集しています。興味のある方は是非ブコメなどに「興味あるな〜。@your_twitter_screenname まで連絡くれないかな〜」とか書いておいてください。あるいは @neko_gata_s にリプライをくれてもいいです。一緒に悩みながらやっていく仲間がほしくてたまらないんです!よろしくおねがいします。