Rubyにはウンザリ!動的型付け、副作用、およびオブジェクト指向プログラミング全般からの考察

この記事を書き上げるには、相当長い時間がかかりました。本来は今年の年明け、 Rubyの死やデイヴィッド・ハイネマイヤー・ハンソンのTDDは死んだがアップされて騒ぎになる前に投稿するつもりだったのです。昨年末に書いたツイートを見てください。

> Rubyにはもう飽き飽きした。理由はいろいろあるが、特にその副作用と、ステータスが可変なせいで大量のユニットテストを書かされるのにはウンザリだ。 @abevoelker

Rubyの開発に関しては、大勢の人が心のどこかで何かおかしい、何かが欠けていると思っているようですが、たいていの人は責める対象を間違っています。Rubyで書いたアプリがとんでもない代物になったって? それはあなたがきちんとテストコードを書かなかったか、テスト駆動開発(TDD)の指針に則って開発しなかったからです。もしくは、正しいデザインパターンに切り分けるための知識が不足していたのでしょう。はたまた、単一責任の原則SOLIDデメテルの法則などの、数ある法則に従わなかったのかもしれません。そんな向きには、デザインパターンやテスト、オブジェクト指向設計に関する本でも読んで、悟りを得た頃にまた戻ってきてもらいたいものです。

> 正直言って「オブジェクト指向」、特に「オブジェクト指向設計」が何を意味しているのか理解不能だ。人生最良のこの時期に、毎日何時間も費やして理解しようとしているのに。もうイヤだ。 @garybernhardt

Rubyアプリは技術的負債を引き寄せる

業務で常にRubyを使うようになって、もう3年が経とうとしています。主な仕事は、2008年から2010年の間に実装されたRails2 / Ruby 1.8.7のレガシーアプリを保守すること。10個ばかりあるそれらのアプリケーションには、一切テストコードが書かれていませんでした(少なくとも私が手をつけた時点では)。バグの改修を行う時はわざわざ失敗するテストを書き、新規機能を追加する時はできる限りテスト駆動開発に徹しましたが、全般的に見てどのアプリも未だにひどい状態です。ちょっとした修正が新たなバグを生むのではないかと常に心配しているので、ちっとも楽しくありません。

きっと皆さんにも、同じような経験があると思います。

> 5年前に書かれたRailsのコードを引き継いだ開発者は、密かに会社の倒産を願っている。 @HackerNewsOnion

どうしてこんなことが起きるのでしょう。そしてもっと重要なのは、どうしたらこの問題を回避できるのかということです。テストとは、リファクタリングにおいて開発者に自信を与えてくれるもののはずですよね。ならばより多くのテストを書き、テスト駆動開発をきちんと行うだけで、全てが解決してしかるべきなのですが。

git blameコマンドを実行し、保守対象のコードを実装した開発者を見つけ出して、テストを書かなかったことを責めれば、確かに気分は晴れます。しかし最近、全てが全て彼らの責任だとは限らないということが分かってきました。テストを書けば、状況はある程度耐えうるところまで改善されます(真剣な話、テストが一切実施されていないというのは論外です)。その一方で、テストコードをどんどん追加すればRubyの問題点が全て解消されるかと言われると、そこまでは確信が持てないのです。

難解なテストコーディング

あまり知られていませんが、Rubyでテストを行うにはかなりの労力が求められます。Rubyやオブジェクト指向のテストについて学ぼうとすれば、1行もコーディングしないうちからどっぷりと深みにハマることになるでしょう。mock、stub、double、fake、spyなどさまざまな抽象クラスがあり、あらゆる知識を吸収する必要があります。オブジェクト指向のテストに関しては業界が総力をあげて取り組んでいて、その福音を説くことに全キャリアを捧げる人もいるほどです。

理論が複雑になれば、実装も複雑になります。私はRSpecの方が好きですが、Test::UnitやMiniTestを使う人もいますから、常にプロジェクトを移動できる態勢でいたいと思えば、それらの構文にも通じていなくてはなりません(ユニットテスト・フレームワーク以外にも学ぶことはたくさんあるというのにです)。それに、常に一時的な流行に翻弄されるという面もあります。最近では、factoryではなくfixtureを使うべきだという議論が盛んですね(FactoryGirlなど)。同じライブラリを使っていても書き方はさまざまですし(このサイトにベストプラクティスが紹介されています)、バージョンが違うだけでも大きな差があります(例えばRSpecはshouldを捨てexpectを使うようになりました)。

結局Rubyでテストを行うにあたっては、こうした多大なる先行投資に加えて、精神的にも常に注力し続けることが求められます。あまり他のコードでの応用が利かない知識、加えて残された他のコード部分と同様にいずれただ腐れていくだけのテストコードが要求されるのです。こうした負荷に苦しんだ結果、開発者は結局テストを書かなくなり、罪の意識で恥じ入ることになってしまうのです。

> テスト至上主義というものは、抑制されたセックスのようなものです。もしくは気の進まない自己批判のための、非現実的かつ非効率な道徳行動にすぎません。
デイヴィッド・ハイネマイヤー・ハンソン (DHH) – TDDは死んだ

テストのたびに、Rubyの動的型付けはまるで粘土のようにぐちゃぐちゃ変形します。この状況をコントロールする重荷を、開発者だけにいつも押しつけてしまうことは全く意味がありません。チェーンソーが持ち出されるたび、防護具を作りなおすことができる誰かに頼り続けるようなものです。ボスに昨日やれと言われただとか、製造ライン上の緊急事態だとか、もしくは単にそれがスクリプトだからだとかそういった理由で、ただもう防護具があろうがなかろうが気にせずやって、指をなくすリスクを取ってもいいのですから。そしてもちろんRubyコードに特有の類似性という問題は、今後あなたのコードを使う全ての追随者にも同じリスクを強いるでしょう。そんなリスクは時とともにより大きくなっていきます。

テストは必要、しかし不十分

> プログラムのテストとは、バグが今ここに存在することを表すために実施されます。バグがないということを証明するものではないのです!
エドガー・ダイクストラ – EWD249 構造化プログラミングに関する覚書

哀しいかな、たとえあなたがオブジェクト指向TDDにおいて第一人者と呼ばれるようになって、平面的な要素に深みが増し、よく練られたテストも網羅している完ぺきなコードをどんな時でも書けるようになったとしても、そんな知識は何ら助けにはなりません。


> Ruby: それはstdlibに搭載されているモジュールをrequireすると、整数割り算の動き方に影響を与えてしまう言語。 @tomdale

オブジェクト指向のテスト駆動開発については悟りを得た仏陀ですらこのような、コードを変え続けることができる全ての組み合わせを網羅するテストを書くことはできないでしょう。ランタイムで実行されるコードによって変わる動き方を、事前に推測するのは困難なことです。せいぜいのんびりコードを眺めながら、予期すらできない最悪の事態をがんばって想定してみることくらいしかできません。

ある時点で私たちはついに、こんなテストを使って骨格部分にパッチを当てようと試みること自体のバカバカしさに気づくでしょう。そもそも、もしあなたの銀行がそんな動きを容認するソフトウェアを採用していると知ったらどう思いますか?嫌だなと感じるなら、なぜそんなものをいつも使っているのですか?あなたのビジネスロジックはそれで良いのですか?

私たちが本当に必要としているものは、Rubyから得られるよりも明白な、全てのコード(重要なことは、自分で書いたコードだけではなく、クレイジーな粗悪品からのインポートコードも含むこと)における保証です。もう取り除かれることはないガードが、そして、正しく組み合わせなければコンパイルが通らないコードが必要なのです。

> 「ランチでビール飲んじゃう?」
「いいよ、今タイプセーフな言語を使っているからね」 @coach

正しいやり方は、長く維持されているRubyコードを見れば分かる

よりキレイなRubyのアプリケーションには次のような傾向があります:

  • 各機能がそれぞれ小さなオブジェクトに落とし込まれている
  • 可能なかぎり不変型オブジェクトが使われている(プリミティブ型を使ってなんとかしのいでいる、オブジェクトを不変化するadmantiumというgemを利用する、など)
  • ビジネスロジックが複数の機能に切り分けられ、上述したオブジェクトとして動作できるよう振り分けられている(サービスオブジェクト
  • 状態変化や副作用を可能なかぎり最小化している
  • ユニットテストにおいて、オブジェクトのインスタンス化およびメソッドの呼び出しで予測される引数の型が全て記載されている(静的型システムの模倣)

こうなってくると、もはや、このコードは関数型プログラミング言語の様相を呈しているとしか思えません。

> 可変オブジェクトを排除すると、オブジェクト指向が消える。それが、この5年間で分かったことだ(継承を使わないと仮定した場合)。 @garybernhardt

Haskellにハマっています

私は(クリス・アレンのガイドを使って)Haskellを学ぶことにしました。Rubyで起こる問題の多くは、Haskellを使えば解決すると思います。まだ学び始めたばかりなので、今の私はHaskellにハマっていると言えます。今のところ、とても気に入っていますが、実際にはまだあまり使っていません。それに私は、理論的なレベルで評価できるほど、数学理論/カテゴリー理論を分かっていません。しかし、賢い人たちの書いたものをたくさん読んで、納得しています。

Haskellの気に入っているところ:

  • 可変オブジェクトがない – 全てのデータ構造が不変。
  • 副作用がない – コードを呼び出す際に破壊的代入を心配する必要がありません。
  • 純粋関数であり、参照透明性がある – 同じ引数を持った関数の入力に対し、常に同じ出力を返すので、(繰り返しますが)副作用が起きません。等式推論が可能。
  • 静的型付け – 型エラーのあるプログラムはコンパイルできませんが、型システムが強力で、型推論があります – Javaの冗長性は不要です。
  • 前述の純粋関数、副作用の制御、不変性により並行処理が容易。
  • 型がドキュメントになる上に、コンパイラがチェックしてくれる。
  • コンパイルが通れば、多くの場合は動く
  • カテゴリー理論における基礎によるコード再利用率が高い。これはオブジェクト指向では実現できなかったことだと思います。

世の中には十分な学習材料があるし、クリス・アレン(@bitemyapp)のように意欲的に人に教えようとする人物もいると思います。だから、私のように数学に弱い人間であってもHaskellを使って本格的に仕事をすることができるのです。Haskellは奥が深いので、Haskellやその基となる数学理論(カテゴリー理論)にますます多くの時間を割いて、直接使用できる数学的に正しい抽象化(LensArrowなど)を新たに解き明かすつもりです。恐らく、いつか私も数学の天才になって、さらに強力な型システムを持つ言語(IdrisAgdaCoqのような依存型を持つ言語)を理解できるようになるでしょう(人は夢を見ることができるのです)。基本的には、Haskellはあっという間に飽きてしまうような言語ではなさそうです。

特効薬はないし、Rubyが悪いわけでもない

私はHaskellが完ぺきな言語だと思い込んでいるわけではありません(完ぺきな言語が存在すると思い込んでいるわけでもありません)。テストを書くのが心底嫌になったように見えるかもしれませんが、Haskellでもテストを書く必要がなくなるわけではないのです。ただRubyのような動的型付けの言語とは違い、不必要なテストを大量に書く手間が省けます。以前に私がこの話をした時にあきれた顔をした人たちに、このことをはっきりと伝えたいと思います。

私はRubyが死んだ、あるいは死にかけていると言っているわけでもありません。Rubyは当分消えることはないでしょう。Rubyの構文はとても読みやすく、習得しやすい言語デザインですし、世の中には大量のgemがあり、あらゆることができます。Rubyは良くも悪くも当面は多くの新人プログラマを引きつけるのではないかと思います。最も重要なのは、私が当面はRubyで金を稼ぎ続けることになるということです。

しかし、さらに強力な言語が次々に出てくるので、私自身は最近Rubyを楽しく使えていません。

2014年6月29日 エイブ・ヴォルカー