Ruby on Railsのパターンとアンチパターン入門 ― MVCの落とし穴とリファクタリングの基本
Ruby on Railsのパターンとアンチパターンに関するシリーズの第1回へようこそ。このシリーズでは、Railsアプリケーションの開発で遭遇しがちなさまざまなパターンについて、各回でじっくりと掘り下げていきます。
今回はまず、「(デザイン)パターンとは何か」そして「アンチパターンとは何か」を解説します。説明を分かりやすくするために、長い歴史を持つRuby on Railsフレームワークを例に取り上げます。もし何らかの理由でRailsが好みでない方も、ご安心ください。ここで紹介する考え方(パターン)は、あなたが最終的に使うどんな技術にも応用できるはずです。
しかし、パターンとアンチパターンの説明に入る前に、そもそもなぜ私たちはそれらを必要とするようになったのでしょうか?なぜソフトウェアにこれらの仕組みが必要なのでしょうか?なぜソリューションを設計する必要があるのでしょうか?
実は、あなたはデザイナーである
コンピュータープログラミングの初期の時代から、人々は自分が書くプログラムの設計と向き合ってきました。プログラム(ソフトウェア)を書くということは、ある問題に対するソリューションを設計することです。ソフトウェアを書いているとき、あなたはデザイナーなのです――遠慮なくその肩書きを職名に追加してみてください。優れたソリューションを設計することが重要なのは、私たちが書いたソフトウェアは他の人によって読まれ、編集されるからです。さらに、私たちが考案したソリューションは、将来、他の人によって拡張されていくことになります。
こうした背景の中で、世代を超えたエンジニアたちが、キャリアの中でコードやアーキテクチャに似た設計を目にするようになりました。そこで人々は、問題に対する標準的な解決策を抽出し、文書化し始めました。これは人間としてのごく自然な営みだと言えるでしょう。私たちはあらゆるものを分類し、パターンを見出すのが好きです。ソフトウェアもその例外ではありません。
人間である以上、ソフトウェアエンジニアリングが複雑になるにつれて、パターンはますます多く登場するようになりました。ソフトウェアデザインパターンは発展し、世界中のエンジニアの間で定着していきました。書籍、エッセイ、カンファレンスでの発表を通じて、熟考され実戦で鍛えられたソリューションのアイデアが広まっていきました。これらのソリューションは多くの人の時間とコストを節約してきました。それでは、「デザインパターン」という用語が本当に何を意味するのか見ていきましょう。
デザインパターンとは何か?
ソフトウェアエンジニアリングにおいて、パターンとは「よくある問題を解決するために再利用できるソリューション」と定義されます。パターンとは、ソフトウェアエンジニアの間で良いプラクティスとみなされているものです。エンジニアがそれを定める以上、パターンはすぐにその逆――アンチパターン――になり得ますが、これについては後ほど触れます。
デザインパターンは解決への道筋を示してくれますが、そのままソフトウェアに組み込める完成したコードを提供するわけではありません。パターンは、良く設計されたコードを書くためのガイドと考えてください。実装は自分で考える必要があります。日常のコーディングにおけるパターンの活用は1980年代後半に始まりました。Kent BeckとWard Cunninghamが「パターン言語」というアイデアを提唱したのです。
パターン言語の概念自体は、1970年代後半にChristopher Alexanderが著書『A Pattern Language』で提唱したものです。意外に思われるかもしれませんが、この本はソフトウェアエンジニアリングではなく、建築についての本です。パターン言語とは、組織化された一貫性のあるパターンの集合であり、それぞれのパターンが問題と、多様な形で適用できるソリューションの核心を記述しています。どこかで聞き覚えがありませんか?(ヒント:フレームワーク。もう一つのヒント:Rails)
その後、1994年に「Gang of Four(GoF)」による伝説的な書籍『Design Patterns』が出版され、ソフトウェアエンジニアリングにおけるデザインパターンは広く知られるようになりました。この本には、現在でも使われているパターンの解説と定義が収録されています。Factory、Singleton、Decoratorなど、ほんの一例を挙げるだけでも十分でしょう。
さて、デザインとパターンについて学び直せたところで、次はアンチパターンとは何かを見ていきましょう。
デザインアンチパターンとは何か?
パターンを善者だと考えるなら、アンチパターンは悪者です。より正確に言えば、ソフトウェアのアンチパターンとは、広く使われているものの、非効率または逆効果とみなされるパターンのことです。アンチパターンの典型例としては、多数の機能と依存関係を抱え、本来なら別のオブジェクトに抽出・分割できるはずの「Godオブジェクト(God Object)」が挙げられます。
コードにアンチパターンが生まれる原因は数多くあります。例えば、良い例として挙げられるのは、「善者(パターン)が悪者(アンチパターン)になってしまう」というケースです。前職で特定の技術を使い慣れていて、高いスキルを身につけていたとしましょう。例としてDockerを使ってみます。あなたはアプリケーションを効率的にDockerコンテナにパッケージ化し、クラウド上でオーケストレーションし、クラウドからログを取得する方法を熟知しています。ところが突然、フロントエンドアプリケーションをリリースする新しい仕事に就きました。Dockerとアプリのデプロイに詳しいあなたは、最初の判断として、すべてをコンテナにパッケージ化してクラウドにデプロイすることを選びます。
しかし、気づいていなかったのです。今の職場のフロントエンドアプリはそれほど複雑ではなく、コンテナ化は必ずしも最も効果的なソリューションではないかもしれないことに。最初は良いアイデアに思えても、先に進むうちに逆効果だと判明します。このアンチパターンは「Golden Hammer(黄金のハンマー)」と呼ばれます。
これは「ハンマーを持っていると、すべてが釘に見える」という格言に集約できます。Dockerとサービスのオーケストレーションに非常に精通していると、すべてがクラウドでオーケストレーションされるべきDockerサービスに見えてしまうのです。
こうしたことは起こり得ますし、これからも起こります。善者が悪者になり、悪者が善者になる。では、RubyとRailsはこの全体像のどこに位置するのでしょうか?
まずRuby、それからRails
多くの人は、Webサイトを素早く構築できる人気フレームワーク「Ruby on Rails」を通じてRubyに出会いました。私も同じ道を辿りましたが、それ自体は何も悪いことではありません。Railsは、Model-View-Controller(MVC)と呼ばれる確立されたソフトウェアパターンを基盤としています。しかし、RailsにおけるMVCパターンの詳細に入る前に、よくある大きな誤りとして、「Rubyをきちんと学ばずにRailsを使ってしまう」ことが挙げられます。
Railsフレームワークは、アイデアを素早く形にしたいときの定番フレームワークの一つでした。現在では状況は一変し、Railsは依然として使われていますが、全盛期ほどではありません。あまりに簡単に使い始められるため、多くの初心者がrails newコマンドでWebアプリの開発をスタートさせます。するとどうなるか。道半ばで問題が発生し始めます。初心者のうちは、Railsによる開発の手軽さとシンプルさに魅了され、最初はすべてが魔法のようにスムーズに動きます。しかしそのうち、多くの「魔法」を当たり前のものとして受け入れてしまい、カーテンの裏側で何が起きているのか理解できなくなっていることに気づくのです。
私自身この問題を経験しましたし、多くの初心者や中級初心者が同じ悩みを抱えているはずです。フレームワークを手にして開発を進めていくと、高度にカスタマイズされたものを追加しようとした瞬間に、それができなくなります。フレームワークの「魔法ポイント」を使い果たしてしまったからです。その時点で、原点に立ち返って基礎を学び直す必要があります。立ち返ること自体は大した問題ではありません。誰にでも起こることです。しかし、Rubyのような本質的なことを学ばないまま先へ進むと、問題は深刻化します。この点で役立つ良書の一つが『The Well-Grounded Rubyist』です。
初心者の段階で最初から最後まで読み切る必要はありません。手元に置いておき、必要なときにすぐ参照できるようにしておきましょう。急に手を止めて全編を読むべきだと言っているわけではありません。時折立ち止まってRubyの基礎を復習すれば、新たな視野が開けるかもしれません。
MVC:Railsの屋台骨
さて、MVCについてはどうでしょうか?Model-View-Controllerパターンは古くから存在し、Ruby(Rails)、Python(Django)、Java(Play、Spring MVC)など、さまざまな言語の多くのフレームワークに採用されてきました。その考え方は、それぞれが独自の役割を担う独立したコンポーネントを持つことです:
- Modelはデータとビジネスロジックを扱います。
- Viewはデータの表示とユーザーインターフェースを担当します。
- Controllerは両者をつなぎ、Modelからデータを取得し、Viewをユーザーに表示します。
理論上は素晴らしく、ロジックが最小限で、Webサイトが複雑なロジックを抱えていない場合には非常に有効です。ここからが厄介なのですが、これについては後ほど詳しく触れます。
MVCは野火のようにWeb開発コミュニティに広まりました。今日絶大な人気を誇るReactのようなライブラリでさえ、Webアプリのビュー層として説明されることがあります。これほど普及し、揺るがないパターンは他にありません。RailsはActionCableによってPublish-Subscribe(パブリッシュ・サブスクライブ)モデルを追加し、チャンネルの概念をMVCパターンのコントローラとして位置づけました。
しかし、これほど広く使われているパターンには、どのようなアンチパターンが潜んでいるのでしょうか?MVCパターンの各要素における最も一般的なアンチパターンを見ていきましょう。
Modelの問題
アプリケーションが成長しビジネスロジックが拡大するにつれ、人はモデルを過密にしがちです。絶え間ない成長は、「Fat Model(肥大化したモデル)」というアンチパターンにつながります。
有名な「Fat Model, Skinny Controller(太ったモデル、痩せたコントローラ)」パターンは、悪者とみなす人もいれば、善者とみなす人もいます。私たちは、どちらが太っていてもアンチパターンだと考えます。理解を深めるために、例を見てみましょう。SpotifyやDeezerのようなストリーミングサービスがあると想像してください。その中に、次のような楽曲のモデルがあります:
class Song < ApplicationRecord
belongs_to :album
belongs_to :artist
belongs_to :publisher
has_one :text
has_many :downloads
validates :artist_id, presence: true
validates :publisher_id, presence: true
after_update :alert_artist_followers
after_update :alert_publisher
def alert_artist_followers
return if unreleased?
artist.followers.each { |follower| follower.notify(self) }
end
def alert_publisher
PublisherMailer.song_email(publisher, self).deliver_now
end
def includes_profanities?
text.scan_for_profanities.any?
end
def user_downloaded?(user)
user.library.has_song?(self)
end
def find_published_from_artist_with_albums
...
end
def find_published_with_albums
...
end
def to_wav
...
end
def to_mp3
...
end
def to_flac
...
end
endこのようなモデルの問題は、楽曲に関連しうるさまざまなロジックの「捨て場」になってしまうことです。メソッドが時間とともに少しずつ追加されていくうちに、モデル全体が巨大で複雑になり、ロジックをいくつかの場所に分割することが将来的に有益になる可能性があります。
一目見ただけでも、このモデルが推奨されるプラクティスに反していることが分かります。単一責任の原則(SRP)に違反しているのです。フォロワーとパブリッシャーへの通知を行い、テキストの不適切表現をチェックし、楽曲をさまざまな音声フォーマットにエクスポートするメソッドまで持っています。これらすべてがモデルの複雑さに拍車をかけます。このモデルのテストファイルがどれほどのものになるか、想像すらできません。
このモデルをどうリファクタリングするかは、メソッドが他の場所でどのように呼び出され、使われているかに大きく依存します。ここでは一般的なアプローチをいくつか紹介するので、自分のケースに最も合うものを選んでください。
フォロワーとパブリッシャーに通知するコールバックは、ジョブに抽出できます。ジョブはキューに登録され、ロジックはモデルの外に保たれます。次のようにです:
class NotifyFollowers < ApplicationJob
def perform(followers)
followers.each { |follower| follower.notify }
end
end
class NotifyPublisher < ApplicationJob
def perform(publisher, song)
PublisherMailer.song_email(publisher, self).deliver_now
end
endジョブはモデルとは別のプロセスで独立して実行されます。これでジョブのロジックを個別にテストでき、モデル側では正しいジョブがエンキューされたことだけを確認すればよくなります。
不適切表現のチェックや、ユーザーが楽曲をダウンロード済みかどうかの判定が、すべてアプリのビュー部分で行われているとしましょう。その場合、Decoratorパターンを活用できます。すぐに始められる人気のソリューションとして、Draper gemがあります。これを使えば、次のようなデコレータを書けます:
class SongDecorator < Draper::Decorator
delegate_all
def includes_profanities?
object.text.scan_for_profanities.any?
end
def user_downloaded?(user)
object.user.library.has_song?(self)
end
endそして、コントローラでdecorateを呼び出します。例えば:
def show
@song = Song.find(params[:id]).decorate
endビューでは次のように使えます:
<%= @song.includes_profanities? %>
<%= @song.user_downloaded?(user) %>依存関係を追加したくない場合は、独自のデコレータを実装することもできますが、これについては別のブログ記事で取り上げます。モデルの関心事の大部分を分離できたところで、次は楽曲の検索メソッドと変換メソッドを処理しましょう。モジュールを使って分離できます:
module SongFinders
def find_published_from_artist_with_albums
...
end
def find_published_with_albums
...
end
end
module SongConverter
def to_wav
...
end
def to_mp3
...
end
def to_flac
...
end
endSongモデルはSongFindersモジュールをextendすることで、そのメソッドをクラスメソッドとして利用できるようにします。また、SongConverterモジュールをincludeすることで、そのメソッドをインスタンスメソッドとして利用できるようにします。
これらすべてにより、Songモデルはかなりスリムで的を絞ったものになります:
class Song < ApplicationRecord
extend SongFinders
include SongConverter
belongs_to :album
belongs_to :artist
belongs_to :publisher
has_one :text
has_many :downloads
validates :artist_id, presence: true
validates :publisher_id, presence: true
after_update :alert_artist_followers, if: :published?
after_update :alert_publisher
def alert_artist_followers
NotifyFollowers.perform_later(self)
end
def alert_publisher
NotifyPublisher.perform_later(publisher, self)
end
endモデルのアンチパターンはまだまだたくさんあり、これはモデルで何がうまくいかなくなり得るかの一例にすぎません。このシリーズの今後の記事で、より多くのモデルのアンチパターンについて詳しく掘り下げますので、お楽しみに。ここからは、ビューで何が問題になり得るかを見ていきましょう。
Viewの問題
モデルの問題に加えて、Rails開発者はビューの複雑さに苦しむこともあります。かつてはHTMLとCSSがWebアプリケーションのビュー部分の王者でした。しかし徐々にJavaScriptが支配権を握り、フロントエンドのほぼすべての側面がJavaScriptで書かれるようになりました。Railsはこれに関して少し異なるパラダイムに従います。ビューをすべてJavaScriptにするのではなく、JSを「振りかける(sprinkle)」だけにすべきだという考え方です。
いずれにせよ、HTML、CSS、JS、Rubyを同じ場所で扱わなければならない状況は混乱を招きがちです。Railsのビュー構築で厄介なのは、ドメインロジックがビューの中に紛れ込むことがある点です。これはまずいことで、何よりもMVCパターンを壊してしまいます。
もう一つのケースは、ビューやパーシャルで埋め込みRuby(ERB)を使いすぎることです。ロジックの一部はヘルパーやデコレータ(ビューモデル、プレゼンターとも呼ばれます)に移動できるかもしれません。具体例についてはシリーズの今後の記事で取り上げますので、お楽しみに。
Controllerの問題
Railsのコントローラも、さまざまな問題に悩まされることがあります。その一つが「Fat Controller(肥大化したコントローラ)」アンチパターンです。
先ほどモデルは太っていましたが、減量に成功しました。すると今度は、その過程でコントローラに余分な重量が加わったことに気づきます。通常、これはビジネスロジックがコントローラ内に置かれたときに起こりますが、本来あるべき場所はモデルか、あるいは別の場所です。肥大化したModelのセクションで共有したアイデアの一部は、コントローラにも当てはまります。プレゼンターへのコード抽出、ActiveRecordコールバックの活用、Serviceオブジェクトの利用などです。
Trailblazerやdry-transactionのようなgemを使う開発者もいます。ここでの考え方は、特定のトランザクションを扱うクラスを作成することです。すべてをコントローラから移動させ、モデルをスリムに保ちながら、ロジックをこれらの独立したクラスの中に保存し、テストします。これらはサービス、トランザクション、アクションなどと呼ばれています。
まとめ
アンチパターンはまだまだたくさんあり、その解決策はさらに多く存在します。この記事ですべてを網羅しようとすると、スペースと時間がかかりすぎて、記事自体が太ってしまいます(ちょうど話題にしたモデルやコントローラのように)。ぜひこのシリーズをフォローしてください。RailsにおけるMVCパターンのあらゆる側面を深く掘り下げ、最も有名なアンチパターンへの対処法をお届けします。それまでの間、パターンとアンチパターンとは何か、そしてRuby on Railsフレームワークにおける最も一般的なものについてのこの概要を楽しんでいただけたなら幸いです。
それでは次回まで、Cheers!
P.S. Ruby Magicの記事が公開されたらすぐに読みたい方は、Ruby Magicニュースレターを購読して、記事を見逃さないようにしましょう!
-
Rubyで学ぶニューラルネットワーク超入門――思っているほど難しくない!
この記事では、ニューラルネットワークの基礎と、Rubyを使った実装方法を学びます。人工知能やディープラーニングに興味はあるものの、何から始めればいいのか分からない方にぴったりの内容です。シンプルな例題を通じて、重要な概念をわかりやすく解説していきます。実際にRubyで多層ニューラルネットワークを書くことはほとんどありませんが、コードがシンプルで読みやすいため、内部で何が起きているのかを理解するには最適なアプローチです。まずは少し立ち止まって、私たちがここに至るまでの道のりを見てみましょう。 映画『エクス・マキナ』のワンシーン 『エクス・マキナ(Ex Machina)』は2014年に公開され
-
Ruby on Railsとは?初心者にもわかる仕組み・魅力・学び方を徹底解説
Ruby on Railsとは? Ruby on Rails(略称:RoR)は、世界で最も人気のあるオープンソースのWebアプリケーションフレームワークです。プログラミング言語「Ruby」をベースに構築されており、シンプルなサイトから大規模で複雑なサービスまで、幅広いWebアプリケーションの開発を支援します。 そもそもフレームワークとは? フレームワークとは、ソフトウェア開発の際に土台となる構造を提供してくれるコードやツール、ユーティリティの集合体です。あらかじめ用意された構造に沿ってコードを書くことで、プログラムが整理され、保守性も高まります。正しく使いこなせるようになれば、開発作業は格段に