2013年8月10日土曜日

TDDBC参加レポート

TDD Boot Camp Tokyo 2013-07に参加して来ました。

「おい7月の話じゃねーか」と言いたくなりますがぐっと堪えて参加レポートでも書こうと思います。

基調講演


基調講演のまとめはめんd僕が改めて書くよりも素晴らしいまとめをされている方がいらっしゃるので、いくつか紹介させて頂いて終わろうと思いますw

http://makopi23.blog.fc2.com/blog-entry-96.html
http://taichiw.hatenablog.com/entry/2013/07/29/003642

ペアプロ大会


やっぱりTDDBCのいいところは、発表聞いて終わりじゃなくて、実践までがセットになっているところだなーと改めて思いました。

僕のペアもなかなか変わったペアだったので、この記事もペアプロの話メインにしようかと。
ちなみに、今回のお題は「飲み物自動販売機 Ver 2.0」でした。

で、改めてtogetter見なおしてみたんだけど、


なんか基調講演始まる前から実はもう予定調和っぽくなってるねw


というわけで、@razonさんとScalaでペアプロさせていただきました。@razonさんありがとうございます!

で、当日書いたコードがこちら。

https://github.com/shizone/tddbc201307

今回の記事ではこのコードの解説でもしてみようと思います。

「やっぱオブジェクト指向はダメだね」


というセリフが出たシーンがこのペアプロのハイライトでした。

Scalaでやるということで、コーディングスタイルとしてオブジェクト指向に寄るか関数型に寄るか選べるわけです。で最初僕らはオブジェクト指向の方を選びました。
最初のうちはうまく行ってたのですが、在庫管理を始めたあたりからテスト書くのに「モック必要じゃね」とか「他の機能もいくつか先に実装しないとテスト書けないよね」とかいうオブジェクト指向TDDあるあるにハマり始めて、イライラしてきました。

で、「よし、やっぱり関数で行こう」となったわけです。

状態(内部変数)の排除


まずはVendingMachineという型をつくります。1行だけです。



「class」という名前がついているので紛らわしいですが、これはオブジェクト指向でいうクラスとは全く違うものです。
「total」や「stock」という変数は再代入不可能になるので、VendingMachineはimmutableであり*1、状態を持ちません。
つまり型VendingMachineは現実世界のオブジェクトのテンプレートとしてのクラスではなく、ある瞬間の自販機の状態を表現するスナップショットとしての単なるデータにすぎません。

処理を関数で記述


オブジェクト指向の場合は、例えば「10円を入れる」という処理はVendingMachineクラスのメソッドとして表現し、内部変数を書き換えるという実装をするわけですが、関数型の場合は、処理を行う前のスナップショットから処理を行った後のスナップショットを計算する関数を記述します。



もちろん「関数を返す関数」を作ることもできるので、「コインを入れる」という関数はこのような表現が可能です。



関数のテストを書く


ちょっとその前に、テストを書きやすくする補助関数も作っておきます。
初期状態を取得するinitと、パイプライン演算子*2を。



パイプライン演算子の実装の方はちょっと高度なので今回は詳しく書きませんが、よく見るパターンです。Scala2.10で入った新しい機能、implicit classとvalue classを使って、メソッド「|>」を記述しています。
意味合い的にはただ単に関数適用の記述を楽にするだけのものです。VendingMachine型を引数とする関数fを、値vmに適用する際に、普通は「f(vm)」と書くところを、「vm |> f」と書けるようにするだけ。「VendingMachine => VendingMachine」という型の関数を連続で適用していく場合に、普通なら「h(g(f(vm)))」となるところが「vm |> f |> g |> h」となって素敵です。

さて、ここまで準備すると、テストコードがどうなるか。

「初期状態の自動販売機に100円玉を入れて、次に10円玉をいれると、投入金額合計は110円になる」というテスト、こうなります。
(当日はScalaTestを使いましたが、僕はSpecs2の方が慣れてるのでSpecs2式に書きます)



まとめ


「TDDBC当日のペアプロ大会で、Scala組はこんな感じのコードを書いてました」という紹介をしてみました。
一部Scala黒魔術が顔を見せたりしてはいるものの、簡潔で読みやすいテストコードが書けるよーという様子を感じて頂ければと思います。

実は「関数型はUnit Testと相性がいい」という僕の持論もあるのですが、その話はまた今度。



*1: ちなみにScalaではMap型も(特にmutableなものを使うことを明示しない限り)immutableです。
*2: パイプライン演算子はもともと「F#」由来の素敵な演算子です。ちょうどいいページが見つからなかったのでテキトーにググってくださいwちなみにScalazに実装されています。

2013年8月7日水曜日

Gitについて書いておく

※ 本記事の内容は「僕の個人的な考え」であって、正解とは限りません。予めご了承ください。あと、長いです。

能書き


ここ半年ぐらい、僕のまわりでは「Gitに移行しようぜー」的な波が押し寄せてきてる。前職の職場も、今の職場も、大学時代の友達も。一方僕は、学生時代(3年前ぐらい?)にたまたま偶然Gitに首突っ込んだので、もう既にGitがないと生きていけない体になってしまっているという立場。ということで、Gitについて思うところを僕なりに一回文章にしてみようと思う。
ここに書くのはGit入門ではなく、「Gitの基本コマンドは覚えて使えるけど、どうやったらGitっぽくつかえるか、Gitの恩恵を得られるか掴みきれてない」ぐらいの習熟度の方に参考になるような内容の予定。もちろんここに書いてあることに従えと言いたいわけではないので、違う意見の場合はぜひコメントなど頂ければ幸いですし、Git勉強中の方は1つの意見として参考にしていただければと思う。

SVNとの違い


SVNなど、一世代(あるいはもっと)前のVCSからGitに移ってくる場合に、SVNとGitの違いという観点からGitを理解しようとすることは多いと思う。そのアプローチ自体は良いとして、1つ問題がある。SVNとGitの違いの話をするときに大抵の場合、まず最初に「中央管理型か分散型か」を挙げられてしまうことだ。もちろんそれは重要な違いではあるのだが、その違いが重要なケースは稀だ。Linuxプロジェクトのように(開発者の人数という意味で)大規模なプロジェクトの管理をする立場になったら考えることであって、一開発者の立場なら、「へー」ぐらいの理解から始めれば十分だ。(もちろんこれから挙げていくGitの特徴の根本にあるのは「分散型」の恩恵なので、深堀りするときには勉強しよう)

というまえがきを置いた上で、僕はSVNとGitとの違いとして、「ブランチの粒度の大きさ」を挙げたい。

Gitでは、とにかくブランチを細かくたくさん切るのだ。たった1行の修正のために1つのブランチを切るなんてことも決して珍しくはない。
一例として、今僕は会社でソロ開発してんだけど、7/1から7/25までの1ヶ月弱の間に切ったブランチの数カウントしたら、120だった。たぶん、SVNでは想像できない数なんじゃないかな。

というわけで、これからブランチを細かく切ると何が嬉しいのかってところを掘り下げて行くことにする。

帽子をかぶり分けるためにブランチを切る


開発の世界では「帽子をかぶり分ける」という言い回しをよく使う*1のでここでも引用させてもらった。
つまり、今、機能Aの開発をしているのか、バグBを直しているのか、処理Cのパフォーマンス向上のための改修をしているのか。どの作業をやっているのか明確にした上で、そのことだけを考える。そのためにブランチを切るのである。

例えば、開発してると、以下の様な場面に出くわすことがあるだろう。

  • 機能Aの開発中に、機能Aとは関係ないバグBを見つけた
  • 機能Cを作っていたが、機能Dを先に作っておく必要があることに気づいた
  • コメントが気に食わない、タイプミスが気になる、リファクタリングしたい、等々

このような場面でそのまま本能の赴くままに手を加えてはいけない。あれもこれも一緒に実装しようとすると、だいたい何か考慮漏れがあって、バグる。そこで、「このブランチではこの作業だけをやる」というルールを自分に言い聞かせて、頑なに守るのだ。ブランチの切り替え(git checkout)が帽子のかぶり換え。定量的にはわかりにくいかもしれないが、帽子のかぶり分けの効果は、スピードや品質の向上にかなり有効だと思う。
ちなみに、「今どの帽子をかぶってるか=今どのブランチにいるか」がすぐわかるようになってると、脳の切り替えには有効だ。なので、手慣れたシェルがあれば、是非、プロンプトに「今いるブランチ名」を表示するように設定しておこう。
また、あとで少し触れるが、マージ作業や、後から変更履歴を確認するときなんかにも、「機能Aのブランチでは必ず機能Aの実装だけが行われている」ことはとても有効だ。

"じゃあ、「今のブランチとは関係ないバグB」や「先に作らなきゃいけない機能D」はどうするの?"
答えはこう。

  1. issue tracking systemにissue切る(まずは何も考えずにissue切る。これやらないと、忘れるよ。)
  2. 関係ないバグBは放置。あとでやろう。
  3. 先に作らなきゃいけない機能Dが見つかったら、今のブランチのことはひとまず忘れて、機能D用ブランチを切ろう。


できる限り狭い範囲だけを考えればいいように影響範囲を限定しながらソースコードに手を加えるのは、開発の基本。ブランチの切り方も、この考え方にきっちり従っていこう。

粒度の話


ここまでの話をより深くイメージするには、「機能A」ってのはどれくらいの粒度なのよってのがあると良いと思うので、ここで触れておこう。

僕は1つのfeatureブランチは、だいたい以下の様な大きさに収まるように、という方針で切っている。(もちろん状況によるので必ずしも全てのブランチがそうなっているということはないが。)

ブランチ単位のdiffをとったとき、出てくる各diffが何を意図して行われた変更か、すぐに想像できる程度

ソースレビューなんかを実施する文化で開発したことのある方だと想像しやすいと思うが、ちょっとdiffの量が増えると、すぐ「必死にdiffを読み込まないと意図が把握できない」という状況になってしまう*2。そうならない程細かくブランチを切るのだ。

なぜそこまで細かくするか。

繰り返しになるが、それだけ「考える範囲を限定する」ことによってスピードと品質の高い開発をしようというのが一点。

そしてもうひとつは、「マージのため」という視点だ。VCSの利用において、一番事故につながりやすい作業が「マージ」だ。できるだけマージはしたくないという意図はわかるが、Gitの考え方は逆方向で、「マージの回数は増えても、安全なマージをする」だ。まず単純に、diffが少なければ競合しにくい。マージってのはブランチ単位でするものだから、ブランチが小さければ小さいほど競合しにくいという至極単純な理屈。
しかし競合は起きるものであり、いかに競合を正しく解決するかも考えなければいけない。正しく競合解決するために必要なものは「コード変更の意図を正しく把握すること」だと僕は思っている。ブランチの規模は「diffを見て意図をすぐ読み取れる」程度にしておくと、競合した時にお互いの変更をどのように組み合わせるのが正しいか把握しやすい。この辺りは文章では伝わりにくいかもしれないので、是非手を動かして実感して欲しい。


CIとの相性


あともう一つ、ブランチを細かく切るということは、CI(継続的インテグレーション)との相性が良いということにも言及しておきたい。

ビルドを回すには、最新のコードが「動くもの」になっている必要があるが、複数featureが混ざった長いブランチで開発していると、「動く状態」のタイミングが訪れにくい。「機能Aの実装は終わったけど機能Bが中途半端に実装開始している状態」コミットとかね。そうなると、結局全機能実装し終わるまでテストを回すことができなくなって、それって結局CIでもなんでもないということになる。

それに対して、あくまで最小単位の機能でブランチを切り、その機能だけが新たに動く状態になった時点でマージするというのがGitのスタイルになる。マージ先になるブランチには、機能が出来上がるたびに、プロダクトが動く状態でマージされていくので、このブランチはCIツールに監視させるブランチにピッタリだ。
(A successful Git branching modelをイメージして書いてます。これで言う、「develop」ブランチは、CIのためにあると僕は思っている。)

なんかGitの使い方というより、ブランチの切り方の話してない?


仰るとおり。

あくまでブランチの切り方が大事だと思っているのは確か。そして、ツールの機能としてはSVNでもこのブランチの切り方を実践することはできるだろう。だけど、このブランチの切り方を実践するにはGitじゃないとダメなんだ、これが。
なぜなら、この開発スタイルになってくると、ものすごい頻度でブランチ切るし、diff見るし、マージすることになる。SVNだとそれらのコマンドを打つたびにリポジトリサーバと通信が走り、待たされる。でもGitならこれらのコマンドはすべてローカルリポジトリで完結するから、一瞬で結果が表示される。だから分散リポジトリ型のVCSが、Gitがイイ。

一度Gitに慣れてしまってから「もうSVNには戻れない」って言う人は、そのサーバと通信してる数秒×何千回の時間浪費に戻れないという面が大きいのだと思う。

まとめ


というわけで何が言いたかったかというと、僕は別に「Gitのこの機能が良い!」という風には全然考えてない。「CVSからSVNに移行したときのように、SVNからGitに移行した時も打つコマンドが変わるだけ」というGitの使い方では意味が無いのだ。
では何がすごいのか。それは、「ちっちゃいブランチを切る」ことから始まる新しい開発スタイル*3だと僕は思っている。



*1: TDDでは「テストを書いてコンパイルを通す」「テストをグリーンにする」「リファクタリング」のステップがあり、明確に作業を分離する。例えば「コンパイルを通す」のステップで実装(テストを通す)を考えてはいけない。このように今どの作業をしているのか明確にし、その作業のことだけを考えるというスタイルを「帽子をかぶり分ける」と表現する。
*2: 「そして形式だけのソースレビューが行われて、レビューの効果が出るわけもなく、バグる」までが予定調和。
*3: ゆーてもGit自体そこまで新しいというわけではないので「新しい開発スタイル」は言いすぎかもしれないが、少なくともSVNベースとは違う開発スタイルになる。