こんにちは!ツールの口コミサイト「Wantedly Tools」の開発を行っている南(@south37)です。
先週、第一回スタートアップRails勉強会 (http://connpass.com/event/39963/)という勉強会があったのですが、そこで「Rails で高速に開発をするために気をつけていること」というタイトルでLTをしきました!
自分は普段、なかなか登壇などを行わないので、LTでの発表は自分の考えをまとめる良い機会となったと思います。その一方で、5分のLTという短い時間では話しきれない部分も数多くありました。そこで今日のブログでは、LTで話した内容の補足も兼ねて、「Rails で高速に開発をするために気をつけていること」について簡単にまとめてみたいと思います。
ちなみに、「Rails で」と書いてはいますが、Rails に限定しない話も多いので、Rails 知らないって人が読んでも大丈夫だと思います!
Rails で高速に開発をするために気をつけていること
今回LTでお話ししたのは、主に以下の3つの観点で「Rails で高速に開発をするために気をつけている」という内容でした。
- 1. Rails に秩序を与える
- 2. YAGNI を徹底する
- 3. GitHub Flow を高速に回す
順に簡単に説明してみます。
- 「1. Rails に秩序を与える」では「依存管理や命名、DRYなどコード整理のテクニック」を中心に、「スピードを落とさずに変更を加え続けるためにどういった点に気をつけているか」を話しました。
- 「2. YAGNI を徹底する」では、改めて「必要ないものを作らない事」を徹底しようという話をしました(そもそも "作らない” で目的を達成できるなら、それが一番高速なはずです!)。
- 「3. Gitub Flow を高速に回す」では、チームで開発を進める場合に「コミュニケーションや進め方が原因でスピードを落とさないために出来ること」という話をしました。
「プロジェクトを前に進める」という文脈であれば、他にも「目標やそれを達成するためにやる事をどう決めるか」や「開発以外の部分で出来る取り組み」などの話も入ってくるとは思うんですが、話の幅を広げすぎると収拾がつかなくなると考え、今回は「開発」にフォーカスした話をしました。
それでは、「1. Rails に秩序を与える」、「2. YAGNI を徹底する」、「3. GitHub Flow を高速に回す」についてもう少し詳細に見てみましょう。
1. Rails に秩序を与える
スピードを落とさずに開発を進める工夫として、「コードを整理してソフトウェアを変更しやすい状態に保つ」という考え方があります。
この逆パターンとして、「変更がしづらいソフトウェア」、いわば「硬直化したシステム」を考えるとその重要性がイメージがしやすいと思います。すなわち、あるシステムを見たとき、そのシステムの中に密結合なコードが散らばっており、依存関係がぐちゃぐちゃで、「一ヶ所の変更によってどこにどんな影響が出るかが分からない」状態になっていると、継続的に変更を加え続けるのが難しくなります。そして、その難しさ、複雑さはシステムが巨大になる程指数関数的に膨れ上がっていきます。
こういった「コードベースが巨大になると共に増大する複雑さ」に対処する術として、人類は様々なテクニックや考え方、プログラミング言語やランタイムの機能を発展させてきました(デザインパターンやオブジェクト指向原則、package や class 単位で閉じた scope、 DRY の為のコードジェネレーターなど、その範囲は多岐に渡ります)。それらの中でも、特に Rails で自分が開発してる時に意識してることは以下の3つになります。
1-1. 依存管理
1-2. 命名
1-3. DRY
それぞれ、順に説明していきます。
1-1. 依存管理
Ruby の様なオブジェクト指向言語において、ソフトウェアを構成する単位はオブジェクトになります。その場合、「変更の影響範囲」は「オブジェクトの依存関係」と密接に関係してきます。すなわち、「広く利用されているオブジェクト」ほど、変更は難しくなります(影響範囲が広くなりがちなので、注意して変更する必要が出てきます)。
その為、出来れば「限られた範囲でだけ利用される」オブジェクトを作りたくて、それが「責務の分離」といった話に繋がります。特に、Rails で開発してる時は「Fat Controller, Fat View, Fat Model を避けよう」みたいな心がけを行う事になります。
この点(「Rails で Fat Model などを避けるためにどうすれば良いのか」という点)については、ある程度大きい開発をしていると必ず出てくる問題なので、議論が盛んに行われています。参考になる資料も多いので、それらを見ながら考えることもできるのではないでしょうか。
cf. http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/
cf. http://qiita.com/yuku_t/items/961194a5443b618a4cac
ちなみに、自分が開発してる際は、「特定用途でしか使わないものは 1つの class として切り出す」様にしていました。「どこで何が使われてるか」をはっきりさせたかったのと、「不要になったらすぐに捨てられる」様にする為です。
多分、どう class を切ってくかについては色んな方針があると思いますが、「気軽に変更が行える様になっているか」みたいな事を意識すると良いんじゃないかと思います(もちろん、本質的に変更が難しいオブジェクト、システム全体から激しく依存されてしまうオブジェクトは存在すると思います。そういった存在を「可能であれば減らす」のが重要だと思います)
1-2. 命名
ソフトウェア開発において、命名の重要性は頻繁に議論されます。 それは、「class 名や method 名を分かりやすい(名前から用途や起きることをイメージしやすい)ものにしよう」みたいな話だったり、「ユビキタス言語を使おう」みたいな主張だったりする訳ですが、実際に命名の工夫によって解決できる問題は多数あります。
その一例として、僕らのチームでは「依存関係やscopeを命名で表現する」という事をやってみていました。事例として、ちょっとだけ紹介してみます。
Web の開発において、view の部品は「HTML, CSS, JS」の3つから構成されます。Webというプラットフォーム上ではこれは避けられず、「綺麗な部品を簡単に作れる」のはメリットである一方、以下の様な扱いづらい性質もあります。
- 1. CSS は global scopeしか存在しない
- 2. JS も DOM の抽出は global scope でやる必要がある
- 3. HTML, CSS, JS という複数箇所に1つの部品の情報が分散してしまう
最近のフロントエンドフレームワークは「JSの世界で全てやる」事でこれらの問題を解決してる事が多いです。すなわち、DOM の構築は JS サイドでやる事で「DOM抽出」という工程を無くし、さらにスタイルの適用もJSでやる(タグの属性として指定 or class 名をユニークな hash 値で表現して適用)といった取り組みがなされています。
ただ、このアプローチだと、SEOやパフォーマンスを考えて「Server でレンダリングしたい」という要望があるときに、要望に答えられない事が多いように思います(クライアントで使うDOM生成用ライブラリと同様のものを Node.js で動かし、response 用の HTML を生成する Server Side Rendering という取り組みも注目されてますが、パフォーマンスなどの観点からまだ現実解とはなっていない印象です)。そのため、別のアプローチが必要でした。
Wantedly Tools の開発時は、「SEOは捨てられない」という判断で traditional な HTML, CSS, JS を返す方法で view 部品を作っていました。その際、 view 部品の命名ルールを運用する事で、「scopeの問題」や「HTML, CSS, JS の対応付けの問題」に対処してました。
具体例として、「ツールカード」と僕らが読んでる部品に注目します。これを構成する HTML, CSS, JS それぞれを以下の様な「tools/shared/tool_card」というファイル名で用意します。ファイル名で対応付けを示す様にします。
- app/views/tools/shared/_tool_card.html.haml
- app/assets/stylesheets/tools/shared/tool_card.scss
- app/assets/javascripts/tools/shared/tool_card.js.coffee
また、部品の動的な性質を定義する JS クラス名を `Tools.Shared.ToolCard` とし、css クラス名を `.tools-shared-tool-card` とすることで、「ユニークな名前を持つ JS クラスや、ユニークな css クラス」を用意する様にします。ディレクトリ名も含めたファイル名と対応づけることで、グローバルにユニーク性を担保するのが狙いでした(同じファイル名のファイルが存在しないことは、OSのファイルシステムが保証してくれます)
こうやって部品ごとにファイルを分けつつ、命名のルールを決めることで、もともと対処が必要だと考えていた「CSS が global scope、JS の DOM 抽出も global scope、HTML, CSS, JS に情報が分散」などの問題は起きなくなりました。
命名のルールだけで幾つかの問題が解決できた事例だと思ってます。
1-3. DRY
「DRY にする」というのは「達人プログラマー」などでも紹介されているテクニックですが、なぜ「DRY にする」事に価値が見出されているのでしょうか?
ポイントは、「結合性の高いコードを避ける」という事です。
「結合性の高い状態」というと「1-1. 依存管理」で問題にしたような「クラス間の依存が激しく、片方を変更するともう片方も同時に変更しなければならなくなってる状態」を想像すると思いますが、「DRYではない状態」も、もう1種の典型的な「結合性の高い状態」です。
例えば、「同じ validation のロジックを複数箇所で別々に定義してる」みたいな例がわかりやすくて、システムとしての整合性を保つためには「2箇所以上のコードを同時に変更する」必要があります。難しいのは、「同時に変更されるべき複数の箇所」が自明ではない状況が数多くある点で、1箇所変更してテストが壊れて気づけるならまだマシ、テストも何もないと全く気付かず、「システムの整合性が取れなくなって不明瞭なバグに苦しむ」事になります。
DRY にするのは難しいケースもありますが、ちょっと考えてロジックを共通化すれば何とかなるケースも多々あるので、グッとこらえて考える習慣をつけると良いと思います!コードとしての共通化がすごく難しければ、最悪でもコメントとテストを用意して、「結合性の高いコード同士の関係性をコード上で明示する」様にすると良いと思います。
1章まとめ
Rails で「コードを整理してソフトウェアを変更しやすい状態に保つ」という観点から、以下の3つのポイントについてまとめました。
1-1. 依存管理
1-2. 命名
1-3. DRY
「変更の難しい硬直化したシステム」になると、結局「改善のサイクル」自体を回しづらくなるので、プロダクトやサービスを成長させるためにも「変更しやすい状態に保つ」という意識は持つと良いと思います!
2. YAGNI を徹底する
スタートアップに限らない事ですが、「やらずに済む事はやらないで済ます」のは本当に重要です。特にリソースの少ないスタートアップでは尚更です。それはすなわち、「You ain’t gonna need it(必要になるまで作るな)」というYAGNI原則を徹底しようという事でもあります。
このYAGNI原則は適用範囲が広くて幾つかの観点から見ることができます。
- そもそも、機能単位で見た時に、「優先度の高い機能」に優先して取り組む。「優先度が低く、必要になるか分からない」ものはやらない。
- 実装の時間、コードの複雑さを抑えるために、「不要な抽象化」をいれない。例えば、1つしか具象クラスが存在しない抽象クラスなどは確実に不要。レビューにかかるコストも増大するし、有害。
- 最初から最適化しない。特に、ロジックを色々試してる段階(かつ、production には出して関係者だけ見える機能にしてる状態など)では、あえて最適化せずにロジックなどを試しやすい状態に保って、ロジックがほぼ固まってからキャッシュなどの最適化を入れる(response time に問題がなければ最後まで最適化しない)。
- 「使われるかわからないけどあると便利そうなメソッド」は用意しない
- etc...
などなど、「機能」というレベルから「コードをどう書くか」というレベルまで、幅広い視点から「YAGNI原則」は有用です。ゴールは明確で、「限られたリソースで最大のスピード、最大のアウトプットを出す」というのが目標なので、それを満たす事が出来ているかを継続的に振り返ると良いとお思います。
3. GitHub Flow を高速に回す
プロダクトの改善のスピードを上げるには、「変更が継続的かつ高速に production へ反映されていく状況を作る」必要があります。
Wantedly では、インフラチームがしっかりとデプロイシステムを作ってくれているおかげで「コマンド1発で誰でも1日に何度でもデプロイが出来る」ようになっているのですが、そうなると僕らの様なアプリケーションのエンジニアにとっては「いかに master への機能の merge を早めるか」が、「改善のサイクルを早く回す」ために重要になってきます(僕らは GitHub Flow を採用しており、機能ごとに branch を切って master への PR を作り、レビューを受けて master へ merge するというフローで進めています)
すなわち、「タスクを決めてコードを書いてPRを作ってレビューを受けてmergeしてデプロイ」が一連のサイクルで、この1サイクルを高速にする事が重要になります。この3章では、特に「PRにしてレビューを受ける」という部分に着目して、自分が心がけている以下の2点についてまとめてみます。
3-1. 変更は可能な限り分割してPRにする
3-2. レビュアーフレンドリーにする事を心がける
3-1. 変更は可能な限り分割してPRにする
これは意識してやってる人も多いかもしれない Tips ですが、「一度の変更は小さく保って小さなPRにして高速にmergeする」のは、開発のスピードを高めるために重要です。
例えば、依存関係の無い3つの小さな変更があるときは、3つをまとめて1つのPRにするのでは無く、3つを分けてそれぞれ小さなPRにします。それだけでも以下の様に幾つかのメリットがあります
- 1つ1つの機能をPRとして作るのが早くなる(3つまとめると30分後にやっとPRになった機能が、分割すれば10分後には1つのPRが作れる。また、途中で別の割り込み作業が発生した際など、影響を受ける範囲が少ない。)
- 1つ1つの機能のproductionへの反映が早くなる(PRになるのが早い & レビューのコストも小さいので。特に、3つの変更のうち1つに対して修正が入って他の箇所もそれに引きずられて merge できない。。。みたいな事が無くなる。)
- PR がガシガシ merge されていくのでテンションが上がる(意外と重要)
また、ある程度大きな変更を加えたい時でも、「依存関係のある複数の小さなPR」に分割して、順次 merge していく方が良いと思っています。これは、3-2の「レビュアーフレンドリー」にも繋がる話ですが、「大きな変更のPR」だと「そもそもレビューが大変で時間がかかる」上に、「修正が入った後で再度チェックする」のも大変で時間がかかる様になるからです。また、そういった大変な作業はレビュアーもやりたくないので、結果的に後回しにされやすくますます merge までの道のりが遠くなってしまいます。
Tips としては、ローカル環境などで「ある程度まとまった変更」をしてリリースまでの道のりが見えたら、それを適宜分割して小さな PR にしていくと良いと思います。その際、「どういう形の変更になる予定で、どういう位置付けでこの変更を加えてるか」は Issue や 1つ1つのPR で詳細に説明するようにすると良いと思います。
3-2. レビュアーフレンドリーにする事を心がける
3-1 でも触れましたが、「レビューのコスト」を下げることは、最終的に production へ反映されるまでのスピードを上げる為に重要です。その為に出来る事の1つが、「レビュアーフレンドリー」である事です。
例えば、3-1で触れた「可能な限り分割して1つ1つのPRを小さくする」というのは、レビュアーのコストを下げる為のテクニックの1つです。他にも、コードだけだと「そもそもどういう挙動が正しくて何をチェックすれば良いのかが不明瞭なケース」が出てきやすいので、「変更理由、変更点の箇条書き、変更点の詳細説明、変更後の画面のスクリーンショット、etc…」など、レビューに必要な情報はモリモリPRに書くようにします。
ちょっと面倒でも、「レビュアーにとってレビューしやすい」状態になってる方が結果的には「master への merge が高速に行われる」ので良いことが多いです!
まとめ
「Rails で高速に開発をするため」に自分が普段気をつけていることを、以下の3つの観点から簡単にまとめてみました。
- 1. Rails に秩序を与える
- 2. YAGNI を徹底する
- 3. GitHub Flow を高速に回す
実際の内容は「よく言われる心がけ」ばかりでしたが、なんだかんだで意識して取り組むと効果はあるなーと感じています。開発環境によって違う部分などあるかもしれませんが、このブログを読んだ開発者の方にとっても、参考になれば幸いです!
余談
余談ですが。。。今回のLTでは1人5分という発表時間を与えられたのですが、勢い余って50枚くらいスライドを作ってしまいました。なんとか話しきったものの、若干時間オーバーをしてしまったのは反省点でした。。。主催のLang-8さん、すいませんでした。。。