2015/05/14
いまどきの道具、たとえばRuby on Railsを使うと、ビックリするくらい簡単にWebアプリケーションを作れます。このような優れた道具を活用できるのだから、システム開発はいつも大成功で儲かってウハウハ……なんてことはなくて、プロジェクトは必ず炎上して働いても働いても我が暮らしは楽になりません。いったい、どうしてなのでしょうか?
実はRailsが便利じゃないなら、システム開発が楽にならないのは納得できます。
ただ、自分で書いておきなからなんなのですけれど、この仮説は明らかに間違ってそう。Railsの便利機能を紹介して、仮説が間違っていることを証明しましょう。Railsには様々な便利機能があるのですけど、今回は、検証機能を取り上げます。
Railsでは、入力値の検証を、モデルに対して記述します。たとえば社員モデルの社員番号と指名を必須入力としたいなら、以下のように記述します。とにかくやたらと簡単。
class Employee < ActiveRecord::Base
validates :code, :presence: true # この1行だけで、社員番号は必須入力になります。
validates :name, :presence: true # 氏名に関しても同様。
end
検証機能を使っている場合、そのユニット・テストの作成も簡単です。以下のような感じ。
test "validations" do
employee = employees(:employee_1) # テスト用のデータを取得
employee.validate! # テスト用のデータが正しいことを確認。
assert_raise ActiveRecord::RecordInvalid do # 社員番号の必須入力が正しく機能しているか確認。
employee.code = nil
employee.validate!
end
assert_raise ActiveRecord::RecordInvalid do # 氏名も同様に確認。
employee.name = nil
employee.validate!
end
end
少し補足します。必須チェックでエラーになるケースには、nil
の他にも""
(空文字列)や" "
(空白のみの文字列)や"\t"
(タブ文字のみの文字列)などが考えられます。もしRailsの検証機能を使わずにif
文でチェックしていたなら、これらのすべてのケースをテストしなければなりません。Railsの検証機能を使っている上のコードでは、それらがたった1つのnil
の場合のテストだけで済んでいるわけです。
あ、そうそう、Railsの流儀に従っているなら、入力値検証のテストはこれで終了です。ブラウザを開いての手動テストは不要。Railsのビューは、一般に以下のようなコードになるのですけど……。
<%= form_for(@employee) do |f| %>
<% if @employee.errors.any? %>
<div id="error_explanation">
<h2><%= @employee.errors.count %>件のエラーがあります。保存できませんでした。</h2>
<ul>
<% @employee.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :code %><br>
<%= f.text_field :code %>
</div>
<div class="field">
<%= f.label :name %><br>
<%= f.text_field :name %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
1行目で使っているform_for
や途中のlabel
、text_field
は、Railsには検証機能があることを知っています。なので、エラーがある項目については、<label>
や<input type="text">
をエラーがあることが分かるように表示してくれます(赤い枠で囲まれる)。if @employee.errors.any?
のブロックで、エラーの内容も表示できています。早い話が、Railsの流儀に従って上のコードのようにビューのコードを書くだけで、以下のキャプチャのように、エラーがある場合は適切な表示がなされるというわけです。
#次の仮説の検証コードなので画面に好きな言語が入っていますけど、無視してください。あと、CSSを書いていないため見た目が悪いですが、ご容赦ください。
うん、やっぱりRailsは便利ですね。わずか数行のコードを書いてRailsの流儀に従うだけで、手動のテストなしで入力値の検証を実現できるのですから。
「先程のは、要件が単純だから上手くいっただけだ。実際の開発ではもっと要件が難しくて……」みたいな意見があるかもしれません。検証してみましょう。
社員には「好きな言語」と「好きな業界」という属性があって、社員番号の最初の1桁が「D」(Developerの略。SEなどという日本でしか通用しない用語は絶対に使いません)の場合は「好きな言語」が必須、「S」(Sales staff)の場合は「好きな業界」が必須となるような場合を考えてみます。
この場合に対応するためにRailsのリファレンスを見てみたら、値が空でないかの検証に:if
というオプションがありました。これを使用してみます。
class Employee < ActiveRecord::Base
validates :code, presence: true
validates :name, presence: true
validates :favorite_language, presence: {if: -> (employee) { employee.code.try(:at, 0) == "D" }} # 1文字目がDなら、必須。
validates :favorite_industry, presence: {if: -> (employee) { employee.code.try(:at, 0) == "S" }} # 1文字目がSなら、必須。
end
ちょっと面倒なコードになっているのは、code
がnil
の場合を考慮しなければならないためです。なので、指定した箇所の文字を取得するメソッドであるat
を、try
で囲って呼び出しています。-> (parameter) { body }
は、Rubyのラムダ式です。
「いや、もっと要件が複雑な場合があり得る。職掌は、他の情報も含めて総合的に判断する。社員番号の1桁目が「D」であっても、営業部に所属している場合は営業として扱う」なんて場合もあるかもしれません。
ふむふむ、なるほど。社員番号は社員を一意に識別する番号なので、変更できない。でも、職掌は変更になり得る。だから、実は社員番号の1桁目での判断なんてのはすでに破綻していて、今は他の情報を使って無理やり判断している……ってそれ、単なる設計ミスじゃあないですか!
そもそも、「カラムXの値がαだったらカラムYの値はβでなければならない」ってのは、リレーショナル・データベースの鉄則である正規化に抵触します(言語や業界を別テーブルに分割していないという正規化違反は、すみません、無視します)。第三正規化は、キー以外は全て主キーに非推移的に関数従属するという、まぁ、私のような文系人間には何を言っているのかわからない話ではあるのですが、主キーの値が決まれば値が決まる属性Xの値が決まると値が決まる属性Yがある場合、属性Xと属性Yを別のテーブルにしましょうというものみたい(品番が「20BS0040JP」の商品(ThinkPad X1 Carbon)の商品カテゴリ番号は「1234」で、商品カテゴリ番号が「1234」の商品カテゴリ名は「コンピューター」のようば場合、商品カテゴリを別テーブルに分離する)。
第三正規化のポイントは全ての属性は主キーにのみ依存しなければならないというもので、もちろん正規化というのは値の話なのですけれど、意味的なところまで拡張して考えれば、favorite_languageやfavorite_industryの存在の有無が、他の属性に依存しているのはおかしいと考えられる訳です。
というわけで、オブジェクト指向屋ならば継承、Railsでは単一テーブル継承を使ってこの問題を根本解決してしまいましょう。
class Employee < ActiveRecord::Base
validates :code, presence: true
validates :name, presence: true
end
class Developer < Employee
validates :favorite_language, presence: true
pend
class SalesStaff < Employee
validates :favorite_industry, presence: true
end
ほら、面倒臭かった:if
の部分がなくなりました。これならとても簡単です。
少し補足。Railsの単一テーブル継承は、子孫クラス全てのカラム+文字列型のtype
というカラムを持ったテーブルを作ると、Railsが自動的にtype
の値と同じ名前のクラスのインスタンスを作ってくれるというものです。上の例で言えば、type
カラムの値が「Developer」ならばDeveloper
クラスのインスタンス、「SalesStaff」ならばSalesStaff
クラスのインスタンスが作成されるというわけ。そうそう、type
はごく普通の属性なので、一般的なオブジェクト指向言語では難しいクラスの変更もできます。なので、職掌の変更も可能です。残った問題は、社員番号の最初の一桁を今後はどうするかお客様と打ち合わせるだけ。
やっぱり、Railsには十分な機能がありますね。正しくモデルを設計すれば、一つ前の仮説検証の時と同じような単純なコードで入力値の検証ができるのですから。
どんなに便利な道具であっても、使いこなすのが難しいのであれば、上手く使いこなせなくて生産性が上がらないかもしれません。
でもね、Railsって難しくないと思うんですよ。以下に、私がRailsを簡単だと考えている理由を挙げます。
validates :presence
の:if
を知らなかったのですけれど、検索して3分で見つけられました。そもそも、私は普段はRubyではなくClojureを使っていて、実はRubyもRailsも初心者です。そして私は物忘れが激しくなった45歳のおっさん。それなのに普通に使えちゃうんですから、これはRailsが簡単である証拠と言えるのではないでしょうか。
これはもうアレだ。にわかには信じられないけれど、せっかくの便利機能を敢えて使ってないんじゃ……。
私がこんな無茶な仮説を出すのは、これまでの経験で無茶なやり方の開発がされるのを見てきたからです。
システム開発の現場では、画面の設計から入る方式をよく見ます。どのような入力項目があって、どのような入力値の検証をするかを書いた文書を作る方式です。このやり方の場合、画面Aと画面Bで入力値検証が異なる危険性があります。画面Aと画面Bでは同じだったとしても、このあと作られる画面Cの設計書でも同じ入力値検証になるかは、設計が終わるまで分かりません。だからそう、画面の設計を進めていく開発プロセスの場合、モデルに対して検証内容を設定していくRailsの検証機能は使えないわけです。if
を使って入力値を検証するロジックを書いてWebブラウザを開いて手動でテストしまくるわけで、それじゃあ生産性が上がるはずなんかありません。
オブジェクト指向のクラスのような大きな塊から順に詳細化していくのではなく、細かな項目である属性をいきなり決定していく設計の方式もよく見ます。「得意な言語に記入される内容は?どのような場合に入力が必須になりますか?入力がなかった場合のエラー・メッセージは?」みたいにヒアリングを進めていく感じ。このようなやり方で行く限り、単一テーブル継承を使って整理した場合のような単純なモデルは出来上がらないでしょう。:if
を使った面倒なロジックと、そのロジックを使う場合に対応するための面倒なテストが必要になってしまいます。こんなのが積み重なるのですから、そりゃあ、残業が増えますよ。
あと、そもそもRailsでは、データに対する操作画面のあり方を定義しています。Railsのルーティング情報はconfig/routes.rb
に書くのですけれど、ここにresources :employee
と書けば、それだけで社員を一覧、閲覧、登録、更新、削除するためのルーティングが定義されます。でも、「まず画面遷移を設計して……」という開発プロセスの場合は、このような便利機能は使えず、画面毎にURLやパラメーターを決めていくことになります。その場合はRailsの前提が壊れるので、使えない便利機能も出てきます。たとえば、普段は何も意識しないでも自動で実行されるのでこのエントリーでは触れていない、Webアプリケーションの脆弱性の一つであるCSRF(Cross Site Request Forgeries)への対策とかね。RailsとCSRFでGoogle検索するとCSRF対策を無効にする方法が見つかるわけですけれど、CSRF対策をオンにしたままだと動作できないようなRailsの流儀から外れた画面遷移を実装しなければならない場合は、まぁ、CSRF対策を無効にするしかありませんよね。で、アプリケーションを脆弱なまま放っておくわけにはいかないので、独自のCSRF対策を入れ、頑張ってテストする。Railsは生産性が高いというのを前提にしたコスト計算をしているなら、プロジェクトが炎上するのも当たり前でしょう。
うん、これが正しそうです。Railsの流儀から外れた設計を、それもモデルではなく画面として設計するプロジェクトが多いから、私は貧乏らしい。
でも、いったいどうして、Railsの流儀に従ったり、モデルとして抽象化してアプリケーション全体を設計したりすることができないのでしょうか?それについては、また今度考えさせてください。