概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: 5 years of professional Ruby and Rails development - My Reflections - Karol Galanciak - Ruby on Rails and Ember.js consultant
- 原文公開日: 2017/08/27
- 著者: Karol Galanciak
先日のTechRacho記事「DHHのYouTube動画を辛口レビュー」とある意味好一対をなしているように思いました。
Ruby/Railsのプロ開発者としての5年間を振り返る(翻訳)
自分でもちょっと信じられないのですが、RubyとRailsのプロとしての経験年数が早くも5年を超えていました。その間のRailsに対する私の姿勢は、盲目的な愛から容赦ない批判(ActiveRecord、あんたのことだよ)の間で大きく揺れ動き、ようやっとバランスが取れてきたところですが、今でも前向きに取り組んでいることは間違いありません。このぐらい年月を経れば、何らかのフレームワークを用いた開発経験を有意義に総括するには十分でしょう。そこで、特に私自身の振り返りを中心に据えつつ、Railsについて少しばかり語ってみたいと思います。
ActiveRecordとモデル層
Railsで最も重要な部分を占めるのがActiveRecordであることは言うまでもありません。ActiveRecordはそれ自身もかなり複雑ですが、「コントローラを薄く、モデルを厚くすべし」というお題目に従っていると、ActiveRecord::Base
をextendするモデルがみるみる膨れ上がり、事実上アプリの大半を占めてしまうことがちょくちょくあります。この(モデル)層について、この5年間の経験でどんな知見を得ることができたでしょうか。
Railsを使い始めの頃はデフォルトの「Rails Way」に素直に従いました。つまり、ロジックをコントローラからモデルに移動し、ビジネスロジック全体をモデルのクラス内で扱い、永続化にからむロジックのコールバックをあちこちに追加していたのです。当初は素晴らしい手応えでした!Railsでのまともな経験がろくになかったにもかかわらず、どんな機能でも進捗は極めて良好でした。
しかしながら、やがて深刻な問題にいくつも直面するようになったのです。条件にまみれたロジックの大半を、コンテキストに応じて使い分けなければならなくなりました。レコード作成のロジックとレコード更新のロジックの両方を担当するメソッドがあると、両者の違いはほとんどないも同然であるにもかかわらず、さらに条件が上乗せされます。バリデーションロジックはたちまち複雑怪奇の様相を呈し、ついに条件付きバリデーションに手を出さざるを得なくなります。そしてとうとうある日のこと、とあるデータマイグレーションでupdate_columns
の代わりにupdate_attributes
を使ったところ、何やら通知のコールバックをつっついてしまい、通知メールが怒涛の勢いで多数のユーザーに飛んでしまったのです。
そのときの私は、アプリのロジックをまともに制御できる状態ではありませんでした。update
呼び出しやsave
呼び出しのような基本的な操作で何が起きるかわかったものではなかったからです。その日を境に、モデルに関する私のデフォルトポリシーは「コールバック使うな、絶対」「条件付きバリデーション使うな、絶対」「ロジックがひとかけらもないのが理想」に変わりました。このアプローチはしばらくはうまくいきましたが、今度は同じようなロジックが多数のオブジェクトに撒き散らされてしまったり(そのほとんどがService ObjectやForm Objectです)、それどころかロジックがモデルに所属することがほぼ明らかであるにもかかわらず、ロジックの重複(duplicate code)や機能クレクレ君(feature envy)の「コードの臭い」が何度か発生するという事態になってしまいました。どうも「薄いドメインモデル(anemic domain model)」アプローチが期待どおりの成果を出してくれていないようです。またあるときは、何が何でもコールバックを避けようとするあまり、コールバックでモデルと結合していたgemを引っぺがそうとしてシャレにならない時間を使ってしまいました。「純潔を守る」という意味では間違っていなかったのでしょうが、ビジネス上の決定としてはとても褒められたものではありません。コードは、ビジネスを支えて必要な機能を提供するのが目的であり、けがれを知らない乙女のように一点の汚れもないソリューションを目指すのが目的ではありません。メンテナンスのしやすさももちろん重要ですが、混じりけのない美しい設計を実現したところでビジネス上の価値が高まるとも限らず、むしろ時間ばかりかかってしまうようでは、それを目指したところでほとんどのアプリがすぐにも採算割れしてしまうでしょう。
そんなこんなで、ActiveRecordやモデル層への取り組み方もだいぶバランスを取り戻しました。コールバックや複雑な条件付きバリデーションといったRails Wayでお馴染みの手法は、私がビジネスロジックを取り扱うときに好む手法からはかけ離れていますし、一般にこうしたアプローチは長期的に見て害があるとも思っていますが、今の私には、スピードが勝負の短期開発でメンテナンス性は二の次である場合や、最小限の手間でもっと複雑なアプリを賢く扱える(Carrierwaveのコールバックでtouch
やdependent
といった関連付けオプションなどをうまく活用するなど)場合に、短期的なMVP(Minimum Viable Product)開発におけるRails Wayのメリットがどれほど大きいか、痛いほどわかります。
私はモデルに関係のあるロジックをモデルに置く傾向もあるのですが、さてここで問題です。こういうことをしているとファットモデルになりやすいでしょうか?モデルのコードが200〜300行にも達する大規模アプリなら、「イエス」の場合もあります。しかしロジックの凝集度が高く、コンテキストにそれほど癒着していないのであれば、大した問題にはなりませんでした。明確さが落ちることもほとんどありませんでしたし、メンテナンス性に負の影響が生じたわけでもありませんでした。重要なのは、一般的なドメインモデルロジックだけをモデルに配置することです。理想は、そのロジックが永続性そのものとは無関係であることです。
このアプローチに沿うようにしたことで、私にとってはだいぶうまくいくようになりましたし、ActiveRecordに本気で頭にくることもなくなりました。この場合アーキテクチャに軽い制約が生じるでしょうし、柔軟性という意味ではData Mapperパターンなどの方が上かもしれません。他にも、update_attribute
やupdate_columns
といったメソッドは、十分な理由なしに用いると深刻な混乱を呼び起こす可能性がありますし、ActiveRecord.suppress
のような見慣れない機能をうかつに使うとコードが理解不能すれすれになる可能性もあります。それでもActiveRecordを使いつつモデルをきれいに保つことは可能ですし、深刻な害をもたらす可能性があることがわかっているからといって、必ずしも絶対に使ってはならないということにはなりません。その部分の責任は、問題をスマートに解決して長期間メンテ可能になるよう、ツールや設計を選ぶ開発者にあります。
高レベルアーキテクチャの不在
Railsは、高レベルアーキテクチャを提供していないという批判を受けることがあり、その穴を埋めることを想定したソリューションも山ほどあります(たとえばTrailblazerはForm ObjectやOperationクラスなどを提供するミニフレームワークです)。しかし、Railsのこの選択は必ずしも悪いものではないと私は思っています。
モデルやコントローラの一般性は十分高いので、ほとんどのアプリは多かれ少なかれ似通ったものになります。高レベルアーキテクチャではたとえばどんな感じになるのでしょうか?
Form ObjectやService Objectを実装したり層を追加したりするgemはいろいろありますが、そのほとんどは互いに著しく異なっています。Service ObjectやForm Objectを追加するだけで極上の設計になるなどということはまずありません。CQRS(コマンドクエリ責任分離: Command Query Responsibility Segregation)をフル実装すれば、あるいはモデルの読み書きをイベントソーシング(Event Sourcing)すれば改善されるものでしょうか?さらに言えば、Service ObjectやOperationやForm Object、はたまた他の抽象化の構造のあるべき理想の姿をどうやって知ればよいのでしょうか?
ほとんどのRails開発者が満足できるソリューションを見いだすのはおそらく死ぬほど困難でしょうし、Railsにそうした層を追加することで結果的に実装の詳細やインターフェイスが競合してしまうようでは、本当に生産的であるとは言えないでしょう。
私見では、既存の層を対象とする現在のアプローチこそが正解であり、十分経験を積み重ねた開発者なら、どのようなアーキテクチャを用いるアプローチがアプリに最適であるかを見極められるはずです。
ActiveSupport
ActiveSupportが、特にモンキーパッチ周りで多くの開発者に問題視されている層であることは確かでしょう。私自身はモンキーパッチを偏愛しているわけでもありませんし、使うこともまずありませんが、ActiveSupportが提供するコア拡張機能は実に有用かつ便利極まると思いますし、ActiveSupportで大きな問題に突き当たった覚えもありません。純粋さという意味では「エレガントな」ソリューションからはほど遠いであろうという点については同意しますが、長期的にはアプリでこれといった問題を引き起こすでもなく、着々黙々と仕事をやり遂げてくれています。突き詰めれば、これこそがソフトウェアエンジニアリングにおいて最も肝心な点ではありませんか。ActiveSupportに対する批判は総じて少々無理筋気味で、ActiveSupportがもたらす恩恵など取るに足らないものと決めてかかっているフシがありますが、ActiveSupportには多くの利点があることを見過ごす訳にはいきません。
他のフレームワークはどうよ?
この5年間で、Rails以外のフレームワークについてもいくつか試す機会に恵まれました。その中にはDjango(Python)もあればPlay(Java)もあればPhoenix(Elixir)もあればMeteor(JavaScript)もありましたし、Ruby製の他のフレームワークとしてはSinatraやHanamiも試しました。これらのフレームワークではそれなりに楽しく作業できましたが、生産性や開発の楽しさという点ではRailsでの経験を超えるほどではありませんでした。これについては明らかに(Railsの)エコシステムが成熟してきていることが大きな役割を果たしていますし、だからこそ新しいフレームワークたちの一部がRailsと互角に戦おうとして四苦八苦しているわけですが、gemを追加していない素っ裸のRailsですら、その生産性は他のフレームワークと比べて著しく上回っています。
将来について
少なくとも一般的なWeb開発向け以外の用途であれば、私にとってRailsをここしばらく置き換えるに足るであろうフレームワークは今のところ見当たりません。Phoenixはある意味でRailsに似ている面がありますし、もしかすると一番近いのかもしれませんが、私にすればElixir言語や関数型パラダイムの学習は、Rubyやオブジェクト指向プログラミングを学ぶよりずっとつらいものです。しかもRailsの方がコミュニティもずっと大きく成熟しており、総合的な開発しやすさでも遥かに上です。Phoenixは、RubyやRailsと比較して速度やコンカレンシーといった点において明らかに上回っているにもかかわらず、この境地に達するにはかなり時間を要するかもしれません。
まとめ
Ruby on Railsは、間違いなく私のプロ人生を素晴らしいものにしてくれましたし、これまで関わる機会のあったどのアプリを開発しているときにも計り知れない喜びを味わいました。本気で頭にきたことも一度や二度ではなかったにもかかわらず、です。至らない点はそれなりにあるものの、それでもRailsは私にとってほとんどの場合真っ先に選ぶフレームワークであり、これに取って代わるものは今のところ心当たりがありません。
関連記事
Rails: DHHのYouTube動画を辛口レビュー「On Writing Software Well #2」(翻訳)