tomykaira makes love with codes

http://d.hatena.ne.jp/ToMmY/

All snippets without notes are distributed under MIT License.
特記なき場合、コードスニペットは MIT License です。

Rails、あんたなんか嫌いよ - Rails での OO 設計について

最近はずっと Rails 書いてるんですが、書けば書くほど嫌いになってくるんです。 倦怠期的なやつなんですが、 Rails さんの悪いところばっかり見えてきて、もう一緒にいたくないんです。 でも別れるほどじゃないし…

という愚痴にみせかけた Rails での設計についての議論です。 長いけどコードは一切出てこないので通勤中にでもよんでください。

注意

一部にはげしい言葉遣いがでてくるので、読んで不快になるかもしれません。 不快になったとしても責任は負いかねます。

次のような方の期待に沿う結論はでません。残念でした。

  • Sinatra, Padrino の人
  • 関数型の人
  • 静的型付けの人
  • C の人

TL;DR

Rails にだまされない。 自分の道を見定める。

欺瞞にみちた Rails の世界

Rails はほんとうに優れたフレームワークなのか、主観的および客観的に検討してみましょう。 以下ででてくる話は、ちょっと誇張されていますが、だいたい実話です。私の2年くらいの Rails の経験で遭遇した事態です。

Rails は「プログラマの幸福と持続可能な生産性に特化したフレームワーク」だそうです(http://rubyonrails.org)。

信じられる?

私には選挙公約なみに信じられない。

"Rails bad practice" v.s. "Rails good practice"

ぐぐって結果件数を比べてみました(google.co.jp, 2013.6.25, w/o personalization)。

  • rails bad practices: 6.75M 件
  • rails good practices: 4.63M 件

Rails は bad practice を導くフレームワークだった! 納得できる結果ですね。

rails best practices だと 6.29M 件なのですが、これはサイトと gem の名前なので、バイアスがかかってると判断しました。 rails best practices はぜんぜんベストじゃない場合があるので、鵜呑みにしないほうがいいです。騙されるな。

bad practices の真相

最近みた Rails 関係の記事は、「Rails の xx という問題に対処するにはどうすればいいか」という形式がほとんどでした。 適当に履歴から持ってきているので、全部よむ価値があるかというと、そんなことはありません。

どうやら、 Rails は自然につかっていると問題の発生するフレームワークなので、 gem をいれたり書きかたを変えたりしてどうにか問題に対処しないといけないのです。

これは実感とも符合します。 Rails の開発とは、「機能追加 → 腐る → 対策を探す → 直す」の繰り返しです。

Rails が full stack だからと調子にのって、Rails のすばらしいとおぼしき機能を盲目的に使っていると、やがて stuck します。 テストが書きにくくなり、テスト実行に10分かかるようになり、拡張性がなくなり、どこからともなく例外が飛んできて、本番で 500 が出ます。

Rails 解説サイトや SO の適当な説明にまどわされ、場当たり的な解決法をとったばかりに、自力ではどうしようもない混乱におちいります。 あなたは Rails の豊富すぎる機能のなかで自分をみうしなっています。

gem 地獄

対策記事では、しばしば「こういう問題に対処するために AmazingGem というの作ったから使いなさい」という内容の場合があります。

そうしてあなたの Gemfile は100行を越え、起動は死ぬほど遅くなり、 app の下には触ったこともないディレクトリができて、テストは pending "implement here" で埋まります。 親切で作ってくれた migration file が山とたまり、なんのためにあるのかよくわからないカラムが追加され、後から外そうとおもってもどうしようもなくなってます。 3段階くらい依存関係をおったところでよく知らない gem が勝手に追加され、読み込まれ、 BaseObject が汚染されます。 最悪の場合は dev ではロードされるけど production ではロードされない gem が implicit な動作をしていたせいで、本番で落ちたりします。

gem は車輪の再発明を避けるから、どんどん導入すべきだというのは幻想です。 宝石は装飾品です。多少のアクセサリはエレガントになりますが、宝石だらけの人物に魅力があるでしょうか。

とくに Rails に特化した gem の場合、モデルを増やしたり controller を作ったり、好き勝手なことをします。 out of box で使えると書いてありますが、大抵すこし変更したくなるものです。 変更するには monkey patch するか、似た名前で元のテーブルを参照するとか、fork して自分バージョンを作ります。 もとの gem のバージョンが上がって壊れ、書いた人しかわからないから直せないという結果になります。

最初から minimal な実装を自分で書いたほうがどれだけすっきりするでしょうか。 RubyRails は記述力にすぐれた環境なので、大抵の機能はクラス3つも切れば実装できます。 gem の lib のしたにファイルが30個くらいあるのは、設定を増やしたり、あなたが使わない機能も実装してたりするからです。 なんでも gem に頼るのは、実装力の不足を恥じるべきです。

ところで、 gem 追加するときにそのソース読んでますか? rubytoolbox や github でパブリシティや更新頻度を確認してますか? セキュリティだなんだと言いながらそのへんにころがってる、100人くらいしか使ってない、テストも足りてないようなライブラリをどんどん Gemfile に追加するのは滑稽です。

Being paranoid with Ruby gems - Gemnasium Discussions

あなたは、Rails だけではなく、 gem 作者と、それを無意味なサンプルアプリケーションとともに唱道するブロガー達にも騙されています。

設計してますか

Rails はあまりに巨大で、メジャーなフレームワークなので、情報の洪水、機能の洪水に簡単にのまれてしまいます。 だれでも簡単に書けるあまり、安易にコードを書くようになり、それでもなんとか動く、という成果物ができあがります。 とくに勉強中はそうです。

流されない自分を作るには、どうすればいいのか。 一歩上の Rails プログラマになる方法を提案します。

真剣に書く

  • 「この機能必要だから
  • とりあえずテスト書こう
  • ここにこんな感じで書いてみるか
  • あ、動いた
  • よっしゃー終わり」

これはプロフェッショナルの開発ではありません。 3日で書いて、 twitter で1日話題になって、1週間後には作者もログインしないサービスならこれでいいのですが、 どこかに納品したり、数年にわたってサービスを提供していこうというもののとるべき態度ではありません。

実装の前に

必要な機能の詳細は与えられているものとします。 これを決めるまでにはユーザインタビュー、ペルソナをつかった思考実験、承認プロセスなどいろいろあると思いますが、省略。

機能の詳細が足りなければ、何度でも実装ステップから返して検討しなおします。 出来かけの機能やプロトタイプをもっていって試験してもらう場合もあるでしょう。 このプロセスのプラクティスにかんしても省略。

  • どのモデル(クラス)の責務か、新しく作る必要があるか
  • アクセシビリティをどのように提供するか
  • DB の変更が必要か
  • 期待される入出力、境界値
  • 期待される例外入力、エラー

こういったことを考えれば複数の実装方法がうかんできて、それぞれの実装コストを計算できます。 あまりに単純でひとつの実装方法しかありえない場合も、それでもコードをよく検討すれば既存の処理とまとめられたり、構造の変更が必要だときがつくこともあります。

このプロセスで、自分が進むべき道をみさだめます。 テストケース、実装、考えられる例外的状況などがすべて頭のなかに出揃います。 そうすればもう迷うことはありません。 テストケースを順に書き、実装を順に書き、並行性などについて想定されていた問題をチェックして、おわりです。 これこそが Rails の誘惑を断ち、迷妄からのがれ、解脱した Rails プログラマになる方法です。

設計するためには自分のアプリケーション + Rails の機能、そして利用可能な gem を知り尽くしている必要があります。 一度 Rails の海にもぐり、ダークサイドに落ちてみないと解脱できないかもしれません。 他の言語の豊富な経験があれば、また違うのかもしれません。

MVC の呪い

まだ一般的なことしか言ってないので、もうすこし Rails にふみこんだ話をしてみます。

RailsMVCAR::Base を継承したモデルがあり、コントローラがあり、erb や haml のファイルがあるという構成です。 これが問題含みなことは多少 Rails をさわった経験があれば知っているでしょう。

よくあるのは次のようなケースです。

  • view が分岐でふくれる
  • コントローラがふとる
  • model がふとる

MVC のどこかにロジックを配置しようという思考」が間違いであることは明らかです。 「MVC に分ける」は「MVC のかならずどれかに属する」ことを意味しません。 rails の app の下にディレクトリが models, controllers, views という構造、generator (とくにscaffold)、 Rails 入門テキストは、すべてこの間違いを助長しています。

もっと自由な発想をもちましょう。

view がふくれるのは、表示をきりかえるロジックの考えがあまいか、モデルを表示する処理が複雑すぎるかです。 どちらも処理を独立したクラスに追い出すことで解決します。

controller がふとるのは、パラメータの処理、トランザクション、分岐などが多すぎるからです。 controller がふとる問題はよく認識されていて、rails からも補助がいろいろ提供されていますが、例外的なケースでは綺麗にいかない場合もあります。 このあいだ QA@IT で質問したところすばらしい回答がいくつも得られました(rails で params に対して複雑な処理をするときのベストプラクティスは? - QA@IT)。 OO 的に適切に責務を分割し、デザインパターンを参照することで綺麗にできる、ということを再確認しました。

model がふとるのは、MVC の呪いの最たるものです。 controller や view がふとらないように気にした結果、すべてのロジックが model に実装され、 メソッドが一画面を越えたりクラスが200行になったりテストが500行になったりします(当然全部オーバーリミット)。

AR model (ここでは AR::Base を継承したもののこと)の責務はビジネスロジックの実装ではありません。 データベース上のデータの一貫性を保ちながら、変更を反映することです。 (異論もあると思いますが、そこまで絞ったほうがいいと考えています)。

validation、callback、relation などはすべてこの目的のためだけに利用されるべきです。 validation は自身のデータと関連の正当性を保証するため、callback は自身のイベントを他のモデルに通知し、波及する影響を吸収するためにあります。 callback で通知を送るのも、ユーザに見せるためのエラーを投げるのも、外部のデータを取りにいくのも、すべて仕事しすぎです。 ひたすら切り出しましょう。"small objects connected with message passing" が理想形です。

このあいだも議論したのですが(Response to "7 Patterns to Refactor Fat ActiveRecord Models")、切り出したモデルをどこに置くか、というファイル名とディレクトリの問題があります。

MVC をそのままディレクトリにするという Rails のルールがこの問題の発端です。 ディレクトリ(Java でいえばパッケージに対応)は設計にもとづいて、結合性にもとづいて決定されるべきです。 ファイルの種類で決めるのはナンセンスではないでしょうか。

モデル名で名前空間を切ったり、まとまった機能の単位の場合は app/decoratorsapp/workers のようにしていますが、 この問題はまだ自分のなかで未解決です。

余談ですが、Rails 界隈でつかわれる MVC、Active Record などの概念は、別の界隈ではもっと抽象的だったり、意味がちがったりします。 Rails では実装にかたよった見方をされる傾向があり、非常にわるい点です。 ほかの概念を勉強してみると新しい見方が得られます。

つっぱしらない

たいてい、「こういう機能がある。これを使えば綺麗になるじゃないか」とかいっている人には注意が必要です。 Rails のへんな機能も、 gem も、他の複雑なテクニック(メタプログラミングや OO 言語と親和性のない概念)も、同様に警戒すべきです。

Rails の枷をはずすと、ショートコーディングかと思うくらいトリッキーなプロジェクト内ライブラリができることがあります。 Option だの Either だのといったクラスが出来ていることもありそうです。もちろん join だの return だのがついています。 警戒すべきは機能と情報の洪水だけではなく、あなたのすばらしすぎるプログラミングスキルもです。

そのような物をつかう必要はないはずです。 Good Old Plain Ruby Object でできるはずです。 そういった特殊な機能は、いまコードを縮め、ごちゃっとしたところを隠蔽する役には立つでしょうが、次に変更する必要が生じたときにすべて破綻します。

可読性、処理の流れが単純であること、明示的であり、暗黙的な関係がすくないことを意識しましょう。

おわりに

Rails の利点のひとつに、 Rails ができればどの Rails プロジェクトでも働けるという点があるかもしれません。 私の提案するスタイルは、多少なりともその前提を崩すでしょう。

本当に効率的なオブジェクト指向設計オブジェクト指向プログラミングを行うには、全員がきわめて高いスキルを持っている必要があります。 そうでないと、適当な場所にメソッドを適当に足して実装するスタイルに戻ってしまいます。

まず、情報の洪水を警戒する。自分の道をえがこうと意識する。好みにしたがってではなく、そうあるべきコードを書く。 そのような意識づくりから始めてはいかがでしょうか。 大切なのは嗅覚をやしなうことです。 コードに敏感になり、疑うことを知れば、いつどんな知識が必要か気が付けます。必要なときに人に聞けばいいのです。

ところで、私の提案は既存の Rails の機能を捨てようというものではないので、他のフレームワークや言語に移行する必要は感じていません。 「ほら、こっちのほうが綺麗でしょ」と例をだされても、それはあなたができることで、私ができることではありません。 学習コストや職さがしのコストがかかります。 良いコードを書くことはしょせん自己満足です。環境を変えればよいプログラムが書けるといわれても、魅力はありません。

完璧な言語もフレームワークもありません。「あれを使ったらもっとよくなるんじゃないか」は幻想です。あなたがよくならないかぎり、なにもよくなりません。